Fix thinking state management and UI improvements for chat messages
- Reset expansion state on new thinking - Prevent content leakage during thinking - Update thinkingTime type to allow null - Add memory leak prevention cleanup - Add missing i18n translations
This commit is contained in:
parent
2ba10dbb5b
commit
eeff0d5c67
8 changed files with 112 additions and 43 deletions
|
|
@ -101,7 +101,7 @@ export type Message = {
|
||||||
content: string
|
content: string
|
||||||
thinkingContent?: string
|
thinkingContent?: string
|
||||||
displayContent?: string
|
displayContent?: string
|
||||||
thinkingTime?: number
|
thinkingTime?: number | null
|
||||||
}
|
}
|
||||||
|
|
||||||
export type QueryRequest = {
|
export type QueryRequest = {
|
||||||
|
|
|
||||||
|
|
@ -39,11 +39,21 @@ export const ChatMessage = ({ message }: { message: MessageWithError }) => { //
|
||||||
// Directly use props passed from the parent.
|
// Directly use props passed from the parent.
|
||||||
const { thinkingContent, displayContent, thinkingTime, isThinking } = message
|
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.
|
// The content to display is now non-ambiguous.
|
||||||
const finalThinkingContent = thinkingContent
|
const finalThinkingContent = thinkingContent
|
||||||
// For user messages, displayContent will be undefined, so we fall back to content.
|
// For user messages, displayContent will be undefined, so we fall back to content.
|
||||||
// For assistant messages, we prefer 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
|
const finalDisplayContent = message.role === 'user'
|
||||||
|
? message.content
|
||||||
|
: displayContent || ''
|
||||||
|
|
||||||
// Load KaTeX dynamically
|
// Load KaTeX dynamically
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|
@ -106,7 +116,12 @@ export const ChatMessage = ({ message }: { message: MessageWithError }) => { //
|
||||||
<div className="mb-2">
|
<div className="mb-2">
|
||||||
<div
|
<div
|
||||||
className="flex items-center text-gray-500 dark:text-gray-400 hover:text-gray-700 dark:hover:text-gray-200 transition-colors duration-200 text-sm cursor-pointer select-none"
|
className="flex items-center text-gray-500 dark:text-gray-400 hover:text-gray-700 dark:hover:text-gray-200 transition-colors duration-200 text-sm cursor-pointer select-none"
|
||||||
onClick={() => setIsThinkingExpanded(!isThinkingExpanded)}
|
onClick={() => {
|
||||||
|
// Allow expansion when there's thinking content, even during thinking process
|
||||||
|
if (finalThinkingContent && finalThinkingContent.trim() !== '') {
|
||||||
|
setIsThinkingExpanded(!isThinkingExpanded)
|
||||||
|
}
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
{isThinking ? (
|
{isThinking ? (
|
||||||
<>
|
<>
|
||||||
|
|
@ -116,10 +131,17 @@ export const ChatMessage = ({ message }: { message: MessageWithError }) => { //
|
||||||
) : (
|
) : (
|
||||||
typeof thinkingTime === 'number' && <span>{t('retrievePanel.chatMessage.thinkingTime', { time: thinkingTime })}</span>
|
typeof thinkingTime === 'number' && <span>{t('retrievePanel.chatMessage.thinkingTime', { time: thinkingTime })}</span>
|
||||||
)}
|
)}
|
||||||
{finalThinkingContent && <ChevronDownIcon className={`ml-2 size-4 shrink-0 transition-transform ${isThinkingExpanded ? 'rotate-180' : ''}`} />}
|
{/* Show chevron when there's thinking content, even during thinking process */}
|
||||||
|
{finalThinkingContent && finalThinkingContent.trim() !== '' && <ChevronDownIcon className={`ml-2 size-4 shrink-0 transition-transform ${isThinkingExpanded ? 'rotate-180' : ''}`} />}
|
||||||
</div>
|
</div>
|
||||||
{isThinkingExpanded && finalThinkingContent && (
|
{/* Show thinking content when expanded and content exists, even during thinking process */}
|
||||||
|
{isThinkingExpanded && finalThinkingContent && finalThinkingContent.trim() !== '' && (
|
||||||
<div className="mt-2 pl-4 border-l-2 border-primary/20 text-sm prose dark:prose-invert max-w-none break-words prose-p:my-1 prose-headings:my-2">
|
<div className="mt-2 pl-4 border-l-2 border-primary/20 text-sm prose dark:prose-invert max-w-none break-words prose-p:my-1 prose-headings:my-2">
|
||||||
|
{isThinking && (
|
||||||
|
<div className="mb-2 text-xs text-gray-400 dark:text-gray-500 italic">
|
||||||
|
{t('retrievePanel.chatMessage.thinkingInProgress', 'Thinking in progress...')}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
<ReactMarkdown
|
<ReactMarkdown
|
||||||
remarkPlugins={[remarkGfm, remarkMath]}
|
remarkPlugins={[remarkGfm, remarkMath]}
|
||||||
rehypePlugins={[
|
rehypePlugins={[
|
||||||
|
|
@ -170,7 +192,12 @@ export const ChatMessage = ({ message }: { message: MessageWithError }) => { //
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{message.content === '' && !isThinking && !thinkingTime && <LoaderIcon className="animate-spin duration-2000" />} {/* Check for empty string specifically */}
|
{(() => {
|
||||||
|
// More comprehensive loading state check
|
||||||
|
const hasVisibleContent = finalDisplayContent && finalDisplayContent.trim() !== '';
|
||||||
|
const isLoadingState = !hasVisibleContent && !isThinking && !thinkingTime;
|
||||||
|
return isLoadingState && <LoaderIcon className="animate-spin duration-2000" />;
|
||||||
|
})()}
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -68,6 +68,16 @@ export default function RetrievalTesting() {
|
||||||
const messagesEndRef = useRef<HTMLDivElement>(null)
|
const messagesEndRef = useRef<HTMLDivElement>(null)
|
||||||
const messagesContainerRef = useRef<HTMLDivElement>(null)
|
const messagesContainerRef = useRef<HTMLDivElement>(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
|
// Scroll to bottom function - restored smooth scrolling with better handling
|
||||||
const scrollToBottom = useCallback(() => {
|
const scrollToBottom = useCallback(() => {
|
||||||
// Set flag to indicate this is a programmatic scroll
|
// Set flag to indicate this is a programmatic scroll
|
||||||
|
|
@ -116,6 +126,9 @@ export default function RetrievalTesting() {
|
||||||
// Clear error message
|
// Clear error message
|
||||||
setInputError('')
|
setInputError('')
|
||||||
|
|
||||||
|
// Reset thinking timer state for new query to prevent confusion
|
||||||
|
thinkingStartTime.current = null
|
||||||
|
|
||||||
// Create messages
|
// Create messages
|
||||||
// Save the original input (with prefix if any) in userMessage.content for display
|
// Save the original input (with prefix if any) in userMessage.content for display
|
||||||
const userMessage: MessageWithError = {
|
const userMessage: MessageWithError = {
|
||||||
|
|
@ -128,7 +141,11 @@ export default function RetrievalTesting() {
|
||||||
id: generateUniqueId(), // Use browser-compatible ID generation
|
id: generateUniqueId(), // Use browser-compatible ID generation
|
||||||
content: '',
|
content: '',
|
||||||
role: 'assistant',
|
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]
|
const prevMessages = [...messages]
|
||||||
|
|
@ -160,10 +177,10 @@ export default function RetrievalTesting() {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Real-time parsing for streaming
|
// Real-time parsing for streaming
|
||||||
const thinkStartTag = '<think>';
|
const thinkStartTag = '<think>'
|
||||||
const thinkEndTag = '</think>';
|
const thinkEndTag = '</think>'
|
||||||
const thinkStartIndex = assistantMessage.content.indexOf(thinkStartTag);
|
const thinkStartIndex = assistantMessage.content.indexOf(thinkStartTag)
|
||||||
const thinkEndIndex = assistantMessage.content.indexOf(thinkEndTag);
|
const thinkEndIndex = assistantMessage.content.indexOf(thinkEndTag)
|
||||||
|
|
||||||
if (thinkStartIndex !== -1) {
|
if (thinkStartIndex !== -1) {
|
||||||
if (thinkEndIndex !== -1) {
|
if (thinkEndIndex !== -1) {
|
||||||
|
|
@ -173,20 +190,21 @@ export default function RetrievalTesting() {
|
||||||
const duration = (Date.now() - thinkingStartTime.current) / 1000
|
const duration = (Date.now() - thinkingStartTime.current) / 1000
|
||||||
assistantMessage.thinkingTime = parseFloat(duration.toFixed(2))
|
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()
|
assistantMessage.displayContent = assistantMessage.content.substring(thinkEndIndex + thinkEndTag.length).trim()
|
||||||
} else {
|
} else {
|
||||||
// Still thinking
|
// Still thinking
|
||||||
assistantMessage.isThinking = true;
|
assistantMessage.isThinking = true
|
||||||
assistantMessage.thinkingContent = assistantMessage.content.substring(thinkStartIndex + thinkStartTag.length);
|
assistantMessage.thinkingContent = assistantMessage.content.substring(thinkStartIndex + thinkStartTag.length)
|
||||||
assistantMessage.displayContent = '';
|
assistantMessage.displayContent = ''
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
assistantMessage.isThinking = false;
|
assistantMessage.isThinking = false
|
||||||
assistantMessage.displayContent = assistantMessage.content;
|
assistantMessage.displayContent = assistantMessage.content
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
// Detect if the assistant message contains a complete mermaid code block
|
// Detect if the assistant message contains a complete mermaid code block
|
||||||
// Simple heuristic: look for ```mermaid ... ```
|
// Simple heuristic: look for ```mermaid ... ```
|
||||||
const mermaidBlockRegex = /```mermaid\s+([\s\S]+?)```/g
|
const mermaidBlockRegex = /```mermaid\s+([\s\S]+?)```/g
|
||||||
|
|
@ -201,16 +219,21 @@ export default function RetrievalTesting() {
|
||||||
}
|
}
|
||||||
assistantMessage.mermaidRendered = mermaidRendered
|
assistantMessage.mermaidRendered = mermaidRendered
|
||||||
|
|
||||||
|
// Single unified update to avoid race conditions
|
||||||
setMessages((prev) => {
|
setMessages((prev) => {
|
||||||
const newMessages = [...prev]
|
const newMessages = [...prev]
|
||||||
const lastMessage = newMessages[newMessages.length - 1]
|
const lastMessage = newMessages[newMessages.length - 1]
|
||||||
if (lastMessage.role === 'assistant') {
|
if (lastMessage && lastMessage.id === assistantMessage.id) {
|
||||||
lastMessage.content = assistantMessage.content;
|
// Update all properties at once to maintain consistency
|
||||||
lastMessage.thinkingContent = assistantMessage.thinkingContent;
|
Object.assign(lastMessage, {
|
||||||
lastMessage.displayContent = assistantMessage.displayContent;
|
content: assistantMessage.content,
|
||||||
lastMessage.isThinking = assistantMessage.isThinking;
|
thinkingContent: assistantMessage.thinkingContent,
|
||||||
lastMessage.isError = isError;
|
displayContent: assistantMessage.displayContent,
|
||||||
lastMessage.mermaidRendered = assistantMessage.mermaidRendered;
|
isThinking: assistantMessage.isThinking,
|
||||||
|
isError: isError,
|
||||||
|
mermaidRendered: assistantMessage.mermaidRendered,
|
||||||
|
thinkingTime: assistantMessage.thinkingTime
|
||||||
|
})
|
||||||
}
|
}
|
||||||
return newMessages
|
return newMessages
|
||||||
})
|
})
|
||||||
|
|
@ -261,21 +284,29 @@ export default function RetrievalTesting() {
|
||||||
setIsLoading(false)
|
setIsLoading(false)
|
||||||
isReceivingResponseRef.current = false
|
isReceivingResponseRef.current = false
|
||||||
|
|
||||||
// Final calculation for thinking time, only if not already calculated
|
// Enhanced cleanup with error handling to prevent memory leaks
|
||||||
if (assistantMessage.thinkingContent && thinkingStartTime.current && !assistantMessage.thinkingTime) {
|
try {
|
||||||
const duration = (Date.now() - thinkingStartTime.current) / 1000
|
// Final calculation for thinking time, only if not already calculated
|
||||||
assistantMessage.thinkingTime = parseFloat(duration.toFixed(2))
|
if (assistantMessage.thinkingContent && thinkingStartTime.current && !assistantMessage.thinkingTime) {
|
||||||
}
|
const duration = (Date.now() - thinkingStartTime.current) / 1000
|
||||||
// Ensure isThinking is false at the very end
|
assistantMessage.thinkingTime = parseFloat(duration.toFixed(2))
|
||||||
assistantMessage.isThinking = false;
|
}
|
||||||
// Always reset the timer at the end of a query
|
} catch (error) {
|
||||||
if (thinkingStartTime.current) {
|
console.error('Error calculating thinking time:', error)
|
||||||
|
} finally {
|
||||||
|
// Ensure cleanup happens regardless of errors
|
||||||
|
assistantMessage.isThinking = false;
|
||||||
thinkingStartTime.current = null;
|
thinkingStartTime.current = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
useSettingsStore
|
// Save history with error handling
|
||||||
.getState()
|
try {
|
||||||
.setRetrievalHistory([...prevMessages, userMessage, assistantMessage])
|
useSettingsStore
|
||||||
|
.getState()
|
||||||
|
.setRetrievalHistory([...prevMessages, userMessage, assistantMessage])
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error saving retrieval history:', error)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[inputValue, isLoading, messages, setMessages, t, scrollToBottom]
|
[inputValue, isLoading, messages, setMessages, t, scrollToBottom]
|
||||||
|
|
|
||||||
|
|
@ -337,7 +337,10 @@
|
||||||
"retrievePanel": {
|
"retrievePanel": {
|
||||||
"chatMessage": {
|
"chatMessage": {
|
||||||
"copyTooltip": "نسخ إلى الحافظة",
|
"copyTooltip": "نسخ إلى الحافظة",
|
||||||
"copyError": "فشل نسخ النص إلى الحافظة"
|
"copyError": "فشل نسخ النص إلى الحافظة",
|
||||||
|
"thinking": "جاري التفكير...",
|
||||||
|
"thinkingTime": "وقت التفكير {{time}} ثانية",
|
||||||
|
"thinkingInProgress": "التفكير قيد التقدم..."
|
||||||
},
|
},
|
||||||
"retrieval": {
|
"retrieval": {
|
||||||
"startPrompt": "ابدأ الاسترجاع بكتابة استفسارك أدناه",
|
"startPrompt": "ابدأ الاسترجاع بكتابة استفسارك أدناه",
|
||||||
|
|
|
||||||
|
|
@ -339,7 +339,8 @@
|
||||||
"copyTooltip": "Copy to clipboard",
|
"copyTooltip": "Copy to clipboard",
|
||||||
"copyError": "Failed to copy text to clipboard",
|
"copyError": "Failed to copy text to clipboard",
|
||||||
"thinking": "Thinking...",
|
"thinking": "Thinking...",
|
||||||
"thinkingTime": "Thinking time {{time}}s"
|
"thinkingTime": "Thinking time {{time}}s",
|
||||||
|
"thinkingInProgress": "Thinking in progress..."
|
||||||
},
|
},
|
||||||
"retrieval": {
|
"retrieval": {
|
||||||
"startPrompt": "Start a retrieval by typing your query below",
|
"startPrompt": "Start a retrieval by typing your query below",
|
||||||
|
|
|
||||||
|
|
@ -337,7 +337,10 @@
|
||||||
"retrievePanel": {
|
"retrievePanel": {
|
||||||
"chatMessage": {
|
"chatMessage": {
|
||||||
"copyTooltip": "Copier dans le presse-papiers",
|
"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": {
|
"retrieval": {
|
||||||
"startPrompt": "Démarrez une récupération en tapant votre requête ci-dessous",
|
"startPrompt": "Démarrez une récupération en tapant votre requête ci-dessous",
|
||||||
|
|
|
||||||
|
|
@ -339,7 +339,8 @@
|
||||||
"copyTooltip": "复制到剪贴板",
|
"copyTooltip": "复制到剪贴板",
|
||||||
"copyError": "复制文本到剪贴板失败",
|
"copyError": "复制文本到剪贴板失败",
|
||||||
"thinking": "正在思考...",
|
"thinking": "正在思考...",
|
||||||
"thinkingTime": "思考用时 {{time}} 秒"
|
"thinkingTime": "思考用时 {{time}} 秒",
|
||||||
|
"thinkingInProgress": "思考进行中..."
|
||||||
},
|
},
|
||||||
"retrieval": {
|
"retrieval": {
|
||||||
"startPrompt": "输入查询开始检索",
|
"startPrompt": "输入查询开始检索",
|
||||||
|
|
|
||||||
|
|
@ -337,7 +337,10 @@
|
||||||
"retrievePanel": {
|
"retrievePanel": {
|
||||||
"chatMessage": {
|
"chatMessage": {
|
||||||
"copyTooltip": "複製到剪貼簿",
|
"copyTooltip": "複製到剪貼簿",
|
||||||
"copyError": "複製文字到剪貼簿失敗"
|
"copyError": "複製文字到剪貼簿失敗",
|
||||||
|
"thinking": "正在思考...",
|
||||||
|
"thinkingTime": "思考用時 {{time}} 秒",
|
||||||
|
"thinkingInProgress": "思考進行中..."
|
||||||
},
|
},
|
||||||
"retrieval": {
|
"retrieval": {
|
||||||
"startPrompt": "輸入查詢開始檢索",
|
"startPrompt": "輸入查詢開始檢索",
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue