diff --git a/src/App.jsx b/src/App.jsx index 4eb712e..ffa4894 100644 --- a/src/App.jsx +++ b/src/App.jsx @@ -1085,324 +1085,6 @@ export default function App() { } -async function sendMessage() { - const trimmedInput = input.trim() - if (isSending || (!trimmedInput && composerAttachments.length === 0) || !model) return - if (composerAttachments.some(attachmentIsImage) && !canAttachImages) { - window.alert(imageAttachmentUnavailableReason) - return - } - - let targetSessionId = activeSessionId - let isNewChat = false - if (!targetSessionId) { - const newSession = await createNewChat() - await new Promise(resolve => setTimeout(resolve, 200)) - targetSessionId = newSession.session_id - isNewChat = true - } else { - const currentSession = chatSessions.find(s => s.session_id === targetSessionId) - isNewChat = currentSession && currentSession.name === "New Chat" && currentSession.messages.length === 0 - } - - const existingMessages = (chatSessions.find(s => s.session_id === targetSessionId)?.messages) || [] - const outgoingAttachments = composerAttachments.map(({ id, ...attachment }) => ({ ...attachment })) - const historyNeedsVision = existingMessages.some(messageHasImageAttachments) - if (historyNeedsVision && !canAttachImages) { - window.alert(imageAttachmentUnavailableReason) - return - } - const composerSnapshot = input - const attachmentSnapshot = composerAttachments.map(attachment => ({ ...attachment })) - const userMsg = { - role: 'user', - content: trimmedInput, - attachments: outgoingAttachments, - id: `msg-${Date.now()}-${Math.random()}` - } - setIsAttachmentMenuOpen(false) - setUserScrolledUp(targetSessionId, false) - - if (activeSessionIdRef.current === targetSessionId) { - restoredForRef.current = activeSessionIdRef.current - } - - flushSync(() => { - setChatSessions(prevSessions => - prevSessions.map(session => - session.session_id === targetSessionId - ? { ...session, messages: [...(session.messages || []), userMsg] } - : session - ) - ) - setInput('') - setComposerAttachments([]) - }) - requestAnimationFrame(() => scrollToBottom('auto', targetSessionId)) - - const requestController = beginCancelableRequest(targetSessionId) - try { - let historyForSearch = [] - if (userMsg.content) try { - const existing = (chatSessions.find(s => s.session_id === targetSessionId)?.messages) || [] - const lastFew = existing.slice(-8).map(m => ({ role: m.role, content: m.content || '' })) - historyForSearch = [...lastFew, { role: 'user', content: userMsg.content }] - } catch {} - - let enrichedPrompt = userMsg.content || null - let citationSources = [] - const contextBlocks = [] - - const selectedLibrary = getChatLibraryForSession(targetSessionId) - - if (userMsg.content && selectedLibrary?.states?.is_indexed) { - try { - const localContext = await fetchLocalLibraryContext(backendApiUrl, selectedLibrary.slug, userMsg.content, requestController.signal) - if (localContext.contextBlock) { - contextBlocks.push(localContext.contextBlock) - } - if (Array.isArray(localContext.sources)) { - citationSources.push(...localContext.sources) - } - } catch (error) { - if (isAbortError(error)) throw error - console.warn('local library enrichment failed', error) - } - } - - if (userMsg.content && webSearchEnabled) { - try { - const searchContext = await fetchWebSearchContext({ - apiBase: backendApiUrl, - engines: searxEngines, - historyLimit: 8, - messages: historyForSearch, - model, - prompt: userMsg.content, - searxUrl, - signal: requestController.signal, - }) - if (searchContext.contextBlock) { - contextBlocks.push(searchContext.contextBlock) - } - if (Array.isArray(searchContext.sources)) { - citationSources.push(...searchContext.sources) - } - } catch (error) { - if (isAbortError(error)) throw error - console.warn('web search enrichment failed', error) - } - } - - citationSources = [...new Set(citationSources)] - if (userMsg.content && contextBlocks.length > 0) { - enrichedPrompt = `${userMsg.content}\n\n${contextBlocks.join('\n\n')}` - } - - if (streamOutput) { - const assistantMsgId = `msg-${Date.now()}-${Math.random()}` - let fullReply = '' - const assistantMsg = { role: 'assistant', content: '', id: assistantMsgId, sources: citationSources } - setChatSessions(prevSessions => - prevSessions.map(session => - session.session_id === targetSessionId - ? { ...session, messages: [...(session.messages || []), assistantMsg] } - : session - ) - ) - - try { - const res = await postChatMessage({ - apiBase: backendApiUrl, - attachments: outgoingAttachments, - enrichedMessage: userMsg.content && contextBlocks.length > 0 ? enrichedPrompt : null, - message: userMsg.content, - model, - sessionId: targetSessionId, - signal: requestController.signal, - sources: citationSources || [], - stream: true, - transcriptionModel, - visionModel, - }) - if (!res.ok) { - const error = new Error(await readBackendErrorText(res)) - error.status = res.status - throw error - } - - const reader = res.body?.getReader() - if (!reader) throw new Error('Missing response body') - - const decoder = new TextDecoder() - let pendingMarked = false - - while (true) { - const { value, done } = await reader.read() - if (done) { - setAssistantMessageContent(targetSessionId, assistantMsgId, fullReply) - - if (activeSessionIdRef.current === targetSessionId) { - if (!userScrolledUpRef.current[targetSessionId]) { - requestAnimationFrame(() => scrollMessageToTop(assistantMsgId, 'smooth', targetSessionId)) - } else { - setNewMsgTip(prev => ({ ...prev, [targetSessionId]: assistantMsgId })) - } - } else { - setPendingScrollToLastUser(prev => ({ ...prev, [targetSessionId]: assistantMsgId })) - setUnreadSessions(prev => [...new Set([...prev, targetSessionId])]) - } - - break - } - - const chunk = decoder.decode(value, { stream: true }) - fullReply += chunk - setAssistantMessageContent(targetSessionId, assistantMsgId, fullReply) - - if (activeSessionIdRef.current === targetSessionId && !userScrolledUpRef.current[targetSessionId]) { - scrollToBottom('auto', targetSessionId) - } - if (activeSessionIdRef.current !== targetSessionId && !pendingMarked) { - setPendingScrollToLastUser(prev => ({ ...prev, [targetSessionId]: assistantMsgId })) - pendingMarked = true - } - } - } catch (error) { - if (isAbortError(error)) { - setAssistantMessageContent(targetSessionId, assistantMsgId, fullReply, { removeIfEmpty: true }) - return - } - - console.error('Failed to send message:', error) - if (Number(error?.status) >= 400 && Number(error?.status) < 500) { - setAssistantMessageContent(targetSessionId, assistantMsgId, fullReply, { removeIfEmpty: true }) - setChatSessions(prevSessions => - prevSessions.map(session => - session.session_id === targetSessionId - ? { - ...session, - messages: (session.messages || []).filter(message => message.id !== userMsg.id), - } - : session - ) - ) - setInput(composerSnapshot) - setComposerAttachments(attachmentSnapshot) - window.alert(getErrorText(error)) - return - } - setAssistantMessageContent(targetSessionId, assistantMsgId, 'Error: ' + getErrorText(error), { removeIfEmpty: true }) - return - } - } else { - const res = await postChatMessage({ - apiBase: backendApiUrl, - attachments: outgoingAttachments, - enrichedMessage: userMsg.content && contextBlocks.length > 0 ? enrichedPrompt : null, - message: userMsg.content, - model, - sessionId: targetSessionId, - signal: requestController.signal, - sources: citationSources || [], - stream: false, - transcriptionModel, - visionModel, - }) - if (!res.ok) { - const error = new Error(await readBackendErrorText(res)) - error.status = res.status - throw error - } - - const data = await res.json() - const assistantMsgId = `msg-${Date.now()}` - const assistantMsg = { - role: 'assistant', - content: data.reply, - id: assistantMsgId, - sources: citationSources - } - - setChatSessions(prevSessions => - prevSessions.map(session => - session.session_id === targetSessionId - ? { ...session, messages: [...(session.messages || []), assistantMsg] } - : session - ) - ) - - if (assistantMsgId) { - if (activeSessionIdRef.current === targetSessionId) { - if (!userScrolledUpRef.current[targetSessionId]) { - requestAnimationFrame(() => scrollMessageToTop(assistantMsgId, 'smooth', targetSessionId)) - } else { - setNewMsgTip(prev => ({ ...prev, [targetSessionId]: assistantMsgId })) - } - } else { - setPendingScrollToLastUser(prev => ({ ...prev, [targetSessionId]: assistantMsgId })) - } - } - } - - if (activeSessionIdRef.current !== targetSessionId) { - setUnreadSessions(prev => [...new Set([...prev, targetSessionId])]) - } - - if (isNewChat) { - requestGeneratedTitle({ - apiBase: backendApiUrl, - message: buildAttachmentTitleSeed(userMsg.content, outgoingAttachments), - model, - sessionId: targetSessionId, - }) - .then(r => r.json()) - .then(data => { - const sanitizedTitle = sanitizeChatTitle(data.title) - setChatSessions(prevSessions => - prevSessions.map(session => - session.session_id === targetSessionId ? { ...session, name: sanitizedTitle } : session - ) - ) - }) - } - } catch (error) { - if (isAbortError(error)) { - finishCancelableRequest(requestController) - return - } - - console.error('Failed to send message:', error) - if (Number(error?.status) >= 400 && Number(error?.status) < 500) { - setChatSessions(prevSessions => - prevSessions.map(session => - session.session_id === targetSessionId - ? { - ...session, - messages: (session.messages || []).filter(message => message.id !== userMsg.id), - } - : session - ) - ) - setInput(composerSnapshot) - setComposerAttachments(attachmentSnapshot) - window.alert(getErrorText(error)) - return - } - const errorMsg = { role: 'assistant', content: 'Error: ' + getErrorText(error), id: `msg-${Date.now()}-${Math.random()}` } - setChatSessions(prevSessions => - prevSessions.map(session => - session.session_id === targetSessionId - ? { ...session, messages: [...session.messages, errorMsg] } - : session - ) - ) - } finally { - finishCancelableRequest(requestController) - } -} - - function toggleWebSearch() { setWebSearchEnabled(prev => !prev);