diff --git a/src/App.jsx b/src/App.jsx index 272a6cd..4eb712e 100644 --- a/src/App.jsx +++ b/src/App.jsx @@ -475,204 +475,6 @@ export default function App() { await regenerateFromIndex(index, next); } -async function regenerateFromIndex(index, overrideUserText = null) { - const sessionId = activeSessionId - if (isSending || !sessionId || typeof index !== 'number') return - - const msgs = (chatSessions.find(s => s.session_id === sessionId)?.messages) || [] - let lastUserIdx = index - for (let i = index; i >= 0; i--) { - if (msgs[i]?.role === 'user') { - lastUserIdx = i - break - } - } - - setChatSessions(prev => - prev.map(s => s.session_id === sessionId - ? { ...s, messages: (s.messages || []).slice(0, lastUserIdx + 1) } - : s - ) - ) - - const conversationNeedsVision = msgs - .slice(0, lastUserIdx + 1) - .some(messageHasImageAttachments) - if (conversationNeedsVision && !canAttachImages) { - window.alert(imageAttachmentUnavailableReason) - return - } - - const requestController = beginCancelableRequest(sessionId) - - let enrichedPrompt = overrideUserText != null ? overrideUserText : (msgs[lastUserIdx]?.content || '') - let citationSources = [] - const contextBlocks = [] - try { - const selectedLibrary = getChatLibraryForSession(sessionId) - const promptText = overrideUserText != null ? overrideUserText : (msgs[lastUserIdx]?.content || '') - const hasPromptText = Boolean((promptText || '').trim()) - - if (hasPromptText && selectedLibrary?.states?.is_indexed) { - try { - const localContext = await fetchLocalLibraryContext(backendApiUrl, selectedLibrary.slug, promptText, 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 (regenerate) failed', error) - } - } - - if (hasPromptText && webSearchEnabled) { - try { - const historyForSearch = msgs - .slice(Math.max(0, lastUserIdx - 7), lastUserIdx + 1) - .map(m => ({ role: m.role, content: m.content || '' })) - if (historyForSearch.length > 0) { - historyForSearch[historyForSearch.length - 1] = { role: 'user', content: promptText } - } - - const searchContext = await fetchWebSearchContext({ - apiBase: backendApiUrl, - engines: searxEngines, - historyLimit: 8, - messages: historyForSearch, - model, - prompt: promptText, - 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 (regenerate) failed', error) - } - } - - citationSources = [...new Set(citationSources)] - if (hasPromptText && contextBlocks.length > 0) { - enrichedPrompt = `${promptText}\n\n${contextBlocks.join('\n\n')}` - } else { - enrichedPrompt = null - } - - if (streamOutput) { - const assistantMsgId = `msg-${Date.now()}-${Math.random()}` - let full = '' - - setChatSessions(prev => - prev.map(s => s.session_id === sessionId - ? { ...s, messages: [...(s.messages || []), { id: assistantMsgId, role: 'assistant', content: '', sources: citationSources }] } - : s - ) - ) - - try { - const res = await postRegenerateMessage({ - apiBase: backendApiUrl, - enrichedMessage: enrichedPrompt, - index, - model, - sessionId, - signal: requestController.signal, - sources: citationSources || [], - stream: true, - transcriptionModel, - visionModel, - }) - if (!res.ok) throw new Error(await readBackendErrorText(res)) - - const reader = res.body?.getReader() - if (!reader) throw new Error('Missing response body') - - const decoder = new TextDecoder() - let unreadMarked = false - - while (true) { - const { value, done } = await reader.read() - if (done) break - - const chunk = decoder.decode(value, { stream: true }) - full += chunk - setAssistantMessageContent(sessionId, assistantMsgId, full) - - if (!unreadMarked && activeSessionIdRef.current !== sessionId) { - unreadMarked = true - setPendingScrollToLastUser(prev => ({ ...prev, [sessionId]: assistantMsgId })) - setUnreadSessions(prev => [...new Set([...prev, sessionId])]) - } - } - - if (activeSessionIdRef.current !== sessionId) { - setPendingScrollToLastUser(prev => ({ ...prev, [sessionId]: assistantMsgId })) - setUnreadSessions(prev => [...new Set([...prev, sessionId])]) - } else if (!userScrolledUpRef.current[sessionId]) { - requestAnimationFrame(() => scrollMessageToTop(assistantMsgId, 'smooth', sessionId)) - } else { - setNewMsgTip(prev => ({ ...prev, [sessionId]: assistantMsgId })) - } - } catch (error) { - if (isAbortError(error)) { - setAssistantMessageContent(sessionId, assistantMsgId, full, { removeIfEmpty: true }) - return - } - - console.error(error) - setAssistantMessageContent(sessionId, assistantMsgId, `Error: ${getErrorText(error)}`, { removeIfEmpty: true }) - return - } - } else { - const res = await postRegenerateMessage({ - apiBase: backendApiUrl, - enrichedMessage: enrichedPrompt, - index, - model, - sessionId, - signal: requestController.signal, - sources: citationSources || [], - stream: false, - transcriptionModel, - visionModel, - }) - if (!res.ok) throw new Error(await readBackendErrorText(res)) - - const data = await res.json() - const assistantMsgId = `msg-${Date.now()}` - setChatSessions(prev => - prev.map(s => s.session_id === sessionId - ? { ...s, messages: [...(s.messages || []), { role: 'assistant', content: data.reply, id: assistantMsgId, sources: citationSources }] } - : s - ) - ) - - if (activeSessionIdRef.current !== sessionId) { - setPendingScrollToLastUser(prev => ({ ...prev, [sessionId]: assistantMsgId })) - setUnreadSessions(prev => [...new Set([...prev, sessionId])]) - } else if (!userScrolledUpRef.current[sessionId]) { - requestAnimationFrame(() => scrollMessageToTop(assistantMsgId, 'smooth', sessionId)) - } else { - setNewMsgTip(prev => ({ ...prev, [sessionId]: assistantMsgId })) - } - } - } catch (error) { - if (!isAbortError(error)) { - console.error(error) - } - } finally { - finishCancelableRequest(requestController) - } -} - // Collapse state per user message: { [msgKey]: boolean } — true means "collapsed" const [collapsedUserMsgs, setCollapsedUserMsgs] = useState({});