diff --git a/src/lib/components/chat/Chat.svelte b/src/lib/components/chat/Chat.svelte index c8749c0b96..c4f1e7dfff 100644 --- a/src/lib/components/chat/Chat.svelte +++ b/src/lib/components/chat/Chat.svelte @@ -2389,7 +2389,7 @@ if (!$chatId) { const modelId = selectedModelIds[0]; const _chatId = await initChatHandler(history); - if (!$temporaryChatEnabled) { + if (_chatId && !$temporaryChatEnabled) { // Realtime replies need no chat completion; start metadata work at chat creation. void generateVoiceChatMetadata(_chatId, modelId, content, chat.title).catch((error) => { console.error('Could not save voice chat metadata', error); @@ -3793,6 +3793,37 @@ prompt = ''; }; + const getChatId = async (_history: typeof history) => { + let _chatId = $chatId; + // Empty embedded drafts create their backing chat only when the first message is sent. + if (!_chatId) { + if (embedded && onCreateEmbeddedChat) { + const createdChat = await onCreateEmbeddedChat(); + if (!createdChat?.id) { + toast.error($i18n.t('Failed to create chat')); + return null; + } + if (history !== _history) return null; + + chat = createdChat; + _chatId = createdChat.id; + loadedChatIdProp = _chatId; + await chatId.set(_chatId); + await chatTitle.set(createdChat?.chat?.title ?? createdChat?.title ?? $i18n.t('Chat')); + + params = structuredClone(createdChat?.chat?.params ?? {}); + delete params.note_id; + chatFiles = mergeFiles(chatFiles, createdChat?.chat?.files ?? []); + await onSelectEmbeddedChat?.(_chatId); + } else if ($temporaryChatEnabled) { + _chatId = createTemporaryChatId($socket?.id); + await chatId.set(_chatId); + } + await tick(); + } + return _chatId; + }; + const submitHandler = async ( userPrompt: string, { @@ -3800,6 +3831,7 @@ bridge: bridgeRequest = null }: { _raw?: boolean; bridge?: { userMessageId: string; modelId: string } | null } = {} ): Promise => { + if (processing === 'uploading') return { status: 'deferred' }; emitTyping(false); if (bridgeRequest && (selectedModelIds.length !== 1 || !bridgeRequest.modelId)) return { status: 'rejected' }; @@ -3880,6 +3912,26 @@ return { status: 'deferred' }; } + const currentHistory = history; + processing = 'uploading'; + try { + const _chatId = await getChatId(currentHistory); + if (_chatId === null) return { status: 'rejected' }; + + if (history !== currentHistory) return { status: 'rejected' }; + if ( + files.some((file) => file.status === 'pending' && file.file instanceof File) && + !(await messageInput?.uploadPendingFiles(_chatId || (await initChatHandler(history)))) + ) + return { status: 'rejected' }; + if (history !== currentHistory) return { status: 'rejected' }; + } catch (error) { + toast.error(`${error}`); + return { status: 'rejected' }; + } finally { + processing = ''; + } + if ( ($chatRequestQueues[$chatId] ?? []).some((m) => (m.files ?? []).some((file) => ['uploading', 'error'].includes(file.status)) @@ -4036,32 +4088,6 @@ } history = history; - // Empty embedded drafts create their backing chat only when the first message is sent. - if (!_chatId) { - if (embedded && onCreateEmbeddedChat) { - const createdChat = await onCreateEmbeddedChat(); - if (!createdChat?.id) { - toast.error($i18n.t('Failed to create chat')); - return; - } - - chat = createdChat; - _chatId = createdChat.id; - loadedChatIdProp = _chatId; - await chatId.set(_chatId); - await chatTitle.set(createdChat?.chat?.title ?? createdChat?.title ?? $i18n.t('Chat')); - - params = structuredClone(createdChat?.chat?.params ?? {}); - delete params.note_id; - chatFiles = mergeFiles(chatFiles, createdChat?.chat?.files ?? []); - await onSelectEmbeddedChat?.(_chatId); - } else if ($temporaryChatEnabled) { - _chatId = createTemporaryChatId($socket?.id); - await chatId.set(_chatId); - } - await tick(); - } - await tick(); // Re-clone history so sendMessageSocket gets the response messages we just added @@ -4405,8 +4431,7 @@ background_tasks: { ...(!$temporaryChatEnabled && (!_chatId || - (embedded && - (userMessage?.parentId ?? null) === null && + ((userMessage?.parentId ?? null) === null && createMessagesList(_history, responseMessageId).length === 2)) ? { title_generation: $settings?.title?.auto ?? true, @@ -4733,12 +4758,12 @@ } }; - const initChatHandler = async (history) => { + const initChatHandler = async (_history: typeof history) => { let _chatId = $chatId; const selectedFolderId = $selectedFolder?.id; if (!$temporaryChatEnabled) { - chat = await createNewChat( + const createdChat = await createNewChat( localStorage.token, { id: _chatId, @@ -4746,8 +4771,8 @@ models: selectedModels, system: $settings.system ?? undefined, params: params, - history: history, - messages: createMessagesList(history, history.currentId), + history: _history, + messages: createMessagesList(_history, _history.currentId), tags: [], timestamp: Date.now() }, @@ -4755,6 +4780,9 @@ chatVariables ); + if (!createdChat?.id) throw new Error($i18n.t('Failed to create chat')); + if (history !== _history || $chatId !== _chatId) return ''; + chat = createdChat; _chatId = chat.id; await chatId.set(_chatId); @@ -4828,7 +4856,7 @@ const getChatInputDraft = () => ({ prompt, files: files - .filter((file) => file.type !== 'image') + .filter((file) => file.type !== 'image' && !(file.file instanceof File)) .map((file) => ({ ...file, user: undefined, @@ -5475,6 +5503,7 @@ {:else}
file.type !== 'image') + .filter((file) => file.type !== 'image' && !(file.file instanceof File)) .map((file) => { return { ...file, @@ -980,6 +980,67 @@ ); }; + const uploadTerminalFile = async (fileItem, sessionId: string | null) => { + fileItem.status = 'uploading'; + fileItem.error = ''; + files = files; + try { + const terminal = getFilesystemUploadTerminal(fileItem.terminal_id); + if (!terminal) throw new Error('The attached file’s terminal is unavailable.'); + if (fileItem.session_id && fileItem.session_id !== sessionId) + throw new Error('Attached files must belong to this chat.'); + if (terminal.contexts?.chat?.context_id === 'chat_id' && !isSavedChatId(sessionId)) + throw new Error('A saved chat is required for this terminal'); + + const cwd = + (terminalContext?.terminalId === fileItem.terminal_id + ? terminalContext?.currentDirectory + : null) || (await getCwd(terminal.url, terminal.key, sessionId || undefined))?.cwd; + if (!cwd) throw new Error($i18n.t('Could not determine the upload folder.')); + const uploadedFile = await uploadNewFileToTerminal( + terminal.url, + terminal.key, + cwd, + fileItem.file, + sessionId || undefined + ); + if (!uploadedFile) throw new Error($i18n.t('Failed to upload file.')); + + fileItem.status = 'uploaded'; + fileItem.session_id = sessionId; + fileItem.name = uploadedFile.path.split('/').pop() || fileItem.name; + fileItem.id = uploadedFile.path; + fileItem.path = uploadedFile.path; + fileItem.url = uploadedFile.path; + fileItem.size = uploadedFile.size ?? fileItem.size; + fileItem.file = uploadedFile; + showFileNavDir.set(uploadedFile.path); + } catch (error) { + fileItem.status = 'error'; + fileItem.error = `${error}`; + toast.error(fileItem.error); + return false; + } finally { + files = files; + } + return true; + }; + + export const uploadPendingFiles = async (sessionId: string) => { + if (!sessionId) return false; + for (const fileItem of files.filter( + (item) => item.status === 'pending' && item.file instanceof File + )) { + if (chatId !== sessionId) return false; + if (!(await uploadTerminalFile(fileItem, sessionId))) { + fileItem.status = 'pending'; + files = files; + return false; + } + } + return !files.some((item) => item.status === 'pending' && item.file instanceof File); + }; + const uploadFileHandler = async (file, process = true, itemData = {}) => { if ($_user?.role !== 'admin' && !($_user?.permissions?.chat?.file_upload ?? true)) { toast.error($i18n.t('You do not have permission to upload files.')); @@ -1019,54 +1080,20 @@ files = [...files, fileItem]; if (filesystemUploadTerminal) { + fileItem.type = 'filesystem'; + fileItem.file = file; fileItem.terminal_id = $selectedTerminalId; fileItem.session_id = chatId || null; - try { - const cwd = - (terminalContext?.terminalId === $selectedTerminalId - ? terminalContext?.currentDirectory - : null) || - ( - await getCwd( - filesystemUploadTerminal.url, - filesystemUploadTerminal.key, - fileItem.session_id || undefined - ) - )?.cwd; - if (!cwd) throw new Error($i18n.t('Could not determine the upload folder.')); - const uploadedFile = await uploadNewFileToTerminal( - filesystemUploadTerminal.url, - filesystemUploadTerminal.key, - cwd, - file, - fileItem.session_id || undefined - ); - - if (uploadedFile) { - fileItem.type = 'filesystem'; - fileItem.status = 'uploaded'; - fileItem.name = uploadedFile.path.split('/').pop() || file.name; - fileItem.id = uploadedFile.path; - fileItem.path = uploadedFile.path; - fileItem.url = uploadedFile.path; - fileItem.size = uploadedFile.size ?? file.size; - fileItem.file = uploadedFile; - files = files; - showFileNavDir.set(uploadedFile.path); - } else { - fileItem.status = 'error'; - fileItem.error = $i18n.t('Failed to upload file.'); - toast.error(fileItem.error); - files = files.filter((item) => item?.itemId !== tempItemId); - } - } catch (e) { - fileItem.status = 'error'; - fileItem.error = `${e}`; - toast.error(`${e}`); - files = files.filter((item) => item?.itemId !== tempItemId); - } finally { - onUpdate({ file: fileItem }); + if (!chatId && filesystemUploadTerminal.contexts?.chat?.context_id === 'chat_id') { + fileItem.status = 'pending'; + files = files; + return; } + + if (!(await uploadTerminalFile(fileItem, fileItem.session_id))) { + files = files.filter((item) => item?.itemId !== tempItemId); + } + onUpdate({ file: fileItem }); return; } @@ -2883,38 +2910,28 @@
{:else}
- +
diff --git a/src/lib/components/chat/Placeholder.svelte b/src/lib/components/chat/Placeholder.svelte index 52d49db818..908842c388 100644 --- a/src/lib/components/chat/Placeholder.svelte +++ b/src/lib/components/chat/Placeholder.svelte @@ -48,6 +48,7 @@ export let selectedModels: ['']; export let history; + export let chatId = ''; export let prompt = ''; export let files = []; @@ -252,6 +253,7 @@
{#if !($selectedFolder && folderReadOnly)}