From b4a58be193031827f2fc19b943646136d1e43575 Mon Sep 17 00:00:00 2001 From: Timothy Jaeryang Baek Date: Wed, 21 Oct 2026 09:15:15 +0900 Subject: [PATCH] refac --- backend/open_webui/models/chats.py | 12 +++ backend/open_webui/routers/chats.py | 33 ++++++++ src/lib/apis/chats/index.ts | 17 ++++ src/lib/components/chat/Chat.svelte | 81 +++++++++++++------ src/lib/components/chat/ChatControls.svelte | 5 +- .../components/chat/Controls/Controls.svelte | 11 ++- .../Settings/Advanced/AdvancedParams.svelte | 55 ++++++++++++- .../Settings/Advanced/CustomParams.svelte | 4 + src/lib/components/common/Textarea.svelte | 1 + 9 files changed, 189 insertions(+), 30 deletions(-) diff --git a/backend/open_webui/models/chats.py b/backend/open_webui/models/chats.py index 202d8cdc60..58469f05bb 100644 --- a/backend/open_webui/models/chats.py +++ b/backend/open_webui/models/chats.py @@ -852,6 +852,18 @@ class ChatTable: except Exception: return + async def update_chat_params_by_id(self, id: str, params: dict) -> ChatModel | None: + async with self._chat_transaction(id) as (_, chat): + if chat is None: + return None + + stored = chat.chat or {} + chat.chat = { + **stored, + 'params': self._clean_null_bytes({**(stored.get('params') or {}), **params}), + } + return ChatModel.model_validate(chat) + async def update_chat_variables_by_id( self, id: str, diff --git a/backend/open_webui/routers/chats.py b/backend/open_webui/routers/chats.py index 0ed3f5c738..e5bdf1916b 100644 --- a/backend/open_webui/routers/chats.py +++ b/backend/open_webui/routers/chats.py @@ -1482,6 +1482,39 @@ async def update_chat_by_id( ) +############################ +# UpdateChatParams +############################ + + +@router.patch('/{id}/params', response_model=dict) +async def update_chat_params( + request: Request, + id: str, + form_data: dict, + user=Depends(get_verified_user), + db: AsyncSession = Depends(get_async_session), +): + if not await Chats.get_chat_by_id_and_user_id(id, user.id, db=db): + raise HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, + detail=ERROR_MESSAGES.ACCESS_PROHIBITED, + ) + + chat = await Chats.update_chat_params_by_id(id, form_data) + if chat is None: + raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=ERROR_MESSAGES.NOT_FOUND) + + await publish_event( + request, + EVENTS.CHAT_UPDATED, + actor=user, + subject_id=id, + data={'title': chat.title}, + ) + return chat.chat['params'] + + ############################ # UpdateChatMessageById ############################ diff --git a/src/lib/apis/chats/index.ts b/src/lib/apis/chats/index.ts index 606d87fc9b..3218e455e6 100644 --- a/src/lib/apis/chats/index.ts +++ b/src/lib/apis/chats/index.ts @@ -1315,6 +1315,23 @@ export const updateChatById = async ( return res; }; +export const updateChatParams = async ( + token: string, + id: string, + params: Record +) => { + const res = await fetch(`${WEBUI_API_BASE_URL}/chats/${id}/params`, { + method: 'PATCH', + headers: { + 'Content-Type': 'application/json', + ...(token && { authorization: `Bearer ${token}` }) + }, + body: JSON.stringify(params) + }); + if (!res.ok) throw getErrorDetail(await res.json()); + return res.json(); +}; + export const compactChatById = async (token: string, id: string, model?: string | null) => { let error = null; diff --git a/src/lib/components/chat/Chat.svelte b/src/lib/components/chat/Chat.svelte index 6813c1b5a5..15f1a9ef8c 100644 --- a/src/lib/components/chat/Chat.svelte +++ b/src/lib/components/chat/Chat.svelte @@ -93,6 +93,7 @@ importChats, resolveChatMessageToolCall, updateChatById, + updateChatParams, updateChatFolderIdById } from '$lib/apis/chats'; import type { FilesystemFile } from '$lib/apis/terminal'; @@ -518,20 +519,13 @@ tool_approval_mode } }); + await saveParam('tool_approval_mode', tool_approval_mode); await updateUserSettings(localStorage.token, { ui: { params: $settings.params } }).catch( (err) => { console.error('[tool permissions settings]', err); } ); - if (!isShared && $chatId && !$temporaryChatEnabled && !isTemporaryChatId($chatId)) { - const res = await updateChatById(localStorage.token, $chatId, { params }).catch((err) => { - console.error('[tool permissions chat]', err); - return null; - }); - if (res) chat = res; - } - if (tool_approval_mode === 'full') { if (isTemporaryChatId($chatId)) { if (browserApprovalMode !== 'full') return; @@ -882,7 +876,7 @@ } let saveControlsTimer; - $: if (!loading && !$temporaryChatEnabled && $chatId && params && chatFiles) { + $: if (!loading && !$temporaryChatEnabled && $chatId && chatFiles) { clearTimeout(saveControlsTimer); saveControlsTimer = setTimeout(saveControls, 400); } @@ -3216,6 +3210,29 @@ const chatActionHandler = async (_chatId, actionId, modelId, responseMessageId, event = null) => { const messages = createMessagesList(history, responseMessageId); + let changed = false; + const actionSocket = $socket; + const onEvent = (event) => { + if (event.chat_id !== _chatId || event.message_id !== responseMessageId) return; + const { type, data } = event.data ?? {}; + // These events are UI-only or already persisted by get_event_emitter. + if ( + [ + 'notification', + 'confirmation', + 'input', + 'status', + 'message', + 'replace', + 'files', + 'embeds' + ].includes(type) || + (['source', 'citation'].includes(type) && data?.type == null) + ) + return; + changed = true; + }; + actionSocket?.on('events', onEvent); const res = await chatAction(localStorage.token, actionId, { model: modelId, @@ -3232,16 +3249,23 @@ chat_id: _chatId, session_id: $socket?.id, id: responseMessageId - }).catch((error) => { - toast.error(`${error}`); - messages.at(-1).error = { content: error }; - return null; - }); + }) + .catch((error) => { + toast.error(`${error}`); + if ($chatId === _chatId && history.messages[responseMessageId]) { + history.messages[responseMessageId].error = { content: error }; + changed = true; + } + return null; + }) + .finally(() => actionSocket?.off('events', onEvent)); - if (res !== null && res.messages) { + if ($chatId !== _chatId) return; + + if (res?.messages) { // Update chat history with the new messages for (const message of res.messages) { - history.messages[message.id] = { + const updated = { ...history.messages[message.id], ...(history.messages[message.id].content !== message.content ? { originalContent: history.messages[message.id].content } @@ -3251,16 +3275,19 @@ : {}), ...message }; + if (!equal(history.messages[message.id], updated)) { + history.messages[message.id] = updated; + changed = true; + } } } - if ($chatId == _chatId) { + if (changed) { if (!$temporaryChatEnabled) { chat = await updateChatById(localStorage.token, _chatId, { models: selectedModels, - messages: messages, + messages: createMessagesList(history, responseMessageId), history: history, - params: params, files: chatFiles }); @@ -4465,9 +4492,7 @@ // chat completion request. Files are now persisted // by the backend at chat creation time. if (Object.keys(params).length > 0) { - await updateChatById(localStorage.token, res.chat_id, { - params: params - }); + await updateChatParams(localStorage.token, res.chat_id, params); } } } @@ -4751,21 +4776,26 @@ models: selectedModels, history: history, messages: createMessagesList(history, history.currentId), - params: params, files: chatFiles }); } } }; + const saveParam = async (key: string, value: unknown) => { + if (isShared || !$chatId || $temporaryChatEnabled || isTemporaryChatId($chatId)) return; + await updateChatParams(localStorage.token, $chatId, { [key]: value }).catch((error) => { + toast.error(`${error}`); + }); + }; + const saveControls = async () => { if (isShared) return; if (!$chatId || $temporaryChatEnabled) return; const loaded = chat?.chat ?? {}; - if (equal(params, loaded.params ?? {}) && equal(chatFiles, loaded.files ?? [])) return; + if (equal(chatFiles, loaded.files ?? [])) return; const res = await updateChatById(localStorage.token, $chatId, { - params, files: chatFiles }).catch((err) => { console.error('[controls autosave]', err); @@ -5490,6 +5520,7 @@ bind:history bind:chatFiles bind:params + onParamChange={saveParam} bind:files chatId={$chatId} chatUser={chatOwner} diff --git a/src/lib/components/chat/ChatControls.svelte b/src/lib/components/chat/ChatControls.svelte index 2b07baa9b5..d8726b50ae 100644 --- a/src/lib/components/chat/ChatControls.svelte +++ b/src/lib/components/chat/ChatControls.svelte @@ -40,6 +40,7 @@ export let chatFiles = []; export let params = {}; + export let onParamChange: (key: string, value: unknown) => void = () => {}; export let bridge: import('$lib/utils/realtime').RealtimeCall; export let callMode = 'current'; @@ -307,7 +308,7 @@ {:else if activeTab === 'files' && codeInterpreterEnabled} {:else} - + {/if} @@ -437,7 +438,7 @@ {:else if activeTab === 'files' && codeInterpreterEnabled} {:else} - + {/if} diff --git a/src/lib/components/chat/Controls/Controls.svelte b/src/lib/components/chat/Controls/Controls.svelte index d45f8e4ecd..743019638f 100644 --- a/src/lib/components/chat/Controls/Controls.svelte +++ b/src/lib/components/chat/Controls/Controls.svelte @@ -12,7 +12,8 @@ import { user, settings } from '$lib/stores'; export let models = []; export let chatFiles = []; - export let params = {}; + export let params: Record = {}; + export let onParamChange: (key: string, value: unknown) => void = () => {}; export let embed = false; // Persist collapsible section open/close state @@ -118,6 +119,7 @@