mirror of
https://github.com/open-webui/open-webui.git
synced 2026-11-15 09:31:21 +00:00
refac
This commit is contained in:
parent
722f71f2cc
commit
193cf225f3
7 changed files with 105 additions and 78 deletions
|
|
@ -753,10 +753,11 @@ export const getChatListByTagName = async (token: string = '', tagName: string)
|
|||
}));
|
||||
};
|
||||
|
||||
export const getChatById = async (token: string, id: string) => {
|
||||
export const getChatById = async (token: string, id: string, signal?: AbortSignal) => {
|
||||
let error = null;
|
||||
|
||||
const res = await fetch(`${WEBUI_API_BASE_URL}/chats/${id}`, {
|
||||
signal,
|
||||
method: 'GET',
|
||||
headers: {
|
||||
Accept: 'application/json',
|
||||
|
|
@ -772,6 +773,7 @@ export const getChatById = async (token: string, id: string) => {
|
|||
return json;
|
||||
})
|
||||
.catch((err) => {
|
||||
if (signal?.aborted) return null;
|
||||
error = err.detail;
|
||||
|
||||
console.error(err);
|
||||
|
|
@ -1465,10 +1467,11 @@ export const deleteChatById = async (token: string, id: string) => {
|
|||
return res;
|
||||
};
|
||||
|
||||
export const getTagsById = async (token: string, id: string) => {
|
||||
export const getTagsById = async (token: string, id: string, signal?: AbortSignal) => {
|
||||
let error = null;
|
||||
|
||||
const res = await fetch(`${WEBUI_API_BASE_URL}/chats/${id}/tags`, {
|
||||
signal,
|
||||
method: 'GET',
|
||||
headers: {
|
||||
Accept: 'application/json',
|
||||
|
|
@ -1484,6 +1487,7 @@ export const getTagsById = async (token: string, id: string) => {
|
|||
return json;
|
||||
})
|
||||
.catch((err) => {
|
||||
if (signal?.aborted) return null;
|
||||
error = getErrorDetail(err);
|
||||
|
||||
console.error(err);
|
||||
|
|
|
|||
|
|
@ -361,10 +361,11 @@ export const stopTasksByChatId = async (token: string, chat_id: string) => {
|
|||
return res;
|
||||
};
|
||||
|
||||
export const getTaskIdsByChatId = async (token: string, chat_id: string) => {
|
||||
export const getTaskIdsByChatId = async (token: string, chat_id: string, signal?: AbortSignal) => {
|
||||
let error = null;
|
||||
|
||||
const res = await fetch(`${WEBUI_BASE_URL}/api/tasks/chat/${encodeURIComponent(chat_id)}`, {
|
||||
signal,
|
||||
method: 'GET',
|
||||
headers: {
|
||||
Accept: 'application/json',
|
||||
|
|
@ -377,6 +378,7 @@ export const getTaskIdsByChatId = async (token: string, chat_id: string) => {
|
|||
return res.json();
|
||||
})
|
||||
.catch((err) => {
|
||||
if (signal?.aborted) return null;
|
||||
console.error(err);
|
||||
if ('detail' in err) {
|
||||
error = err.detail;
|
||||
|
|
|
|||
|
|
@ -899,12 +899,20 @@
|
|||
}
|
||||
|
||||
let saveControlsTimer;
|
||||
$: if (!loading && !$temporaryChatEnabled && $chatId && chatFiles && selectedModels) {
|
||||
$: if (!loading && !$temporaryChatEnabled && $chatId && chatFiles) {
|
||||
clearTimeout(saveControlsTimer);
|
||||
saveControlsTimer = setTimeout(saveControls, 400);
|
||||
}
|
||||
|
||||
let chatLoadController = new AbortController();
|
||||
const startChatLoad = () => {
|
||||
chatLoadController.abort();
|
||||
chatLoadController = new AbortController();
|
||||
return chatLoadController.signal;
|
||||
};
|
||||
onDestroy(() => chatLoadController.abort());
|
||||
const navigateHandler = async () => {
|
||||
const signal = startChatLoad();
|
||||
modelDefaults = null;
|
||||
cancelBrowserInteractions();
|
||||
bridge?.end();
|
||||
|
|
@ -918,6 +926,7 @@
|
|||
|
||||
clearTimeout(saveControlsTimer);
|
||||
await saveControls();
|
||||
if (signal.aborted) return;
|
||||
loading = true;
|
||||
|
||||
prompt = '';
|
||||
|
|
@ -931,47 +940,17 @@
|
|||
imageGenerationEnabled = false;
|
||||
codeInterpreterEnabled = false;
|
||||
|
||||
const storageChatInput = sessionStorage.getItem(
|
||||
`chat-input${chatIdProp ? `-${chatIdProp}` : ''}`
|
||||
);
|
||||
|
||||
const loaded = chatIdProp ? await loadChat() : false;
|
||||
noteChatDebug('loadChat completed inside navigateHandler', { loaded });
|
||||
if (loaded) {
|
||||
await tick();
|
||||
loading = false;
|
||||
noteChatDebug('embedded chat loading false');
|
||||
if (!$page.url.searchParams.has('message')) window.setTimeout(() => scrollToBottom(), 0);
|
||||
|
||||
await tick();
|
||||
|
||||
// Mark chat read when initially loading it
|
||||
if (chatIdProp && !$temporaryChatEnabled) {
|
||||
updateLastReadAt(chatIdProp);
|
||||
}
|
||||
|
||||
// Process any queued requests if the chat is idle
|
||||
const lastMessage = history.currentId ? history.messages[history.currentId] : null;
|
||||
const isIdle = !lastMessage || lastMessage.role !== 'assistant' || lastMessage.done;
|
||||
if (isIdle) {
|
||||
await processNextInQueue(chatIdProp);
|
||||
}
|
||||
|
||||
await setDefaults(await restoreChatInput(storageChatInput));
|
||||
|
||||
messageInput?.focus({ preventScroll: true });
|
||||
if (chatIdProp) {
|
||||
await loadChat(signal);
|
||||
} else if (!embedded) {
|
||||
await goto('/');
|
||||
} else {
|
||||
loading = false;
|
||||
console.warn('[note-chat] embedded load failed; clearing spinner', {
|
||||
chatIdProp,
|
||||
activeChatId: $chatId
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const initEmbeddedDraft = async () => {
|
||||
chatLoadController.abort();
|
||||
modelDefaults = null;
|
||||
terminalContext = null;
|
||||
clearTimeout(saveControlsTimer);
|
||||
|
|
@ -1435,14 +1414,15 @@
|
|||
if (!id || $temporaryChatEnabled) return Promise.resolve();
|
||||
if (chatRefresh?.id === id) return chatRefresh.promise;
|
||||
const baseline = { ...history.messages };
|
||||
const signal = chatLoadController.signal;
|
||||
const promise = (async () => {
|
||||
try {
|
||||
const fresh = await getChatById(localStorage.token, id);
|
||||
if ($chatId !== id || joinedChatId !== id) return;
|
||||
const fresh = await getChatById(localStorage.token, id, signal);
|
||||
if ($chatId !== id || joinedChatId !== id || signal.aborted) return;
|
||||
chat = fresh;
|
||||
mergeChatMessages(fresh.chat.history, baseline);
|
||||
} catch (error) {
|
||||
if ($chatId === id && joinedChatId === id) {
|
||||
if ($chatId === id && joinedChatId === id && !signal.aborted) {
|
||||
clearPresence();
|
||||
chat = null;
|
||||
history = { messages: {}, currentId: null };
|
||||
|
|
@ -2639,6 +2619,7 @@
|
|||
};
|
||||
|
||||
const initNewChat = async () => {
|
||||
chatLoadController.abort();
|
||||
modelDefaults = null;
|
||||
clearTimeout(saveControlsTimer);
|
||||
await saveControls();
|
||||
|
|
@ -2893,7 +2874,7 @@
|
|||
messageInput?.focus({ preventScroll: true });
|
||||
};
|
||||
|
||||
const loadChat = async () => {
|
||||
const loadChat = async (signal = startChatLoad()) => {
|
||||
if (chatIdProp && chatIdProp !== $chatId) {
|
||||
terminalContext = null;
|
||||
history = { messages: {}, currentId: null };
|
||||
|
|
@ -2909,26 +2890,25 @@
|
|||
temporaryChatEnabled.set(false);
|
||||
}
|
||||
|
||||
chat = await getChatById(localStorage.token, $chatId).catch(async (error) => {
|
||||
const id = $chatId;
|
||||
const loadedChat = await getChatById(localStorage.token, id, signal).catch(async (error) => {
|
||||
console.error('[note-chat] getChatById failed', {
|
||||
chatIdProp,
|
||||
activeChatId: $chatId,
|
||||
error
|
||||
});
|
||||
if (!embedded) {
|
||||
await goto('/');
|
||||
}
|
||||
return null;
|
||||
});
|
||||
if (signal.aborted) return;
|
||||
noteChatDebug('getChatById completed', {
|
||||
found: !!chat,
|
||||
chatId: chat?.id,
|
||||
hasChatPayload: !!chat?.chat,
|
||||
title: chat?.title
|
||||
found: !!loadedChat,
|
||||
chatId: loadedChat?.id,
|
||||
hasChatPayload: !!loadedChat?.chat,
|
||||
title: loadedChat?.title
|
||||
});
|
||||
|
||||
if (chat) {
|
||||
tags = await getTagsById(localStorage.token, $chatId).catch(async (error) => {
|
||||
if (loadedChat) {
|
||||
const loadedTags = await getTagsById(localStorage.token, id, signal).catch(async (error) => {
|
||||
console.warn('[note-chat] getTagsById failed; continuing without tags', {
|
||||
chatIdProp,
|
||||
activeChatId: $chatId,
|
||||
|
|
@ -2936,6 +2916,9 @@
|
|||
});
|
||||
return [];
|
||||
});
|
||||
if (signal.aborted) return;
|
||||
chat = loadedChat;
|
||||
tags = loadedTags;
|
||||
noteChatDebug('getTagsById completed', { tagCount: tags?.length ?? 0 });
|
||||
|
||||
const chatContent = chat.chat;
|
||||
|
|
@ -3000,6 +2983,7 @@
|
|||
|
||||
autoScroll = true;
|
||||
await tick();
|
||||
if (signal.aborted) return;
|
||||
|
||||
// Mark all non-current assistant messages as done
|
||||
if (history.currentId) {
|
||||
|
|
@ -3020,7 +3004,7 @@
|
|||
// work (follow-ups, title gen) that shouldn't block the input.
|
||||
const activeTaskIds = taskIds;
|
||||
const currentMessage = history.currentId ? history.messages[history.currentId] : null;
|
||||
const pendingTaskIds = await getTaskIdsByChatId(localStorage.token, $chatId)
|
||||
const pendingTaskIds = await getTaskIdsByChatId(localStorage.token, id, signal)
|
||||
.then((res) => res?.task_ids ?? [])
|
||||
.catch((error) => {
|
||||
console.warn('[note-chat] getTaskIdsByChatId failed; continuing without tasks', {
|
||||
|
|
@ -3030,31 +3014,30 @@
|
|||
});
|
||||
return [];
|
||||
});
|
||||
if (signal.aborted) return;
|
||||
noteChatDebug('task reconciliation completed', {
|
||||
pendingTaskCount: pendingTaskIds.length,
|
||||
hasCurrentMessage: !!currentMessage
|
||||
});
|
||||
if (taskIds !== activeTaskIds) {
|
||||
noteChatDebug('task ids changed during load; aborting stale load');
|
||||
return;
|
||||
}
|
||||
const responseComplete = currentMessage?.role === 'assistant' && currentMessage?.done;
|
||||
|
||||
if (pendingTaskIds.length > 0 && !responseComplete) {
|
||||
taskIds = pendingTaskIds;
|
||||
} else {
|
||||
taskIds = null;
|
||||
// No active tasks and message incomplete → generation was interrupted
|
||||
if (pendingTaskIds.length === 0) {
|
||||
for (const message of Object.values(history.messages)) {
|
||||
if (
|
||||
message?.role === 'assistant' &&
|
||||
(message.user_id ?? chat?.user_id) === $user?.id &&
|
||||
!message.done &&
|
||||
!messageHasPendingAskUser(message)
|
||||
) {
|
||||
message.done = true;
|
||||
history = history;
|
||||
if (taskIds === activeTaskIds) {
|
||||
if (pendingTaskIds.length > 0 && !responseComplete) {
|
||||
taskIds = pendingTaskIds;
|
||||
} else {
|
||||
taskIds = null;
|
||||
// No active tasks and message incomplete → generation was interrupted
|
||||
if (pendingTaskIds.length === 0) {
|
||||
for (const message of Object.values(history.messages)) {
|
||||
if (
|
||||
message?.role === 'assistant' &&
|
||||
(message.user_id ?? chat?.user_id) === $user?.id &&
|
||||
!message.done &&
|
||||
!messageHasPendingAskUser(message)
|
||||
) {
|
||||
message.done = true;
|
||||
history = history;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -3062,6 +3045,27 @@
|
|||
|
||||
await tick();
|
||||
|
||||
if (signal.aborted) return;
|
||||
if (loading) {
|
||||
if (chatIdProp && !$temporaryChatEnabled) updateLastReadAt(chatIdProp);
|
||||
|
||||
const lastMessage = history.currentId ? history.messages[history.currentId] : null;
|
||||
if (!lastMessage || lastMessage.role !== 'assistant' || lastMessage.done) {
|
||||
await processNextInQueue(chatIdProp);
|
||||
}
|
||||
if (signal.aborted) return;
|
||||
const restored = await restoreChatInput(
|
||||
sessionStorage.getItem(`chat-input${chatIdProp ? `-${chatIdProp}` : ''}`)
|
||||
);
|
||||
if (signal.aborted) return;
|
||||
await setDefaults(restored);
|
||||
if (signal.aborted) return;
|
||||
loading = false;
|
||||
await tick();
|
||||
if (signal.aborted) return;
|
||||
if (!$page.url.searchParams.has('message')) scrollToBottom();
|
||||
messageInput?.focus({ preventScroll: true });
|
||||
}
|
||||
return true;
|
||||
} else {
|
||||
console.warn('[note-chat] chat response missing chat payload', {
|
||||
|
|
@ -3069,13 +3073,16 @@
|
|||
activeChatId: $chatId,
|
||||
chat
|
||||
});
|
||||
return null;
|
||||
}
|
||||
}
|
||||
console.warn('[note-chat] no chat returned from getChatById', {
|
||||
chatIdProp,
|
||||
activeChatId: $chatId
|
||||
});
|
||||
chat = null;
|
||||
if (!embedded) await goto('/');
|
||||
else loading = false;
|
||||
return false;
|
||||
};
|
||||
|
||||
const scrollToBottom = async (behavior = 'auto') => {
|
||||
|
|
@ -3769,6 +3776,7 @@
|
|||
|
||||
atSelectedModel = undefined;
|
||||
selectedModels = [model.id];
|
||||
saveControls(selectedModels);
|
||||
saveSessionSelectedModels();
|
||||
toast.success(`Model switched to: ${model.id}`);
|
||||
messageInput?.setText('');
|
||||
|
|
@ -4887,16 +4895,17 @@
|
|||
});
|
||||
};
|
||||
|
||||
const saveControls = async () => {
|
||||
const saveControls = async (models: string[] | null = null) => {
|
||||
if (isShared) return;
|
||||
if (!$chatId || $temporaryChatEnabled || chat?.id !== $chatId) return;
|
||||
const loaded = chat?.chat ?? {};
|
||||
if (equal(chatFiles, loaded.files ?? []) && equal(selectedModels, loaded.models ?? [])) return;
|
||||
// Model choices are passed explicitly; automatic fallbacks must not be saved.
|
||||
if (!models?.length || !models.every(Boolean)) models = null;
|
||||
if (!models && equal(chatFiles, chat.chat?.files ?? [])) return;
|
||||
const id = $chatId;
|
||||
|
||||
const res = await updateChatById(localStorage.token, id, {
|
||||
files: chatFiles,
|
||||
...(selectedModels.length && selectedModels.every(Boolean) ? { models: selectedModels } : {})
|
||||
...(models ? { models } : {})
|
||||
}).catch((err) => {
|
||||
console.error('[controls autosave]', err);
|
||||
return null;
|
||||
|
|
@ -5407,6 +5416,7 @@
|
|||
{history}
|
||||
{taskIds}
|
||||
bind:selectedModels
|
||||
onModelSelect={saveControls}
|
||||
bind:files
|
||||
bind:prompt
|
||||
bind:autoScroll
|
||||
|
|
@ -5504,6 +5514,7 @@
|
|||
{history}
|
||||
{taskIds}
|
||||
bind:selectedModels
|
||||
onModelSelect={saveControls}
|
||||
bind:files
|
||||
bind:prompt
|
||||
bind:autoScroll
|
||||
|
|
@ -5568,6 +5579,7 @@
|
|||
bind:selectedModelIdx
|
||||
{history}
|
||||
bind:selectedModels
|
||||
onModelSelect={saveControls}
|
||||
bind:messageInput
|
||||
bind:files
|
||||
bind:prompt
|
||||
|
|
|
|||
|
|
@ -166,6 +166,7 @@
|
|||
|
||||
export let atSelectedModel: Model | undefined = undefined;
|
||||
export let selectedModels: [''];
|
||||
export let onModelSelect: (models: string[]) => void = () => {};
|
||||
|
||||
let selectedModelIds = [];
|
||||
$: selectedModelIds = atSelectedModel !== undefined ? [atSelectedModel.id] : selectedModels;
|
||||
|
|
@ -2791,6 +2792,7 @@
|
|||
<ModelSelector
|
||||
bind:this={modelSelector}
|
||||
bind:selectedModels
|
||||
{onModelSelect}
|
||||
showSetDefault={!history?.currentId}
|
||||
placement="auto"
|
||||
align="end"
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@
|
|||
const i18n = getContext('i18n');
|
||||
|
||||
export let selectedModels = [''];
|
||||
export let onModelSelect: (models: string[]) => void = () => {};
|
||||
export let disabled = false;
|
||||
|
||||
export let showSetDefault = true;
|
||||
|
|
@ -85,6 +86,7 @@
|
|||
{disabled}
|
||||
bind:compareEnabled={compareModels}
|
||||
bind:values={selectedModels}
|
||||
{onModelSelect}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@
|
|||
import ConfirmDialog from '$lib/components/common/ConfirmDialog.svelte';
|
||||
import { flyAndScale } from '$lib/utils/transitions';
|
||||
|
||||
import { createEventDispatcher, onMount, getContext, tick } from 'svelte';
|
||||
import { onMount, getContext, tick } from 'svelte';
|
||||
|
||||
import { deleteModel, getOllamaVersion, pullModel } from '$lib/apis/ollama';
|
||||
import { deleteModelById } from '$lib/apis/models';
|
||||
|
|
@ -55,11 +55,11 @@
|
|||
import ModelItem from './ModelItem.svelte';
|
||||
|
||||
const i18n: any = getContext('i18n');
|
||||
const dispatch = createEventDispatcher();
|
||||
|
||||
export let id = '';
|
||||
export let value: string | null = '';
|
||||
export let values: string[] | null = null;
|
||||
export let onModelSelect: (models: string[]) => void = () => {};
|
||||
export let compareEnabled = false;
|
||||
export let multipleEnabled = false;
|
||||
export let disabled = false;
|
||||
|
|
@ -417,6 +417,7 @@
|
|||
if (!enabled && values) {
|
||||
values = [primaryValue || selectedValues[0] || ''];
|
||||
value = values[0];
|
||||
onModelSelect(values);
|
||||
}
|
||||
};
|
||||
|
||||
|
|
@ -433,11 +434,13 @@
|
|||
|
||||
values = nextValues.length ? nextValues : [item.value];
|
||||
value = values[0];
|
||||
onModelSelect(values);
|
||||
return;
|
||||
}
|
||||
|
||||
values = [item.value];
|
||||
value = item.value;
|
||||
onModelSelect(values);
|
||||
show = false;
|
||||
window.setTimeout(focusChatInput, 0);
|
||||
return;
|
||||
|
|
|
|||
|
|
@ -46,6 +46,7 @@
|
|||
|
||||
export let atSelectedModel: Model | undefined;
|
||||
export let selectedModels: [''];
|
||||
export let onModelSelect: (models: string[]) => void = () => {};
|
||||
|
||||
export let history;
|
||||
export let chatId = '';
|
||||
|
|
@ -262,6 +263,7 @@
|
|||
bind:this={messageInput}
|
||||
{history}
|
||||
bind:selectedModels
|
||||
{onModelSelect}
|
||||
bind:files
|
||||
bind:prompt
|
||||
bind:autoScroll
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue