Refactor: Remove message sending logic and add web search toggle
This commit is contained in:
318
src/App.jsx
318
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() {
|
function toggleWebSearch() {
|
||||||
setWebSearchEnabled(prev => !prev);
|
setWebSearchEnabled(prev => !prev);
|
||||||
|
|||||||
Reference in New Issue
Block a user