diff --git a/webui/src/features/chatHistory/ChatHistoryContainer.jsx b/webui/src/features/chatHistory/ChatHistoryContainer.jsx index 17d9692..7fcc8d5 100644 --- a/webui/src/features/chatHistory/ChatHistoryContainer.jsx +++ b/webui/src/features/chatHistory/ChatHistoryContainer.jsx @@ -8,6 +8,12 @@ const LIMIT_OPTIONS = [0, 10, 20, 50] const DISABLED_LIMIT = 0 const MESSAGE_COLLAPSE_AT = 700 const VIEW_MODE_KEY = 'ds2api_chat_history_view_mode' +const HISTORY_WRAPPER_PREFIX = '[file content end]\n\n' +const HISTORY_WRAPPER_SUFFIX = '\n[file name]: IGNORE\n[file content begin]' +const BEGIN_SENTENCE_MARKER = '<|begin▁of▁sentence|>' +const USER_MARKER = '<|User|>' +const ASSISTANT_MARKER = '<|Assistant|>' +const END_SENTENCE_MARKER = '<|end▁of▁sentence|>' function formatDateTime(value, lang) { if (!value) return '-' @@ -105,14 +111,137 @@ function MergeModeIcon() { ) } -function RequestMessages({ item, t }) { - const messages = Array.isArray(item?.messages) && item.messages.length > 0 +function unwrapHistoryTranscript(historyText) { + const trimmed = String(historyText || '').trim() + if (!trimmed) return null + + if (trimmed.startsWith(HISTORY_WRAPPER_PREFIX)) { + if (!trimmed.endsWith(HISTORY_WRAPPER_SUFFIX)) return null + return trimmed.slice(HISTORY_WRAPPER_PREFIX.length, trimmed.length - HISTORY_WRAPPER_SUFFIX.length).trim() + } + + if ( + trimmed.includes('[file content end]') + || trimmed.includes('[file name]: IGNORE') + || trimmed.includes('[file content begin]') + ) { + return null + } + + return trimmed +} + +function parseStrictHistoryMessages(historyText) { + const transcript = unwrapHistoryTranscript(historyText) + if (!transcript || !transcript.startsWith(BEGIN_SENTENCE_MARKER)) return null + + let cursor = BEGIN_SENTENCE_MARKER.length + const parsed = [] + let expectedRole = null + let trailingAssistantPromptOnly = false + + while (cursor < transcript.length) { + if (expectedRole === null) { + if (transcript.startsWith(USER_MARKER, cursor)) { + expectedRole = 'user' + } else if (transcript.startsWith(ASSISTANT_MARKER, cursor)) { + expectedRole = 'assistant' + } else if (transcript.slice(cursor).trim() === '') { + break + } else { + return null + } + } + + if (transcript.startsWith(USER_MARKER, cursor)) { + if (expectedRole !== 'user') return null + cursor += USER_MARKER.length + const nextAssistant = transcript.indexOf(ASSISTANT_MARKER, cursor) + const nextSentenceEnd = transcript.indexOf(END_SENTENCE_MARKER, cursor) + if (nextAssistant < 0) return null + if (nextSentenceEnd >= 0 && nextSentenceEnd < nextAssistant) { + const assistantStart = nextSentenceEnd + END_SENTENCE_MARKER.length + if (!transcript.startsWith(ASSISTANT_MARKER, assistantStart)) return null + parsed.push({ + role: 'user', + content: transcript.slice(cursor, nextSentenceEnd), + }) + cursor = assistantStart + expectedRole = 'assistant' + continue + } + parsed.push({ + role: 'user', + content: transcript.slice(cursor, nextAssistant), + }) + const assistantStart = nextAssistant + ASSISTANT_MARKER.length + if (transcript.slice(assistantStart).trim() === '') { + trailingAssistantPromptOnly = true + cursor = assistantStart + break + } + cursor = nextAssistant + expectedRole = 'assistant' + continue + } + + if (transcript.startsWith(ASSISTANT_MARKER, cursor)) { + if (expectedRole !== 'assistant') return null + cursor += ASSISTANT_MARKER.length + const nextSentenceEnd = transcript.indexOf(END_SENTENCE_MARKER, cursor) + if (nextSentenceEnd < 0) return null + parsed.push({ + role: 'assistant', + content: transcript.slice(cursor, nextSentenceEnd), + }) + cursor = nextSentenceEnd + END_SENTENCE_MARKER.length + expectedRole = 'user' + continue + } + + if (transcript.slice(cursor).trim() === '') break + return null + } + + if (!parsed.length) { + return null + } + + if (!trailingAssistantPromptOnly && parsed[parsed.length - 1]?.role !== 'assistant') { + return null + } + + return parsed +} + +function buildListModeMessages(item, t) { + const liveMessages = Array.isArray(item?.messages) && item.messages.length > 0 ? item.messages : [{ role: 'user', content: item?.user_input || t('chatHistory.emptyUserInput') }] + const historyMessages = parseStrictHistoryMessages(item?.history_text) + + if (!historyMessages?.length) { + return { messages: liveMessages, historyMerged: false } + } + + const insertAt = liveMessages.findIndex(message => { + const role = String(message?.role || '').trim().toLowerCase() + return role !== 'system' && role !== 'developer' + }) + const mergedMessages = [...liveMessages] + mergedMessages.splice(insertAt < 0 ? mergedMessages.length : insertAt, 0, ...historyMessages) + + return { messages: mergedMessages, historyMerged: true } +} + +function RequestMessages({ item, t, messages }) { + const requestMessages = Array.isArray(messages) && messages.length > 0 + ? messages + : [{ role: 'user', content: item?.user_input || t('chatHistory.emptyUserInput') }] return (
- {messages.map((message, index) => { + {requestMessages.map((message, index) => { const role = message.role || 'user' const isUser = role === 'user' const isAssistant = role === 'assistant' @@ -121,7 +250,7 @@ function RequestMessages({ item, t }) { ? t('chatHistory.role.user') : (isAssistant ? t('chatHistory.role.assistant') : (isTool ? t('chatHistory.role.tool') : t('chatHistory.role.system'))) return ( -
+
}
-
+
{label}
- + {showHistoryAtTop && } {viewMode === 'list' - ? + ? : }
@@ -410,12 +541,12 @@ export default function ChatHistoryContainer({ authFetch, onMessage }) { }, []) useEffect(() => { - if (!autoRefreshReady) return undefined + if (!autoRefreshReady || limit === DISABLED_LIMIT) return undefined const timer = window.setInterval(() => { loadList({ mode: 'silent', announceError: false }) }, 5000) return () => window.clearInterval(timer) - }, [autoRefreshReady]) + }, [autoRefreshReady, limit]) useEffect(() => { if (!autoRefreshReady || !selectedId || selectedSummary?.status !== 'streaming') return undefined @@ -494,7 +625,12 @@ export default function ChatHistoryContainer({ authFetch, onMessage }) { setLimit(resolvedLimit) listETagRef.current = '' syncItems(Array.isArray(data.items) ? data.items : []) - onMessage?.('success', t('chatHistory.limitUpdated', { limit: resolvedLimit === DISABLED_LIMIT ? t('chatHistory.off') : resolvedLimit })) + onMessage?.( + 'success', + resolvedLimit === DISABLED_LIMIT + ? t('chatHistory.disabledSuccess') + : t('chatHistory.limitUpdated', { limit: resolvedLimit }) + ) } catch (error) { onMessage?.('error', error.message || t('chatHistory.updateLimitFailed')) } finally { @@ -573,6 +709,12 @@ export default function ChatHistoryContainer({ authFetch, onMessage }) { openMobileDetail(itemId, event) return } + if (itemId === selectedId) { + detailETagRef.current = '' + setSelectedDetail(null) + loadDetail(itemId, { announceError: false }) + return + } setPendingJumpToAssistant(true) setSelectedId(itemId) } diff --git a/webui/src/locales/en.json b/webui/src/locales/en.json index 1be3f59..94de9e9 100644 --- a/webui/src/locales/en.json +++ b/webui/src/locales/en.json @@ -264,6 +264,7 @@ "deleteSuccess": "Conversation deleted.", "deleteFailed": "Failed to delete conversation.", "updateLimitFailed": "Failed to update retention limit.", + "disabledSuccess": "Conversation history saving disabled.", "limitUpdated": "Retention limit updated to {limit}", "listTitle": "History", "detailTitle": "Details", diff --git a/webui/src/locales/zh.json b/webui/src/locales/zh.json index 03abbc2..55fd237 100644 --- a/webui/src/locales/zh.json +++ b/webui/src/locales/zh.json @@ -264,6 +264,7 @@ "deleteSuccess": "对话记录已删除", "deleteFailed": "删除对话记录失败", "updateLimitFailed": "更新保留条数失败", + "disabledSuccess": "已关闭对话历史记录", "limitUpdated": "保留条数已更新为 {limit}", "listTitle": "历史列表", "detailTitle": "对话详情",