yangdx
commited on
Commit
·
d8300f7
1
Parent(s):
ac8ad08
Stablize mermaid render in history messages
Browse files
lightrag_webui/src/components/retrieval/ChatMessage.tsx
CHANGED
@@ -1,4 +1,4 @@
|
|
1 |
-
import { ReactNode, useCallback, useEffect, useRef } from 'react'
|
2 |
import { Message } from '@/api/lightrag'
|
3 |
import useTheme from '@/hooks/useTheme'
|
4 |
import Button from '@/components/ui/Button'
|
@@ -19,10 +19,17 @@ import { LoaderIcon, CopyIcon } from 'lucide-react'
|
|
19 |
import { useTranslation } from 'react-i18next'
|
20 |
|
21 |
export type MessageWithError = Message & {
|
|
|
22 |
isError?: boolean
|
|
|
|
|
|
|
|
|
|
|
23 |
}
|
24 |
|
25 |
-
|
|
|
26 |
const { t } = useTranslation()
|
27 |
const handleCopyMarkdown = useCallback(async () => {
|
28 |
if (message.content) {
|
@@ -50,17 +57,23 @@ export const ChatMessage = ({ message, isComplete = true }: { message: MessageWi
|
|
50 |
remarkPlugins={[remarkGfm, remarkMath]}
|
51 |
rehypePlugins={[rehypeReact]}
|
52 |
skipHtml={false}
|
53 |
-
components
|
54 |
-
|
55 |
-
|
56 |
-
|
57 |
-
|
58 |
-
|
59 |
-
|
60 |
-
|
61 |
-
|
62 |
-
|
63 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
64 |
>
|
65 |
{message.content}
|
66 |
</ReactMarkdown>
|
@@ -81,12 +94,14 @@ export const ChatMessage = ({ message, isComplete = true }: { message: MessageWi
|
|
81 |
)
|
82 |
}
|
83 |
|
|
|
|
|
84 |
interface CodeHighlightProps {
|
85 |
inline?: boolean
|
86 |
className?: string
|
87 |
children?: ReactNode
|
88 |
node?: Element // Keep node for inline check
|
89 |
-
|
90 |
}
|
91 |
|
92 |
// Helper function remains the same
|
@@ -101,8 +116,10 @@ const isInlineCode = (node?: Element): boolean => {
|
|
101 |
};
|
102 |
|
103 |
|
104 |
-
|
|
|
105 |
const { theme } = useTheme();
|
|
|
106 |
const match = className?.match(/language-(\w+)/);
|
107 |
const language = match ? match[1] : undefined;
|
108 |
const inline = isInlineCode(node); // Use the helper function
|
@@ -111,35 +128,37 @@ const CodeHighlight = ({ className, children, node, isComplete = true, ...props
|
|
111 |
|
112 |
// Handle Mermaid rendering with debounce
|
113 |
useEffect(() => {
|
114 |
-
//
|
115 |
-
|
116 |
-
|
117 |
-
|
118 |
|
119 |
-
|
120 |
-
|
|
|
|
|
121 |
|
122 |
-
// Set a new timer to render after a short delay
|
123 |
debounceTimerRef.current = setTimeout(() => {
|
124 |
-
|
125 |
-
|
|
|
|
|
126 |
|
127 |
try {
|
128 |
-
// Initialize mermaid config
|
129 |
mermaid.initialize({
|
130 |
startOnLoad: false,
|
131 |
theme: theme === 'dark' ? 'dark' : 'default',
|
132 |
securityLevel: 'loose',
|
133 |
});
|
134 |
|
135 |
-
// Show loading indicator
|
136 |
container.innerHTML = '<div class="flex justify-center items-center p-4"><svg class="animate-spin h-5 w-5 text-primary" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"><circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle><path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path></svg></div>';
|
137 |
|
138 |
// Preprocess mermaid content
|
139 |
-
const rawContent = String(children).replace(/\n$/, '').trim();
|
140 |
|
141 |
// Heuristic check for potentially complete graph definition
|
142 |
-
// Looks for graph type declaration and some content beyond it.
|
143 |
const looksPotentiallyComplete = rawContent.length > 10 && (
|
144 |
rawContent.startsWith('graph') ||
|
145 |
rawContent.startsWith('sequenceDiagram') ||
|
@@ -151,19 +170,17 @@ const CodeHighlight = ({ className, children, node, isComplete = true, ...props
|
|
151 |
rawContent.startsWith('erDiagram')
|
152 |
);
|
153 |
|
154 |
-
|
155 |
if (!looksPotentiallyComplete) {
|
156 |
console.log('Mermaid content might be incomplete, skipping render attempt:', rawContent);
|
157 |
-
//
|
158 |
// container.innerHTML = '<p class="text-sm text-muted-foreground">Waiting for complete diagram...</p>';
|
159 |
-
return;
|
160 |
}
|
161 |
|
162 |
const processedContent = rawContent
|
163 |
.split('\n')
|
164 |
.map(line => {
|
165 |
const trimmedLine = line.trim();
|
166 |
-
// Keep subgraph processing
|
167 |
if (trimmedLine.startsWith('subgraph')) {
|
168 |
const parts = trimmedLine.split(' ');
|
169 |
if (parts.length > 1) {
|
@@ -173,26 +190,25 @@ const CodeHighlight = ({ className, children, node, isComplete = true, ...props
|
|
173 |
}
|
174 |
return trimmedLine;
|
175 |
})
|
176 |
-
.filter(line => !line.trim().startsWith('linkStyle'))
|
177 |
.join('\n');
|
178 |
|
179 |
const mermaidId = `mermaid-${Date.now()}`;
|
180 |
mermaid.render(mermaidId, processedContent)
|
181 |
.then(({ svg, bindFunctions }) => {
|
182 |
-
// Check ref again inside async callback
|
183 |
-
|
184 |
-
if (mermaidRef.current === container) {
|
185 |
container.innerHTML = svg;
|
|
|
186 |
if (bindFunctions) {
|
187 |
-
try {
|
188 |
bindFunctions(container);
|
189 |
} catch (bindError) {
|
190 |
console.error('Mermaid bindFunctions error:', bindError);
|
191 |
-
// Optionally display a message in the container
|
192 |
container.innerHTML += '<p class="text-orange-500 text-xs">Diagram interactions might be limited.</p>';
|
193 |
}
|
194 |
}
|
195 |
-
} else {
|
196 |
console.log('Mermaid container changed before rendering completed.');
|
197 |
}
|
198 |
})
|
@@ -201,11 +217,10 @@ const CodeHighlight = ({ className, children, node, isComplete = true, ...props
|
|
201 |
console.error('Failed content (debounced):', processedContent);
|
202 |
if (mermaidRef.current === container) {
|
203 |
const errorMessage = error instanceof Error ? error.message : String(error);
|
204 |
-
// Make error display more robust
|
205 |
const errorPre = document.createElement('pre');
|
206 |
errorPre.className = 'text-red-500 text-xs whitespace-pre-wrap break-words';
|
207 |
errorPre.textContent = `Mermaid diagram error: ${errorMessage}\n\nContent:\n${processedContent}`;
|
208 |
-
container.innerHTML = '';
|
209 |
container.appendChild(errorPre);
|
210 |
}
|
211 |
});
|
@@ -218,24 +233,28 @@ const CodeHighlight = ({ className, children, node, isComplete = true, ...props
|
|
218 |
const errorPre = document.createElement('pre');
|
219 |
errorPre.className = 'text-red-500 text-xs whitespace-pre-wrap break-words';
|
220 |
errorPre.textContent = `Mermaid diagram setup error: ${errorMessage}`;
|
221 |
-
container.innerHTML = '';
|
222 |
container.appendChild(errorPre);
|
223 |
}
|
224 |
}
|
225 |
-
}, 300); //
|
226 |
}
|
227 |
|
228 |
-
// Cleanup function to clear the timer
|
229 |
return () => {
|
230 |
if (debounceTimerRef.current) {
|
231 |
clearTimeout(debounceTimerRef.current);
|
232 |
}
|
233 |
};
|
234 |
-
|
|
|
|
|
|
|
|
|
235 |
|
236 |
// Render based on language type
|
237 |
-
// If it's a mermaid language block and
|
238 |
-
if (language === 'mermaid' && !
|
239 |
return (
|
240 |
<SyntaxHighlighter
|
241 |
style={theme === 'dark' ? oneDark : oneLight}
|
@@ -273,4 +292,7 @@ const CodeHighlight = ({ className, children, node, isComplete = true, ...props
|
|
273 |
{children}
|
274 |
</code>
|
275 |
);
|
276 |
-
};
|
|
|
|
|
|
|
|
1 |
+
import { ReactNode, useCallback, useEffect, useMemo, useRef, memo, useState } from 'react' // Import useMemo
|
2 |
import { Message } from '@/api/lightrag'
|
3 |
import useTheme from '@/hooks/useTheme'
|
4 |
import Button from '@/components/ui/Button'
|
|
|
19 |
import { useTranslation } from 'react-i18next'
|
20 |
|
21 |
export type MessageWithError = Message & {
|
22 |
+
id: string // Unique identifier for stable React keys
|
23 |
isError?: boolean
|
24 |
+
/**
|
25 |
+
* Indicates if the mermaid diagram in this message has been rendered.
|
26 |
+
* Used to persist the rendering state across updates and prevent flickering.
|
27 |
+
*/
|
28 |
+
mermaidRendered?: boolean
|
29 |
}
|
30 |
|
31 |
+
// Restore original component definition and export
|
32 |
+
export const ChatMessage = ({ message }: { message: MessageWithError }) => { // Remove isComplete prop
|
33 |
const { t } = useTranslation()
|
34 |
const handleCopyMarkdown = useCallback(async () => {
|
35 |
if (message.content) {
|
|
|
57 |
remarkPlugins={[remarkGfm, remarkMath]}
|
58 |
rehypePlugins={[rehypeReact]}
|
59 |
skipHtml={false}
|
60 |
+
// Memoize the components object to prevent unnecessary re-renders of ReactMarkdown children
|
61 |
+
components={useMemo(() => ({
|
62 |
+
code: (props: any) => ( // Add type annotation if needed, e.g., props: CodeProps from 'react-markdown/lib/ast-to-react'
|
63 |
+
<CodeHighlight
|
64 |
+
{...props}
|
65 |
+
renderAsDiagram={message.mermaidRendered ?? false}
|
66 |
+
/>
|
67 |
+
),
|
68 |
+
p: ({ children }: { children?: ReactNode }) => <p className="my-2">{children}</p>,
|
69 |
+
h1: ({ children }: { children?: ReactNode }) => <h1 className="text-xl font-bold mt-4 mb-2">{children}</h1>,
|
70 |
+
h2: ({ children }: { children?: ReactNode }) => <h2 className="text-lg font-bold mt-4 mb-2">{children}</h2>,
|
71 |
+
h3: ({ children }: { children?: ReactNode }) => <h3 className="text-base font-bold mt-3 mb-2">{children}</h3>,
|
72 |
+
h4: ({ children }: { children?: ReactNode }) => <h4 className="text-base font-semibold mt-3 mb-2">{children}</h4>,
|
73 |
+
ul: ({ children }: { children?: ReactNode }) => <ul className="list-disc pl-5 my-2">{children}</ul>,
|
74 |
+
ol: ({ children }: { children?: ReactNode }) => <ol className="list-decimal pl-5 my-2">{children}</ol>,
|
75 |
+
li: ({ children }: { children?: ReactNode }) => <li className="my-1">{children}</li>
|
76 |
+
}), [message.mermaidRendered])} // Dependency ensures update if mermaid state changes
|
77 |
>
|
78 |
{message.content}
|
79 |
</ReactMarkdown>
|
|
|
94 |
)
|
95 |
}
|
96 |
|
97 |
+
// Remove the incorrect memo export line
|
98 |
+
|
99 |
interface CodeHighlightProps {
|
100 |
inline?: boolean
|
101 |
className?: string
|
102 |
children?: ReactNode
|
103 |
node?: Element // Keep node for inline check
|
104 |
+
renderAsDiagram?: boolean // Flag to indicate if rendering as diagram should be attempted
|
105 |
}
|
106 |
|
107 |
// Helper function remains the same
|
|
|
116 |
};
|
117 |
|
118 |
|
119 |
+
// Memoize the CodeHighlight component
|
120 |
+
const CodeHighlight = memo(({ className, children, node, renderAsDiagram = false, ...props }: CodeHighlightProps) => {
|
121 |
const { theme } = useTheme();
|
122 |
+
const [hasRendered, setHasRendered] = useState(false); // State to track successful render
|
123 |
const match = className?.match(/language-(\w+)/);
|
124 |
const language = match ? match[1] : undefined;
|
125 |
const inline = isInlineCode(node); // Use the helper function
|
|
|
128 |
|
129 |
// Handle Mermaid rendering with debounce
|
130 |
useEffect(() => {
|
131 |
+
// Effect should run when renderAsDiagram becomes true or hasRendered changes.
|
132 |
+
// The actual rendering logic inside checks language and hasRendered state.
|
133 |
+
if (renderAsDiagram && !hasRendered && language === 'mermaid' && mermaidRef.current) {
|
134 |
+
const container = mermaidRef.current; // Capture ref value
|
135 |
|
136 |
+
// Clear previous timer if dependencies change before timeout (e.g., renderAsDiagram flips quickly)
|
137 |
+
if (debounceTimerRef.current) {
|
138 |
+
clearTimeout(debounceTimerRef.current);
|
139 |
+
}
|
140 |
|
|
|
141 |
debounceTimerRef.current = setTimeout(() => {
|
142 |
+
if (!container) return; // Container might have unmounted
|
143 |
+
|
144 |
+
// Double check hasRendered state inside timeout, in case it changed rapidly
|
145 |
+
if (hasRendered) return;
|
146 |
|
147 |
try {
|
148 |
+
// Initialize mermaid config
|
149 |
mermaid.initialize({
|
150 |
startOnLoad: false,
|
151 |
theme: theme === 'dark' ? 'dark' : 'default',
|
152 |
securityLevel: 'loose',
|
153 |
});
|
154 |
|
155 |
+
// Show loading indicator
|
156 |
container.innerHTML = '<div class="flex justify-center items-center p-4"><svg class="animate-spin h-5 w-5 text-primary" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"><circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle><path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path></svg></div>';
|
157 |
|
158 |
// Preprocess mermaid content
|
159 |
+
const rawContent = String(children).replace(/\n$/, '').trim();
|
160 |
|
161 |
// Heuristic check for potentially complete graph definition
|
|
|
162 |
const looksPotentiallyComplete = rawContent.length > 10 && (
|
163 |
rawContent.startsWith('graph') ||
|
164 |
rawContent.startsWith('sequenceDiagram') ||
|
|
|
170 |
rawContent.startsWith('erDiagram')
|
171 |
);
|
172 |
|
|
|
173 |
if (!looksPotentiallyComplete) {
|
174 |
console.log('Mermaid content might be incomplete, skipping render attempt:', rawContent);
|
175 |
+
// Optionally keep loading indicator or show a message
|
176 |
// container.innerHTML = '<p class="text-sm text-muted-foreground">Waiting for complete diagram...</p>';
|
177 |
+
return;
|
178 |
}
|
179 |
|
180 |
const processedContent = rawContent
|
181 |
.split('\n')
|
182 |
.map(line => {
|
183 |
const trimmedLine = line.trim();
|
|
|
184 |
if (trimmedLine.startsWith('subgraph')) {
|
185 |
const parts = trimmedLine.split(' ');
|
186 |
if (parts.length > 1) {
|
|
|
190 |
}
|
191 |
return trimmedLine;
|
192 |
})
|
193 |
+
.filter(line => !line.trim().startsWith('linkStyle'))
|
194 |
.join('\n');
|
195 |
|
196 |
const mermaidId = `mermaid-${Date.now()}`;
|
197 |
mermaid.render(mermaidId, processedContent)
|
198 |
.then(({ svg, bindFunctions }) => {
|
199 |
+
// Check ref and hasRendered state again inside async callback
|
200 |
+
if (mermaidRef.current === container && !hasRendered) {
|
|
|
201 |
container.innerHTML = svg;
|
202 |
+
setHasRendered(true); // Mark as rendered successfully
|
203 |
if (bindFunctions) {
|
204 |
+
try {
|
205 |
bindFunctions(container);
|
206 |
} catch (bindError) {
|
207 |
console.error('Mermaid bindFunctions error:', bindError);
|
|
|
208 |
container.innerHTML += '<p class="text-orange-500 text-xs">Diagram interactions might be limited.</p>';
|
209 |
}
|
210 |
}
|
211 |
+
} else if (mermaidRef.current !== container) {
|
212 |
console.log('Mermaid container changed before rendering completed.');
|
213 |
}
|
214 |
})
|
|
|
217 |
console.error('Failed content (debounced):', processedContent);
|
218 |
if (mermaidRef.current === container) {
|
219 |
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
|
220 |
const errorPre = document.createElement('pre');
|
221 |
errorPre.className = 'text-red-500 text-xs whitespace-pre-wrap break-words';
|
222 |
errorPre.textContent = `Mermaid diagram error: ${errorMessage}\n\nContent:\n${processedContent}`;
|
223 |
+
container.innerHTML = '';
|
224 |
container.appendChild(errorPre);
|
225 |
}
|
226 |
});
|
|
|
233 |
const errorPre = document.createElement('pre');
|
234 |
errorPre.className = 'text-red-500 text-xs whitespace-pre-wrap break-words';
|
235 |
errorPre.textContent = `Mermaid diagram setup error: ${errorMessage}`;
|
236 |
+
container.innerHTML = '';
|
237 |
container.appendChild(errorPre);
|
238 |
}
|
239 |
}
|
240 |
+
}, 300); // Debounce delay
|
241 |
}
|
242 |
|
243 |
+
// Cleanup function to clear the timer on unmount or before re-running effect
|
244 |
return () => {
|
245 |
if (debounceTimerRef.current) {
|
246 |
clearTimeout(debounceTimerRef.current);
|
247 |
}
|
248 |
};
|
249 |
+
// Dependencies: renderAsDiagram ensures effect runs when diagram should be shown.
|
250 |
+
// children, language, theme trigger re-render if code/context changes.
|
251 |
+
// Dependencies are minimal: only run when the intent to render changes or the rendered state changes.
|
252 |
+
// Access children, theme, language inside the effect when needed.
|
253 |
+
}, [renderAsDiagram, hasRendered, language]); // Keep language to ensure it IS mermaid
|
254 |
|
255 |
// Render based on language type
|
256 |
+
// If it's a mermaid language block and rendering as diagram is not requested (e.g., incomplete stream), display as plain text
|
257 |
+
if (language === 'mermaid' && !renderAsDiagram) {
|
258 |
return (
|
259 |
<SyntaxHighlighter
|
260 |
style={theme === 'dark' ? oneDark : oneLight}
|
|
|
292 |
{children}
|
293 |
</code>
|
294 |
);
|
295 |
+
});
|
296 |
+
|
297 |
+
// Assign display name for React DevTools
|
298 |
+
CodeHighlight.displayName = 'CodeHighlight';
|
lightrag_webui/src/features/RetrievalTesting.tsx
CHANGED
@@ -2,7 +2,7 @@ import Input from '@/components/ui/Input'
|
|
2 |
import Button from '@/components/ui/Button'
|
3 |
import { useCallback, useEffect, useRef, useState } from 'react'
|
4 |
import { throttle } from '@/lib/utils'
|
5 |
-
import { queryText, queryTextStream
|
6 |
import { errorMessage } from '@/lib/utils'
|
7 |
import { useSettingsStore } from '@/stores/settings'
|
8 |
import { useDebounce } from '@/hooks/useDebounce'
|
@@ -14,9 +14,18 @@ import type { QueryMode } from '@/api/lightrag'
|
|
14 |
|
15 |
export default function RetrievalTesting() {
|
16 |
const { t } = useTranslation()
|
17 |
-
const [messages, setMessages] = useState<MessageWithError[]>(
|
18 |
-
|
19 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
20 |
const [inputValue, setInputValue] = useState('')
|
21 |
const [isLoading, setIsLoading] = useState(false)
|
22 |
const [inputError, setInputError] = useState('') // Error message for input
|
@@ -81,14 +90,17 @@ export default function RetrievalTesting() {
|
|
81 |
|
82 |
// Create messages
|
83 |
// Save the original input (with prefix if any) in userMessage.content for display
|
84 |
-
const userMessage:
|
|
|
85 |
content: inputValue,
|
86 |
role: 'user'
|
87 |
}
|
88 |
|
89 |
-
const assistantMessage:
|
|
|
90 |
content: '',
|
91 |
-
role: 'assistant'
|
|
|
92 |
}
|
93 |
|
94 |
const prevMessages = [...messages]
|
@@ -113,12 +125,28 @@ export default function RetrievalTesting() {
|
|
113 |
// Create a function to update the assistant's message
|
114 |
const updateAssistantMessage = (chunk: string, isError?: boolean) => {
|
115 |
assistantMessage.content += chunk
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
116 |
setMessages((prev) => {
|
117 |
const newMessages = [...prev]
|
118 |
const lastMessage = newMessages[newMessages.length - 1]
|
119 |
if (lastMessage.role === 'assistant') {
|
120 |
lastMessage.content = assistantMessage.content
|
121 |
lastMessage.isError = isError
|
|
|
122 |
}
|
123 |
return newMessages
|
124 |
})
|
@@ -279,20 +307,14 @@ export default function RetrievalTesting() {
|
|
279 |
{t('retrievePanel.retrieval.startPrompt')}
|
280 |
</div>
|
281 |
) : (
|
282 |
-
messages.map((message
|
283 |
-
//
|
284 |
-
// 1. If it's not the last message, it's complete
|
285 |
-
// 2. If it's the last message but we're not receiving a streaming response, it's complete
|
286 |
-
// 3. If it's the last message and we're receiving a streaming response, it's not complete
|
287 |
-
const isLastMessage = idx === messages.length - 1;
|
288 |
-
const isMessageComplete = !isLastMessage || !isReceivingResponseRef.current;
|
289 |
-
|
290 |
return (
|
291 |
<div
|
292 |
-
key={
|
293 |
className={`flex ${message.role === 'user' ? 'justify-end' : 'justify-start'}`}
|
294 |
>
|
295 |
-
{<ChatMessage message={message}
|
296 |
</div>
|
297 |
);
|
298 |
})
|
|
|
2 |
import Button from '@/components/ui/Button'
|
3 |
import { useCallback, useEffect, useRef, useState } from 'react'
|
4 |
import { throttle } from '@/lib/utils'
|
5 |
+
import { queryText, queryTextStream } from '@/api/lightrag'
|
6 |
import { errorMessage } from '@/lib/utils'
|
7 |
import { useSettingsStore } from '@/stores/settings'
|
8 |
import { useDebounce } from '@/hooks/useDebounce'
|
|
|
14 |
|
15 |
export default function RetrievalTesting() {
|
16 |
const { t } = useTranslation()
|
17 |
+
const [messages, setMessages] = useState<MessageWithError[]>(() => {
|
18 |
+
const history = useSettingsStore.getState().retrievalHistory || []
|
19 |
+
// Ensure each message from history has a unique ID and mermaidRendered status
|
20 |
+
return history.map((msg, index) => {
|
21 |
+
const msgWithError = msg as MessageWithError // Cast to access potential properties
|
22 |
+
return {
|
23 |
+
...msg,
|
24 |
+
id: msgWithError.id || `hist-${Date.now()}-${index}`, // Add ID if missing
|
25 |
+
mermaidRendered: msgWithError.mermaidRendered ?? true // Assume historical mermaid is rendered
|
26 |
+
}
|
27 |
+
})
|
28 |
+
})
|
29 |
const [inputValue, setInputValue] = useState('')
|
30 |
const [isLoading, setIsLoading] = useState(false)
|
31 |
const [inputError, setInputError] = useState('') // Error message for input
|
|
|
90 |
|
91 |
// Create messages
|
92 |
// Save the original input (with prefix if any) in userMessage.content for display
|
93 |
+
const userMessage: MessageWithError = {
|
94 |
+
id: crypto.randomUUID(), // Add unique ID
|
95 |
content: inputValue,
|
96 |
role: 'user'
|
97 |
}
|
98 |
|
99 |
+
const assistantMessage: MessageWithError = {
|
100 |
+
id: crypto.randomUUID(), // Add unique ID
|
101 |
content: '',
|
102 |
+
role: 'assistant',
|
103 |
+
mermaidRendered: false
|
104 |
}
|
105 |
|
106 |
const prevMessages = [...messages]
|
|
|
125 |
// Create a function to update the assistant's message
|
126 |
const updateAssistantMessage = (chunk: string, isError?: boolean) => {
|
127 |
assistantMessage.content += chunk
|
128 |
+
|
129 |
+
// Detect if the assistant message contains a complete mermaid code block
|
130 |
+
// Simple heuristic: look for ```mermaid ... ```
|
131 |
+
const mermaidBlockRegex = /```mermaid\s+([\s\S]+?)```/g
|
132 |
+
let mermaidRendered = false
|
133 |
+
let match
|
134 |
+
while ((match = mermaidBlockRegex.exec(assistantMessage.content)) !== null) {
|
135 |
+
// If the block is not too short, consider it complete
|
136 |
+
if (match[1] && match[1].trim().length > 10) {
|
137 |
+
mermaidRendered = true
|
138 |
+
break
|
139 |
+
}
|
140 |
+
}
|
141 |
+
assistantMessage.mermaidRendered = mermaidRendered
|
142 |
+
|
143 |
setMessages((prev) => {
|
144 |
const newMessages = [...prev]
|
145 |
const lastMessage = newMessages[newMessages.length - 1]
|
146 |
if (lastMessage.role === 'assistant') {
|
147 |
lastMessage.content = assistantMessage.content
|
148 |
lastMessage.isError = isError
|
149 |
+
lastMessage.mermaidRendered = assistantMessage.mermaidRendered
|
150 |
}
|
151 |
return newMessages
|
152 |
})
|
|
|
307 |
{t('retrievePanel.retrieval.startPrompt')}
|
308 |
</div>
|
309 |
) : (
|
310 |
+
messages.map((message) => { // Remove unused idx
|
311 |
+
// isComplete logic is now handled internally based on message.mermaidRendered
|
|
|
|
|
|
|
|
|
|
|
|
|
312 |
return (
|
313 |
<div
|
314 |
+
key={message.id} // Use stable ID for key
|
315 |
className={`flex ${message.role === 'user' ? 'justify-end' : 'justify-start'}`}
|
316 |
>
|
317 |
+
{<ChatMessage message={message} />}
|
318 |
</div>
|
319 |
);
|
320 |
})
|