From a10ac774ab5d47b505e840b029c0c0340002508b Mon Sep 17 00:00:00 2001 From: Timothy Jaeryang Baek Date: Thu, 29 Jan 2026 18:51:02 +0400 Subject: [PATCH] enh: manage shared chats --- backend/open_webui/models/chats.py | 51 +++++++ backend/open_webui/routers/chats.py | 43 ++++++ src/lib/apis/chats/index.ts | 49 +++++++ .../chat/Settings/DataControls.svelte | 20 ++- src/lib/components/icons/LinkSlash.svelte | 26 ++++ src/lib/components/layout/ChatsModal.svelte | 65 ++++++--- .../components/layout/SharedChatsModal.svelte | 126 ++++++++++++++++++ 7 files changed, 362 insertions(+), 18 deletions(-) create mode 100644 src/lib/components/icons/LinkSlash.svelte create mode 100644 src/lib/components/layout/SharedChatsModal.svelte diff --git a/backend/open_webui/models/chats.py b/backend/open_webui/models/chats.py index 12359eec9..eb0763048 100644 --- a/backend/open_webui/models/chats.py +++ b/backend/open_webui/models/chats.py @@ -168,6 +168,14 @@ class ChatTitleIdResponse(BaseModel): created_at: int +class SharedChatResponse(BaseModel): + id: str + title: str + share_id: Optional[str] = None + updated_at: int + created_at: int + + class ChatListResponse(BaseModel): items: list[ChatModel] total: int @@ -675,6 +683,49 @@ class ChatTable: all_chats = query.all() return [ChatModel.model_validate(chat) for chat in all_chats] + def get_shared_chat_list_by_user_id( + self, + user_id: str, + filter: Optional[dict] = None, + skip: int = 0, + limit: int = 50, + db: Optional[Session] = None, + ) -> list[ChatModel]: + + with get_db_context(db) as db: + query = db.query(Chat).filter_by(user_id=user_id).filter( + Chat.share_id.isnot(None) + ) + + if filter: + query_key = filter.get("query") + if query_key: + query = query.filter(Chat.title.ilike(f"%{query_key}%")) + + order_by = filter.get("order_by") + direction = filter.get("direction") + + if order_by and direction: + if not getattr(Chat, order_by, None): + raise ValueError("Invalid order_by field") + + if direction.lower() == "asc": + query = query.order_by(getattr(Chat, order_by).asc()) + elif direction.lower() == "desc": + query = query.order_by(getattr(Chat, order_by).desc()) + else: + raise ValueError("Invalid direction for ordering") + else: + query = query.order_by(Chat.updated_at.desc()) + + if skip: + query = query.offset(skip) + if limit: + query = query.limit(limit) + + all_chats = query.all() + return [ChatModel.model_validate(chat) for chat in all_chats] + def get_chat_list_by_user_id( self, user_id: str, diff --git a/backend/open_webui/routers/chats.py b/backend/open_webui/routers/chats.py index 885360aad..e03cdc7ba 100644 --- a/backend/open_webui/routers/chats.py +++ b/backend/open_webui/routers/chats.py @@ -16,6 +16,7 @@ from open_webui.models.chats import ( ChatResponse, Chats, ChatTitleIdResponse, + SharedChatResponse, ChatStatsExport, AggregateChatStats, ChatBody, @@ -858,6 +859,48 @@ async def unarchive_all_chats( return Chats.unarchive_all_chats_by_user_id(user.id, db=db) +############################ +# GetSharedChats +############################ + + +@router.get("/shared", response_model=list[SharedChatResponse]) +async def get_shared_session_user_chat_list( + page: Optional[int] = None, + query: Optional[str] = None, + order_by: Optional[str] = None, + direction: Optional[str] = None, + user=Depends(get_verified_user), + db: Session = Depends(get_session), +): + if page is None: + page = 1 + + limit = 60 + skip = (page - 1) * limit + + filter = {} + if query: + filter["query"] = query + if order_by: + filter["order_by"] = order_by + if direction: + filter["direction"] = direction + + chat_list = [ + SharedChatResponse(**chat.model_dump()) + for chat in Chats.get_shared_chat_list_by_user_id( + user.id, + filter=filter, + skip=skip, + limit=limit, + db=db, + ) + ] + + return chat_list + + ############################ # GetSharedChatById ############################ diff --git a/src/lib/apis/chats/index.ts b/src/lib/apis/chats/index.ts index b33072e89..bfe7a1638 100644 --- a/src/lib/apis/chats/index.ts +++ b/src/lib/apis/chats/index.ts @@ -255,6 +255,55 @@ export const getArchivedChatList = async ( })); }; +export const getSharedChatList = async ( + token: string = '', + page: number = 1, + filter?: object +) => { + let error = null; + + const searchParams = new URLSearchParams(); + searchParams.append('page', `${page}`); + + if (filter) { + Object.entries(filter).forEach(([key, value]) => { + if (value !== undefined && value !== null) { + searchParams.append(key, value.toString()); + } + }); + } + + const res = await fetch(`${WEBUI_API_BASE_URL}/chats/shared?${searchParams.toString()}`, { + 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.error(err); + return null; + }); + + if (error) { + throw error; + } + + return res.map((chat) => ({ + ...chat, + time_range: getTimeRange(chat.updated_at) + })); +}; + export const getAllChats = async (token: string) => { let error = null; diff --git a/src/lib/components/chat/Settings/DataControls.svelte b/src/lib/components/chat/Settings/DataControls.svelte index 748b2548c..5eed8918e 100644 --- a/src/lib/components/chat/Settings/DataControls.svelte +++ b/src/lib/components/chat/Settings/DataControls.svelte @@ -24,6 +24,7 @@ import { goto } from '$app/navigation'; import { toast } from 'svelte-sonner'; import ArchivedChatsModal from '$lib/components/layout/ArchivedChatsModal.svelte'; + import SharedChatsModal from '$lib/components/layout/SharedChatsModal.svelte'; import ConfirmDialog from '$lib/components/common/ConfirmDialog.svelte'; const i18n = getContext('i18n'); @@ -36,6 +37,7 @@ let showArchiveConfirmDialog = false; let showDeleteConfirmDialog = false; let showArchivedChatsModal = false; + let showSharedChatsModal = false; let chatImportInputElement: HTMLInputElement; @@ -136,6 +138,7 @@ +
-
{$i18n.t('Chat Management')}
+
{$i18n.t('Chats')}
@@ -218,6 +221,21 @@
+
+
+
{$i18n.t('Shared Chats')}
+ +
+
+
{$i18n.t('Archive All Chats')}
diff --git a/src/lib/components/icons/LinkSlash.svelte b/src/lib/components/icons/LinkSlash.svelte new file mode 100644 index 000000000..74b0bdee8 --- /dev/null +++ b/src/lib/components/icons/LinkSlash.svelte @@ -0,0 +1,26 @@ + + + diff --git a/src/lib/components/layout/ChatsModal.svelte b/src/lib/components/layout/ChatsModal.svelte index f9a4a6cc6..4df7ad21d 100644 --- a/src/lib/components/layout/ChatsModal.svelte +++ b/src/lib/components/layout/ChatsModal.svelte @@ -20,6 +20,9 @@ import XMark from '../icons/XMark.svelte'; import ChevronUp from '../icons/ChevronUp.svelte'; import ChevronDown from '../icons/ChevronDown.svelte'; + import Link from '../icons/Link.svelte'; + import LinkSlash from '../icons/LinkSlash.svelte'; + import Clipboard from '../icons/Clipboard.svelte'; const i18n = getContext('i18n'); @@ -46,6 +49,7 @@ export let loadHandler: null | Function = null; export let unarchiveHandler: null | Function = null; + export let unshareHandler: null | Function = null; const setSortKey = (key) => { if (orderBy === key) { @@ -286,30 +290,57 @@ {/if} - + {#if unshareHandler && chat.share_id} + + + + {/if} + +
diff --git a/src/lib/components/layout/SharedChatsModal.svelte b/src/lib/components/layout/SharedChatsModal.svelte new file mode 100644 index 000000000..55c5d6cb5 --- /dev/null +++ b/src/lib/components/layout/SharedChatsModal.svelte @@ -0,0 +1,126 @@ + + + { + onUpdate(); + init(); + }} + loadHandler={loadMoreChats} + {unshareHandler} +/>