diff --git a/lightrag_webui/src/api/lightrag.ts b/lightrag_webui/src/api/lightrag.ts index a86519d4..a47c8e4c 100644 --- a/lightrag_webui/src/api/lightrag.ts +++ b/lightrag_webui/src/api/lightrag.ts @@ -101,7 +101,7 @@ export type Message = { content: string thinkingContent?: string displayContent?: string - thinkingTime?: number + thinkingTime?: number | null } export type QueryRequest = { diff --git a/lightrag_webui/src/components/retrieval/ChatMessage.tsx b/lightrag_webui/src/components/retrieval/ChatMessage.tsx index 18b4fb25..13cb74ed 100644 --- a/lightrag_webui/src/components/retrieval/ChatMessage.tsx +++ b/lightrag_webui/src/components/retrieval/ChatMessage.tsx @@ -39,11 +39,21 @@ export const ChatMessage = ({ message }: { message: MessageWithError }) => { // // Directly use props passed from the parent. const { thinkingContent, displayContent, thinkingTime, isThinking } = message + // Reset expansion state when new thinking starts + useEffect(() => { + if (isThinking) { + // When thinking starts, always reset to collapsed state + setIsThinkingExpanded(false) + } + }, [isThinking, message.id]) + // The content to display is now non-ambiguous. const finalThinkingContent = thinkingContent // For user messages, displayContent will be undefined, so we fall back to content. - // For assistant messages, we prefer displayContent. - const finalDisplayContent = message.role === 'user' ? message.content : displayContent + // For assistant messages, we prefer displayContent and don't fallback to avoid content leakage during thinking + const finalDisplayContent = message.role === 'user' + ? message.content + : displayContent || '' // Load KaTeX dynamically useEffect(() => { @@ -106,7 +116,12 @@ export const ChatMessage = ({ message }: { message: MessageWithError }) => { //
setIsThinkingExpanded(!isThinkingExpanded)} + onClick={() => { + // Allow expansion when there's thinking content, even during thinking process + if (finalThinkingContent && finalThinkingContent.trim() !== '') { + setIsThinkingExpanded(!isThinkingExpanded) + } + }} > {isThinking ? ( <> @@ -116,10 +131,17 @@ export const ChatMessage = ({ message }: { message: MessageWithError }) => { // ) : ( typeof thinkingTime === 'number' && {t('retrievePanel.chatMessage.thinkingTime', { time: thinkingTime })} )} - {finalThinkingContent && } + {/* Show chevron when there's thinking content, even during thinking process */} + {finalThinkingContent && finalThinkingContent.trim() !== '' && }
- {isThinkingExpanded && finalThinkingContent && ( + {/* Show thinking content when expanded and content exists, even during thinking process */} + {isThinkingExpanded && finalThinkingContent && finalThinkingContent.trim() !== '' && (
+ {isThinking && ( +
+ {t('retrievePanel.chatMessage.thinkingInProgress', 'Thinking in progress...')} +
+ )} { // )}
)} - {message.content === '' && !isThinking && !thinkingTime && } {/* Check for empty string specifically */} + {(() => { + // More comprehensive loading state check + const hasVisibleContent = finalDisplayContent && finalDisplayContent.trim() !== ''; + const isLoadingState = !hasVisibleContent && !isThinking && !thinkingTime; + return isLoadingState && ; + })()}
) } diff --git a/lightrag_webui/src/features/RetrievalTesting.tsx b/lightrag_webui/src/features/RetrievalTesting.tsx index 288dfeae..237e724d 100644 --- a/lightrag_webui/src/features/RetrievalTesting.tsx +++ b/lightrag_webui/src/features/RetrievalTesting.tsx @@ -68,6 +68,16 @@ export default function RetrievalTesting() { const messagesEndRef = useRef(null) const messagesContainerRef = useRef(null) + // Add cleanup effect for memory leak prevention + useEffect(() => { + // Component cleanup - reset timer state to prevent memory leaks + return () => { + if (thinkingStartTime.current) { + thinkingStartTime.current = null; + } + }; + }, []); + // Scroll to bottom function - restored smooth scrolling with better handling const scrollToBottom = useCallback(() => { // Set flag to indicate this is a programmatic scroll @@ -116,6 +126,9 @@ export default function RetrievalTesting() { // Clear error message setInputError('') + // Reset thinking timer state for new query to prevent confusion + thinkingStartTime.current = null + // Create messages // Save the original input (with prefix if any) in userMessage.content for display const userMessage: MessageWithError = { @@ -128,7 +141,11 @@ export default function RetrievalTesting() { id: generateUniqueId(), // Use browser-compatible ID generation content: '', role: 'assistant', - mermaidRendered: false + mermaidRendered: false, + thinkingTime: null, // Explicitly initialize to null + thinkingContent: undefined, // Explicitly initialize to undefined + displayContent: undefined, // Explicitly initialize to undefined + isThinking: false // Explicitly initialize to false } const prevMessages = [...messages] @@ -160,10 +177,10 @@ export default function RetrievalTesting() { } // Real-time parsing for streaming - const thinkStartTag = ''; - const thinkEndTag = ''; - const thinkStartIndex = assistantMessage.content.indexOf(thinkStartTag); - const thinkEndIndex = assistantMessage.content.indexOf(thinkEndTag); + const thinkStartTag = '' + const thinkEndTag = '' + const thinkStartIndex = assistantMessage.content.indexOf(thinkStartTag) + const thinkEndIndex = assistantMessage.content.indexOf(thinkEndTag) if (thinkStartIndex !== -1) { if (thinkEndIndex !== -1) { @@ -173,20 +190,21 @@ export default function RetrievalTesting() { const duration = (Date.now() - thinkingStartTime.current) / 1000 assistantMessage.thinkingTime = parseFloat(duration.toFixed(2)) } - assistantMessage.thinkingContent = assistantMessage.content.substring(thinkStartIndex + thinkStartTag.length, thinkEndIndex).trim() + assistantMessage.thinkingContent = assistantMessage.content + .substring(thinkStartIndex + thinkStartTag.length, thinkEndIndex) + .trim() assistantMessage.displayContent = assistantMessage.content.substring(thinkEndIndex + thinkEndTag.length).trim() } else { // Still thinking - assistantMessage.isThinking = true; - assistantMessage.thinkingContent = assistantMessage.content.substring(thinkStartIndex + thinkStartTag.length); - assistantMessage.displayContent = ''; + assistantMessage.isThinking = true + assistantMessage.thinkingContent = assistantMessage.content.substring(thinkStartIndex + thinkStartTag.length) + assistantMessage.displayContent = '' } } else { - assistantMessage.isThinking = false; - assistantMessage.displayContent = assistantMessage.content; + assistantMessage.isThinking = false + assistantMessage.displayContent = assistantMessage.content } - // Detect if the assistant message contains a complete mermaid code block // Simple heuristic: look for ```mermaid ... ``` const mermaidBlockRegex = /```mermaid\s+([\s\S]+?)```/g @@ -201,16 +219,21 @@ export default function RetrievalTesting() { } assistantMessage.mermaidRendered = mermaidRendered + // Single unified update to avoid race conditions setMessages((prev) => { const newMessages = [...prev] const lastMessage = newMessages[newMessages.length - 1] - if (lastMessage.role === 'assistant') { - lastMessage.content = assistantMessage.content; - lastMessage.thinkingContent = assistantMessage.thinkingContent; - lastMessage.displayContent = assistantMessage.displayContent; - lastMessage.isThinking = assistantMessage.isThinking; - lastMessage.isError = isError; - lastMessage.mermaidRendered = assistantMessage.mermaidRendered; + if (lastMessage && lastMessage.id === assistantMessage.id) { + // Update all properties at once to maintain consistency + Object.assign(lastMessage, { + content: assistantMessage.content, + thinkingContent: assistantMessage.thinkingContent, + displayContent: assistantMessage.displayContent, + isThinking: assistantMessage.isThinking, + isError: isError, + mermaidRendered: assistantMessage.mermaidRendered, + thinkingTime: assistantMessage.thinkingTime + }) } return newMessages }) @@ -261,21 +284,29 @@ export default function RetrievalTesting() { setIsLoading(false) isReceivingResponseRef.current = false - // Final calculation for thinking time, only if not already calculated - if (assistantMessage.thinkingContent && thinkingStartTime.current && !assistantMessage.thinkingTime) { - const duration = (Date.now() - thinkingStartTime.current) / 1000 - assistantMessage.thinkingTime = parseFloat(duration.toFixed(2)) - } - // Ensure isThinking is false at the very end - assistantMessage.isThinking = false; - // Always reset the timer at the end of a query - if (thinkingStartTime.current) { + // Enhanced cleanup with error handling to prevent memory leaks + try { + // Final calculation for thinking time, only if not already calculated + if (assistantMessage.thinkingContent && thinkingStartTime.current && !assistantMessage.thinkingTime) { + const duration = (Date.now() - thinkingStartTime.current) / 1000 + assistantMessage.thinkingTime = parseFloat(duration.toFixed(2)) + } + } catch (error) { + console.error('Error calculating thinking time:', error) + } finally { + // Ensure cleanup happens regardless of errors + assistantMessage.isThinking = false; thinkingStartTime.current = null; } - useSettingsStore - .getState() - .setRetrievalHistory([...prevMessages, userMessage, assistantMessage]) + // Save history with error handling + try { + useSettingsStore + .getState() + .setRetrievalHistory([...prevMessages, userMessage, assistantMessage]) + } catch (error) { + console.error('Error saving retrieval history:', error) + } } }, [inputValue, isLoading, messages, setMessages, t, scrollToBottom] diff --git a/lightrag_webui/src/locales/ar.json b/lightrag_webui/src/locales/ar.json index 240ea053..f5563cf8 100644 --- a/lightrag_webui/src/locales/ar.json +++ b/lightrag_webui/src/locales/ar.json @@ -337,7 +337,10 @@ "retrievePanel": { "chatMessage": { "copyTooltip": "نسخ إلى الحافظة", - "copyError": "فشل نسخ النص إلى الحافظة" + "copyError": "فشل نسخ النص إلى الحافظة", + "thinking": "جاري التفكير...", + "thinkingTime": "وقت التفكير {{time}} ثانية", + "thinkingInProgress": "التفكير قيد التقدم..." }, "retrieval": { "startPrompt": "ابدأ الاسترجاع بكتابة استفسارك أدناه", diff --git a/lightrag_webui/src/locales/en.json b/lightrag_webui/src/locales/en.json index 7590f74c..af324cfd 100644 --- a/lightrag_webui/src/locales/en.json +++ b/lightrag_webui/src/locales/en.json @@ -339,7 +339,8 @@ "copyTooltip": "Copy to clipboard", "copyError": "Failed to copy text to clipboard", "thinking": "Thinking...", - "thinkingTime": "Thinking time {{time}}s" + "thinkingTime": "Thinking time {{time}}s", + "thinkingInProgress": "Thinking in progress..." }, "retrieval": { "startPrompt": "Start a retrieval by typing your query below", diff --git a/lightrag_webui/src/locales/fr.json b/lightrag_webui/src/locales/fr.json index a0629d17..c4985906 100644 --- a/lightrag_webui/src/locales/fr.json +++ b/lightrag_webui/src/locales/fr.json @@ -337,7 +337,10 @@ "retrievePanel": { "chatMessage": { "copyTooltip": "Copier dans le presse-papiers", - "copyError": "Échec de la copie du texte dans le presse-papiers" + "copyError": "Échec de la copie du texte dans le presse-papiers", + "thinking": "Réflexion en cours...", + "thinkingTime": "Temps de réflexion {{time}}s", + "thinkingInProgress": "Réflexion en cours..." }, "retrieval": { "startPrompt": "Démarrez une récupération en tapant votre requête ci-dessous", diff --git a/lightrag_webui/src/locales/zh.json b/lightrag_webui/src/locales/zh.json index 6d6323aa..fbfdb542 100644 --- a/lightrag_webui/src/locales/zh.json +++ b/lightrag_webui/src/locales/zh.json @@ -339,7 +339,8 @@ "copyTooltip": "复制到剪贴板", "copyError": "复制文本到剪贴板失败", "thinking": "正在思考...", - "thinkingTime": "思考用时 {{time}} 秒" + "thinkingTime": "思考用时 {{time}} 秒", + "thinkingInProgress": "思考进行中..." }, "retrieval": { "startPrompt": "输入查询开始检索", diff --git a/lightrag_webui/src/locales/zh_TW.json b/lightrag_webui/src/locales/zh_TW.json index df35434a..44e1cd0f 100644 --- a/lightrag_webui/src/locales/zh_TW.json +++ b/lightrag_webui/src/locales/zh_TW.json @@ -337,7 +337,10 @@ "retrievePanel": { "chatMessage": { "copyTooltip": "複製到剪貼簿", - "copyError": "複製文字到剪貼簿失敗" + "copyError": "複製文字到剪貼簿失敗", + "thinking": "正在思考...", + "thinkingTime": "思考用時 {{time}} 秒", + "thinkingInProgress": "思考進行中..." }, "retrieval": { "startPrompt": "輸入查詢開始檢索",