From bfbfdae1c5c8aa2f5b94d7f9738d70b061b4abe2 Mon Sep 17 00:00:00 2001 From: Jun Siang Cheah Date: Sun, 31 Mar 2024 22:02:40 +0100 Subject: [PATCH 01/24] feat: add backend functions for sharing chats --- backend/apps/web/models/chats.py | 41 ++++++++++++++++++ backend/apps/web/routers/chats.py | 71 +++++++++++++++++++++++++++++++ 2 files changed, 112 insertions(+) diff --git a/backend/apps/web/models/chats.py b/backend/apps/web/models/chats.py index c9d130044..5a49354ba 100644 --- a/backend/apps/web/models/chats.py +++ b/backend/apps/web/models/chats.py @@ -20,6 +20,7 @@ class Chat(Model): title = CharField() chat = TextField() # Save Chat JSON as Text timestamp = DateField() + share_id = CharField(null=True, unique=True) class Meta: database = DB @@ -31,6 +32,7 @@ class ChatModel(BaseModel): title: str chat: str timestamp: int # timestamp in epoch + share_id: Optional[str] = None #################### @@ -52,6 +54,7 @@ class ChatResponse(BaseModel): title: str chat: dict timestamp: int # timestamp in epoch + share_id: Optional[str] = None # id of the chat to be shared class ChatTitleIdResponse(BaseModel): @@ -95,6 +98,44 @@ class ChatTable: except: return None + def insert_shared_chat(self, chat_id: str) -> Optional[ChatModel]: + # Get the existing chat to share + chat = Chat.get(Chat.id == chat_id) + # Check if the chat is already shared + if chat.share_id: + return self.get_chat_by_id_and_user_id(chat.share_id, "shared") + # Create a new chat with the same data, but with a new ID + shared_chat = ChatModel( + **{ + "id": str(uuid.uuid4()), + "user_id": "shared", + "title": chat.title, + "chat": chat.chat, + "timestamp": int(time.time()), + } + ) + shared_result = Chat.create(**shared_chat.model_dump()) + # Update the original chat with the share_id + result = ( + Chat.update(share_id=shared_chat.id).where(Chat.id == chat_id).execute() + ) + + return shared_chat if (shared_result and result) else None + + def update_chat_share_id_by_id( + self, od: str, share_id: Optional[str] + ) -> Optional[ChatModel]: + try: + query = Chat.update( + share_id=share_id, + ).where(Chat.id == id) + query.execute() + + chat = Chat.get(Chat.id == id) + return ChatModel(**model_to_dict(chat)) + except: + return None + def get_chat_lists_by_user_id( self, user_id: str, skip: int = 0, limit: int = 50 ) -> List[ChatModel]: diff --git a/backend/apps/web/routers/chats.py b/backend/apps/web/routers/chats.py index 5f8c61b70..91b8a7343 100644 --- a/backend/apps/web/routers/chats.py +++ b/backend/apps/web/routers/chats.py @@ -189,6 +189,77 @@ async def delete_chat_by_id(request: Request, id: str, user=Depends(get_current_ return result +############################ +# ShareChatById +############################ + + +@router.post("/{id}/share", response_model=Optional[ChatResponse]) +async def share_chat_by_id(id: str, user=Depends(get_current_user)): + chat = Chats.get_chat_by_id_and_user_id(id, user.id) + if chat: + if chat.share_id: + shared_chat = Chats.get_chat_by_id_and_user_id(chat.share_id, "shared") + return ChatResponse( + **{**shared_chat.model_dump(), "chat": json.loads(shared_chat.chat)} + ) + + shared_chat = Chats.insert_shared_chat(chat.id) + if not shared_chat: + raise HTTPException( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + detail=ERROR_MESSAGES.DEFAULT(), + ) + + return ChatResponse( + **{**shared_chat.model_dump(), "chat": json.loads(shared_chat.chat)} + ) + else: + raise HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, + detail=ERROR_MESSAGES.ACCESS_PROHIBITED, + ) + + +############################ +# DeletedSharedChatById +############################ + + +@router.delete("/{id}/share", response_model=Optional[bool]) +async def delete_shared_chat_by_id(id: str, user=Depends(get_current_user)): + chat = Chats.get_chat_by_id_and_user_id(id, user.id) + if chat: + if not chat.share_id: + return False + result = Chats.delete_chat_by_id_and_user_id(chat.share_id, "shared") + update_result = Chats.update_chat_share_id_by_id(chat.id, None) + + return result and update_result + else: + raise HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, + detail=ERROR_MESSAGES.ACCESS_PROHIBITED, + ) + + +############################ +# GetSharedChatById +############################ + + +@router.get("/share/{id}", response_model=Optional[ChatResponse]) +async def get_shared_chat_by_id(id: str, user=Depends(get_current_user)): + chat = Chats.get_chat_by_id_and_user_id(id, "shared") + + if chat: + return ChatResponse(**{**chat.model_dump(), "chat": json.loads(chat.chat)}) + else: + raise HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, detail=ERROR_MESSAGES.NOT_FOUND + ) + + ############################ # GetChatTagsById ############################ From 196f91d68c9211a37f9d902ef50a6f3cfcb187fa Mon Sep 17 00:00:00 2001 From: Jun Siang Cheah Date: Sun, 31 Mar 2024 22:03:28 +0100 Subject: [PATCH 02/24] feat: add frontend support for locally sharing chats --- src/lib/apis/chats/index.ts | 96 +++++++++ src/lib/components/chat/Messages.svelte | 3 + .../chat/Messages/ResponseMessage.svelte | 158 ++++++++------- .../chat/Messages/UserMessage.svelte | 49 ++--- src/lib/components/chat/ShareChatModal.svelte | 12 ++ src/lib/components/layout/Navbar.svelte | 30 ++- src/routes/(app)/+page.svelte | 2 +- src/routes/(app)/c/[id]/+page.svelte | 2 +- src/routes/(app)/s/[id]/+page.svelte | 187 ++++++++++++++++++ 9 files changed, 433 insertions(+), 106 deletions(-) create mode 100644 src/routes/(app)/s/[id]/+page.svelte diff --git a/src/lib/apis/chats/index.ts b/src/lib/apis/chats/index.ts index 35b259d56..4fcd1b639 100644 --- a/src/lib/apis/chats/index.ts +++ b/src/lib/apis/chats/index.ts @@ -218,6 +218,102 @@ export const getChatById = async (token: string, id: string) => { return res; }; +export const getChatByShareId = async (token: string, share_id: string) => { + let error = null; + + const res = await fetch(`${WEBUI_API_BASE_URL}/chats/share/${share_id}`, { + method: 'GET', + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json', + ...(token && { authorization: `Bearer ${token}` }) + } + }) + .then(async (res) => { + if (!res.ok) throw await res.json(); + return res.json(); + }) + .then((json) => { + return json; + }) + .catch((err) => { + error = err; + + console.log(err); + return null; + }); + + if (error) { + throw error; + } + + return res; +}; + +export const shareChatById = async (token: string, id: string) => { + let error = null; + + const res = await fetch(`${WEBUI_API_BASE_URL}/chats/${id}/share`, { + method: 'POST', + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json', + ...(token && { authorization: `Bearer ${token}` }) + } + }) + .then(async (res) => { + if (!res.ok) throw await res.json(); + return res.json(); + }) + .then((json) => { + return json; + }) + .catch((err) => { + error = err; + + console.log(err); + return null; + }); + + if (error) { + throw error; + } + + return res; +}; + +export const deleteSharedChatById = async (token: string, id: string) => { + let error = null; + + const res = await fetch(`${WEBUI_API_BASE_URL}/chats/${id}`, { + method: 'DELETE', + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json', + ...(token && { authorization: `Bearer ${token}` }) + } + }) + .then(async (res) => { + if (!res.ok) throw await res.json(); + return res.json(); + }) + .then((json) => { + return json; + }) + .catch((err) => { + error = err; + + console.log(err); + return null; + }); + + if (error) { + throw error; + } + + return res; +}; + export const updateChatById = async (token: string, id: string, chat: object) => { let error = null; diff --git a/src/lib/components/chat/Messages.svelte b/src/lib/components/chat/Messages.svelte index 7afb5c376..4cd97ca80 100644 --- a/src/lib/components/chat/Messages.svelte +++ b/src/lib/components/chat/Messages.svelte @@ -16,6 +16,7 @@ const i18n = getContext('i18n'); export let chatId = ''; + export let readOnly = false; export let sendPrompt: Function; export let continueGeneration: Function; export let regenerateResponse: Function; @@ -317,6 +318,7 @@ messageDeleteHandler(message.id)} user={$user} + {readOnly} {message} isFirstMessage={messageIdx === 0} siblings={message.parentId !== null @@ -335,6 +337,7 @@ modelfiles={selectedModelfiles} siblings={history.messages[message.parentId]?.childrenIds ?? []} isLastMessage={messageIdx + 1 === messages.length} + {readOnly} {confirmEditResponseMessage} {showPreviousMessage} {showNextMessage} diff --git a/src/lib/components/chat/Messages/ResponseMessage.svelte b/src/lib/components/chat/Messages/ResponseMessage.svelte index 91bb35c47..7afa7d03e 100644 --- a/src/lib/components/chat/Messages/ResponseMessage.svelte +++ b/src/lib/components/chat/Messages/ResponseMessage.svelte @@ -33,6 +33,8 @@ export let isLastMessage = true; + export let readOnly = false; + export let confirmEditResponseMessage: Function; export let showPreviousMessage: Function; export let showNextMessage: Function; @@ -469,31 +471,33 @@ {/if} - - - + + + + + + {/if} - + + + - - - + + + + {/if} - + + + + + + {/if} + +
{$i18n.t('or')}
diff --git a/src/lib/components/layout/Navbar.svelte b/src/lib/components/layout/Navbar.svelte index a98689618..198aa97d4 100644 --- a/src/lib/components/layout/Navbar.svelte +++ b/src/lib/components/layout/Navbar.svelte @@ -5,7 +5,7 @@ const { saveAs } = fileSaver; import { Separator } from 'bits-ui'; - import { getChatById } from '$lib/apis/chats'; + import { getChatById, shareChatById } from '$lib/apis/chats'; import { WEBUI_NAME, chatId, modelfiles, settings, showSettings } from '$lib/stores'; import { slide } from 'svelte/transition'; @@ -19,6 +19,7 @@ import ChevronUpDown from '../icons/ChevronUpDown.svelte'; import Menu from './Navbar/Menu.svelte'; import TagChatModal from '../chat/TagChatModal.svelte'; + import { copyToClipboard } from '$lib/utils'; const i18n = getContext('i18n'); @@ -32,7 +33,7 @@ export let addTag: Function; export let deleteTag: Function; - export let showModelSelector = false; + export let showModelSelector = true; let showShareChatModal = false; let showTagChatModal = false; @@ -64,6 +65,23 @@ ); }; + const shareLocalChat = async () => { + const chat = await getChatById(localStorage.token, $chatId); + console.log('shareLocal', chat); + if (chat.share_id) { + const shareUrl = `${window.location.origin}/s/${chat.share_id}`; + toast.info( + $i18n.t('Chat is already shared at {{shareUrl}}, copied to clipboard', { shareUrl }) + ); + copyToClipboard(shareUrl); + } else { + const sharedChat = await shareChatById(localStorage.token, $chatId); + const shareUrl = `${window.location.origin}/s/${sharedChat.id}`; + toast.info($i18n.t('Chat is now shared at {{shareUrl}}, copied to clipboard', { shareUrl })); + copyToClipboard(shareUrl); + } + }; + const downloadChat = async () => { const chat = (await getChatById(localStorage.token, $chatId)).chat; console.log('download', chat); @@ -80,7 +98,7 @@ }; - +