@hef2024/llmasaservice-ui 0.22.11 → 0.23.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.css +632 -1
- package/dist/index.d.mts +29 -1
- package/dist/index.d.ts +29 -1
- package/dist/index.js +3829 -3465
- package/dist/index.mjs +3760 -3398
- package/package.json +1 -1
- package/src/AIChatPanel.css +365 -0
- package/src/AIChatPanel.tsx +251 -88
- package/src/ChatPanel.css +379 -3
- package/src/ChatPanel.tsx +264 -190
- package/src/components/ui/ThinkingBlock.tsx +150 -0
- package/src/components/ui/WordFadeIn.tsx +101 -0
- package/src/components/ui/index.ts +6 -0
package/src/AIChatPanel.tsx
CHANGED
|
@@ -19,7 +19,7 @@ import rehypeRaw from 'rehype-raw';
|
|
|
19
19
|
import { Prism as SyntaxHighlighter } from 'react-syntax-highlighter';
|
|
20
20
|
import materialDark from 'react-syntax-highlighter/dist/esm/styles/prism/material-dark.js';
|
|
21
21
|
import materialLight from 'react-syntax-highlighter/dist/esm/styles/prism/material-light.js';
|
|
22
|
-
import { Button, ScrollArea, Tooltip } from './components/ui';
|
|
22
|
+
import { Button, ScrollArea, Tooltip, ThinkingBlock as ThinkingBlockComponent } from './components/ui';
|
|
23
23
|
import ToolInfoModal from './components/ui/ToolInfoModal';
|
|
24
24
|
import './AIChatPanel.css';
|
|
25
25
|
|
|
@@ -136,7 +136,7 @@ interface HistoryEntry {
|
|
|
136
136
|
}
|
|
137
137
|
|
|
138
138
|
interface ThinkingBlock {
|
|
139
|
-
type: 'reasoning' | 'searching';
|
|
139
|
+
type: 'thinking' | 'reasoning' | 'searching';
|
|
140
140
|
content: string;
|
|
141
141
|
index: number;
|
|
142
142
|
}
|
|
@@ -792,6 +792,11 @@ const AIChatPanel: React.FC<AIChatPanelProps> = ({
|
|
|
792
792
|
const [followOnQuestionsState, setFollowOnQuestionsState] = useState(followOnQuestions);
|
|
793
793
|
const [thinkingBlocks, setThinkingBlocks] = useState<ThinkingBlock[]>([]);
|
|
794
794
|
const [currentThinkingIndex, setCurrentThinkingIndex] = useState(0);
|
|
795
|
+
// NOTE: activeThinkingBlock is computed via useMemo, not useState - see below after processThinkingTags
|
|
796
|
+
// Track collapsed state per block (key: "block-{index}" or "active" for streaming block)
|
|
797
|
+
const [collapsedBlocks, setCollapsedBlocks] = useState<Set<string>>(new Set());
|
|
798
|
+
const hasAutoCollapsedRef = useRef(false); // Track if we've auto-collapsed for current response
|
|
799
|
+
const prevBlockCountRef = useRef(0); // Track previous block count to detect new blocks
|
|
795
800
|
const [newConversationConfirm, setNewConversationConfirm] = useState(false);
|
|
796
801
|
const [justReset, setJustReset] = useState(false);
|
|
797
802
|
const [copiedCallId, setCopiedCallId] = useState<string | null>(null);
|
|
@@ -1293,34 +1298,132 @@ const AIChatPanel: React.FC<AIChatPanelProps> = ({
|
|
|
1293
1298
|
}, []);
|
|
1294
1299
|
|
|
1295
1300
|
// Process thinking tags from response
|
|
1296
|
-
const processThinkingTags = useCallback((text: string): {
|
|
1297
|
-
|
|
1298
|
-
|
|
1301
|
+
const processThinkingTags = useCallback((text: string): {
|
|
1302
|
+
cleanedText: string;
|
|
1303
|
+
completedBlocks: ThinkingBlock[];
|
|
1304
|
+
activeBlock: { type: 'thinking' | 'reasoning' | 'searching'; content: string } | null;
|
|
1305
|
+
lastThinkingContent: string;
|
|
1306
|
+
} => {
|
|
1307
|
+
if (!text) {
|
|
1308
|
+
return {
|
|
1309
|
+
cleanedText: '',
|
|
1310
|
+
completedBlocks: [],
|
|
1311
|
+
activeBlock: null,
|
|
1312
|
+
lastThinkingContent: 'Thinking',
|
|
1313
|
+
};
|
|
1314
|
+
}
|
|
1315
|
+
|
|
1316
|
+
// Remove zero-width space characters from keepalive before processing
|
|
1317
|
+
const processedText = text.replace(/\u200B/g, '');
|
|
1318
|
+
|
|
1319
|
+
const allMatches: ThinkingBlock[] = [];
|
|
1299
1320
|
|
|
1300
|
-
// Extract
|
|
1301
|
-
const
|
|
1321
|
+
// Extract complete thinking blocks
|
|
1322
|
+
const thinkingRegex = /<thinking>([\s\S]*?)<\/thinking>/gi;
|
|
1302
1323
|
let match;
|
|
1303
|
-
while ((match =
|
|
1304
|
-
|
|
1324
|
+
while ((match = thinkingRegex.exec(processedText)) !== null) {
|
|
1325
|
+
const content = match[1]?.trim();
|
|
1326
|
+
if (content) {
|
|
1327
|
+
allMatches.push({ content, index: match.index, type: 'thinking' });
|
|
1328
|
+
}
|
|
1329
|
+
}
|
|
1330
|
+
|
|
1331
|
+
// Extract complete reasoning blocks
|
|
1332
|
+
const reasoningRegex = /<reasoning>([\s\S]*?)<\/reasoning>/gi;
|
|
1333
|
+
while ((match = reasoningRegex.exec(processedText)) !== null) {
|
|
1334
|
+
const content = match[1]?.trim();
|
|
1335
|
+
if (content) {
|
|
1336
|
+
allMatches.push({ content, index: match.index, type: 'reasoning' });
|
|
1337
|
+
}
|
|
1305
1338
|
}
|
|
1306
1339
|
|
|
1307
|
-
// Extract searching blocks
|
|
1340
|
+
// Extract complete searching blocks
|
|
1308
1341
|
const searchingRegex = /<searching>([\s\S]*?)<\/searching>/gi;
|
|
1309
|
-
while ((match = searchingRegex.exec(
|
|
1310
|
-
|
|
1342
|
+
while ((match = searchingRegex.exec(processedText)) !== null) {
|
|
1343
|
+
const content = match[1]?.trim();
|
|
1344
|
+
if (content) {
|
|
1345
|
+
allMatches.push({ content, index: match.index, type: 'searching' });
|
|
1346
|
+
}
|
|
1311
1347
|
}
|
|
1312
1348
|
|
|
1313
1349
|
// Sort by position in original text
|
|
1314
|
-
|
|
1350
|
+
const completedBlocks = allMatches.sort((a, b) => a.index - b.index);
|
|
1351
|
+
|
|
1352
|
+
// Check for incomplete (streaming) tags at the end of the text
|
|
1353
|
+
let activeBlock: { type: 'thinking' | 'reasoning' | 'searching'; content: string } | null = null;
|
|
1354
|
+
const tagTypes = ['thinking', 'reasoning', 'searching'] as const;
|
|
1355
|
+
let latestIncompletePos = -1;
|
|
1315
1356
|
|
|
1316
|
-
|
|
1317
|
-
|
|
1357
|
+
for (const tagType of tagTypes) {
|
|
1358
|
+
const openTag = `<${tagType}>`;
|
|
1359
|
+
const closeTag = `</${tagType}>`;
|
|
1360
|
+
const textLower = processedText.toLowerCase();
|
|
1361
|
+
|
|
1362
|
+
// Find the last occurrence of this opening tag
|
|
1363
|
+
const lastOpenIndex = textLower.lastIndexOf(openTag);
|
|
1364
|
+
if (lastOpenIndex === -1) continue;
|
|
1365
|
+
|
|
1366
|
+
// Check if there's a closing tag after this opening tag
|
|
1367
|
+
const afterOpen = processedText.slice(lastOpenIndex + openTag.length);
|
|
1368
|
+
const closeIndex = afterOpen.toLowerCase().indexOf(closeTag);
|
|
1369
|
+
|
|
1370
|
+
// If no closing tag found after the opening tag, this is incomplete
|
|
1371
|
+
if (closeIndex === -1 && lastOpenIndex > latestIncompletePos) {
|
|
1372
|
+
latestIncompletePos = lastOpenIndex;
|
|
1373
|
+
activeBlock = { type: tagType, content: afterOpen };
|
|
1374
|
+
}
|
|
1375
|
+
}
|
|
1376
|
+
|
|
1377
|
+
// Also check for partial opening tags (e.g., "<reas" before full "<reasoning>")
|
|
1378
|
+
if (!activeBlock) {
|
|
1379
|
+
const partialTagPatterns = [
|
|
1380
|
+
{ pattern: /<think(?:i(?:n(?:g)?)?)?$/i, type: 'thinking' as const },
|
|
1381
|
+
{ pattern: /<reas(?:o(?:n(?:i(?:n(?:g)?)?)?)?)?$/i, type: 'reasoning' as const },
|
|
1382
|
+
{ pattern: /<sear(?:c(?:h(?:i(?:n(?:g)?)?)?)?)?$/i, type: 'searching' as const },
|
|
1383
|
+
];
|
|
1384
|
+
|
|
1385
|
+
for (const { pattern, type } of partialTagPatterns) {
|
|
1386
|
+
if (pattern.test(processedText)) {
|
|
1387
|
+
activeBlock = { type, content: '' };
|
|
1388
|
+
break;
|
|
1389
|
+
}
|
|
1390
|
+
}
|
|
1391
|
+
}
|
|
1392
|
+
|
|
1393
|
+
// Clean the text by removing all thinking-related tags (complete and incomplete)
|
|
1394
|
+
let cleanedText = processedText
|
|
1395
|
+
.replace(/<thinking>[\s\S]*?<\/thinking>/gi, '')
|
|
1318
1396
|
.replace(/<reasoning>[\s\S]*?<\/reasoning>/gi, '')
|
|
1319
1397
|
.replace(/<searching>[\s\S]*?<\/searching>/gi, '')
|
|
1398
|
+
// Also remove partial opening tags
|
|
1399
|
+
.replace(/<think(?:i(?:n(?:g)?)?)?$/i, '')
|
|
1400
|
+
.replace(/<reas(?:o(?:n(?:i(?:n(?:g)?)?)?)?)?$/i, '')
|
|
1401
|
+
.replace(/<sear(?:c(?:h(?:i(?:n(?:g)?)?)?)?)?$/i, '')
|
|
1402
|
+
.replace(/<thinking>[\s\S]*$/i, '')
|
|
1403
|
+
.replace(/<reasoning>[\s\S]*$/i, '')
|
|
1404
|
+
.replace(/<searching>[\s\S]*$/i, '')
|
|
1320
1405
|
.trim();
|
|
1321
|
-
|
|
1322
|
-
|
|
1323
|
-
|
|
1406
|
+
|
|
1407
|
+
// Get last thinking content for display
|
|
1408
|
+
let lastThinkingContent = 'Thinking';
|
|
1409
|
+
if (completedBlocks.length > 0) {
|
|
1410
|
+
const lastBlock = completedBlocks[completedBlocks.length - 1];
|
|
1411
|
+
if (lastBlock?.content) {
|
|
1412
|
+
lastThinkingContent = cleanContentForDisplay(lastBlock.content);
|
|
1413
|
+
}
|
|
1414
|
+
} else if (activeBlock?.content) {
|
|
1415
|
+
lastThinkingContent = cleanContentForDisplay(activeBlock.content);
|
|
1416
|
+
}
|
|
1417
|
+
|
|
1418
|
+
return { cleanedText, completedBlocks, activeBlock, lastThinkingContent };
|
|
1419
|
+
}, [cleanContentForDisplay]);
|
|
1420
|
+
|
|
1421
|
+
// Compute active thinking block directly from response during render (avoids state batching issues)
|
|
1422
|
+
const activeThinkingBlock = useMemo(() => {
|
|
1423
|
+
if (!response || justReset) return null;
|
|
1424
|
+
const { activeBlock } = processThinkingTags(response);
|
|
1425
|
+
return activeBlock;
|
|
1426
|
+
}, [response, justReset, processThinkingTags]);
|
|
1324
1427
|
|
|
1325
1428
|
// Built-in action for agent suggestion cards
|
|
1326
1429
|
// Pattern: [SUGGEST_AGENT:agent-id|Agent Name|Brief reason]
|
|
@@ -1516,8 +1619,12 @@ const AIChatPanel: React.FC<AIChatPanelProps> = ({
|
|
|
1516
1619
|
// promptText is now required - comes from the isolated ChatInput component
|
|
1517
1620
|
const continueChat = useCallback((promptText: string) => {
|
|
1518
1621
|
// Clear thinking blocks for new response
|
|
1622
|
+
// Note: activeThinkingBlock is computed via useMemo from response
|
|
1519
1623
|
setThinkingBlocks([]);
|
|
1520
1624
|
setCurrentThinkingIndex(0);
|
|
1625
|
+
setCollapsedBlocks(new Set());
|
|
1626
|
+
hasAutoCollapsedRef.current = false;
|
|
1627
|
+
prevBlockCountRef.current = 0;
|
|
1521
1628
|
|
|
1522
1629
|
// Clear any previous errors
|
|
1523
1630
|
setError(null);
|
|
@@ -1782,6 +1889,10 @@ const AIChatPanel: React.FC<AIChatPanelProps> = ({
|
|
|
1782
1889
|
setFollowOnQuestionsState(followOnQuestions);
|
|
1783
1890
|
setThinkingBlocks([]);
|
|
1784
1891
|
setCurrentThinkingIndex(0);
|
|
1892
|
+
setCollapsedBlocks(new Set());
|
|
1893
|
+
// Note: activeThinkingBlock is computed via useMemo from response
|
|
1894
|
+
hasAutoCollapsedRef.current = false;
|
|
1895
|
+
prevBlockCountRef.current = 0;
|
|
1785
1896
|
setJustReset(true);
|
|
1786
1897
|
setLastController(new AbortController());
|
|
1787
1898
|
setUserHasScrolled(false);
|
|
@@ -1805,10 +1916,40 @@ const AIChatPanel: React.FC<AIChatPanelProps> = ({
|
|
|
1805
1916
|
useEffect(() => {
|
|
1806
1917
|
if (!response || !lastKey || justReset) return;
|
|
1807
1918
|
|
|
1808
|
-
const { cleanedText,
|
|
1919
|
+
const { cleanedText, completedBlocks } = processThinkingTags(response);
|
|
1809
1920
|
|
|
1810
1921
|
// Update display state
|
|
1811
|
-
|
|
1922
|
+
// Note: activeThinkingBlock is computed via useMemo from response directly
|
|
1923
|
+
setThinkingBlocks(completedBlocks);
|
|
1924
|
+
|
|
1925
|
+
// When a new block appears, collapse all previous blocks
|
|
1926
|
+
if (completedBlocks.length > prevBlockCountRef.current) {
|
|
1927
|
+
setCollapsedBlocks(prev => {
|
|
1928
|
+
const next = new Set(prev);
|
|
1929
|
+
// Collapse all blocks except the newest one
|
|
1930
|
+
for (let i = 0; i < completedBlocks.length - 1; i++) {
|
|
1931
|
+
next.add(`block-${i}`);
|
|
1932
|
+
}
|
|
1933
|
+
return next;
|
|
1934
|
+
});
|
|
1935
|
+
prevBlockCountRef.current = completedBlocks.length;
|
|
1936
|
+
}
|
|
1937
|
+
|
|
1938
|
+
// Auto-collapse all thinking blocks when main content starts appearing
|
|
1939
|
+
const hasMainContent = cleanedText.trim().length > 0;
|
|
1940
|
+
const hasThinkingContent = completedBlocks.length > 0 || processThinkingTags(response).activeBlock !== null;
|
|
1941
|
+
if (hasMainContent && hasThinkingContent && !hasAutoCollapsedRef.current) {
|
|
1942
|
+
hasAutoCollapsedRef.current = true;
|
|
1943
|
+
setTimeout(() => {
|
|
1944
|
+
// Collapse all blocks including active
|
|
1945
|
+
setCollapsedBlocks(prev => {
|
|
1946
|
+
const next = new Set(prev);
|
|
1947
|
+
completedBlocks.forEach((_, index) => next.add(`block-${index}`));
|
|
1948
|
+
next.add('active');
|
|
1949
|
+
return next;
|
|
1950
|
+
});
|
|
1951
|
+
}, 500);
|
|
1952
|
+
}
|
|
1812
1953
|
|
|
1813
1954
|
// Update history state with RAW content (actions applied at render time)
|
|
1814
1955
|
setHistory((prev) => {
|
|
@@ -2307,46 +2448,56 @@ const AIChatPanel: React.FC<AIChatPanelProps> = ({
|
|
|
2307
2448
|
},
|
|
2308
2449
|
}), [CodeBlock, AgentSuggestionCard]);
|
|
2309
2450
|
|
|
2310
|
-
// Render thinking blocks
|
|
2311
|
-
const renderThinkingBlocks = useCallback(() => {
|
|
2312
|
-
|
|
2451
|
+
// Render thinking blocks with new collapsible design
|
|
2452
|
+
const renderThinkingBlocks = useCallback((isStreaming: boolean = false): React.ReactElement | null => {
|
|
2453
|
+
const hasActiveBlock = activeThinkingBlock !== null;
|
|
2454
|
+
const hasCompletedBlocks = thinkingBlocks.length > 0;
|
|
2313
2455
|
|
|
2314
|
-
|
|
2315
|
-
if (!currentBlock) return null;
|
|
2456
|
+
if (!hasActiveBlock && !hasCompletedBlocks) return null;
|
|
2316
2457
|
|
|
2317
|
-
const
|
|
2318
|
-
|
|
2319
|
-
|
|
2458
|
+
const handleToggleCollapse = (blockKey: string) => {
|
|
2459
|
+
setCollapsedBlocks(prev => {
|
|
2460
|
+
const next = new Set(prev);
|
|
2461
|
+
if (next.has(blockKey)) {
|
|
2462
|
+
next.delete(blockKey);
|
|
2463
|
+
} else {
|
|
2464
|
+
next.add(blockKey);
|
|
2465
|
+
}
|
|
2466
|
+
return next;
|
|
2467
|
+
});
|
|
2468
|
+
};
|
|
2320
2469
|
|
|
2321
2470
|
return (
|
|
2322
|
-
|
|
2323
|
-
|
|
2324
|
-
|
|
2325
|
-
|
|
2326
|
-
|
|
2327
|
-
<
|
|
2328
|
-
|
|
2329
|
-
|
|
2330
|
-
|
|
2331
|
-
|
|
2332
|
-
|
|
2333
|
-
|
|
2334
|
-
|
|
2335
|
-
|
|
2336
|
-
|
|
2337
|
-
|
|
2338
|
-
|
|
2339
|
-
|
|
2340
|
-
|
|
2341
|
-
|
|
2342
|
-
|
|
2343
|
-
|
|
2344
|
-
|
|
2345
|
-
|
|
2346
|
-
|
|
2347
|
-
|
|
2471
|
+
<>
|
|
2472
|
+
{/* Render completed blocks first */}
|
|
2473
|
+
{thinkingBlocks.map((block, index) => {
|
|
2474
|
+
const blockKey = `block-${index}`;
|
|
2475
|
+
return (
|
|
2476
|
+
<ThinkingBlockComponent
|
|
2477
|
+
key={blockKey}
|
|
2478
|
+
type={block.type}
|
|
2479
|
+
content={block.content}
|
|
2480
|
+
isStreaming={false}
|
|
2481
|
+
isCollapsed={collapsedBlocks.has(blockKey)}
|
|
2482
|
+
onToggleCollapse={() => handleToggleCollapse(blockKey)}
|
|
2483
|
+
/>
|
|
2484
|
+
);
|
|
2485
|
+
})}
|
|
2486
|
+
|
|
2487
|
+
{/* Render active (streaming) block */}
|
|
2488
|
+
{activeThinkingBlock && (
|
|
2489
|
+
<ThinkingBlockComponent
|
|
2490
|
+
key="active-streaming"
|
|
2491
|
+
type={activeThinkingBlock.type}
|
|
2492
|
+
content={activeThinkingBlock.content}
|
|
2493
|
+
isStreaming={true}
|
|
2494
|
+
isCollapsed={collapsedBlocks.has('active')}
|
|
2495
|
+
onToggleCollapse={() => handleToggleCollapse('active')}
|
|
2496
|
+
/>
|
|
2497
|
+
)}
|
|
2498
|
+
</>
|
|
2348
2499
|
);
|
|
2349
|
-
}, [thinkingBlocks,
|
|
2500
|
+
}, [thinkingBlocks, activeThinkingBlock, collapsedBlocks]);
|
|
2350
2501
|
|
|
2351
2502
|
// ============================================================================
|
|
2352
2503
|
// Render
|
|
@@ -2442,43 +2593,55 @@ const AIChatPanel: React.FC<AIChatPanelProps> = ({
|
|
|
2442
2593
|
<div className="ai-chat-message__content">
|
|
2443
2594
|
{/* Streaming state */}
|
|
2444
2595
|
{isLastEntry && (isLoading || !idle) && !justReset ? (
|
|
2445
|
-
|
|
2446
|
-
|
|
2596
|
+
(() => {
|
|
2597
|
+
// During streaming, compute content directly from response (not from history which may be stale)
|
|
2598
|
+
const { cleanedText: streamingCleanedText } = processThinkingTags(response || '');
|
|
2599
|
+
const streamingContent = processActions(streamingCleanedText);
|
|
2600
|
+
const hasStreamingContent = streamingContent.trim().length > 0;
|
|
2447
2601
|
|
|
2448
|
-
|
|
2449
|
-
|
|
2450
|
-
|
|
2451
|
-
|
|
2452
|
-
|
|
2453
|
-
|
|
2454
|
-
|
|
2455
|
-
|
|
2456
|
-
{
|
|
2457
|
-
|
|
2458
|
-
|
|
2459
|
-
|
|
2460
|
-
|
|
2461
|
-
|
|
2462
|
-
|
|
2463
|
-
|
|
2464
|
-
|
|
2465
|
-
|
|
2466
|
-
|
|
2467
|
-
|
|
2468
|
-
|
|
2469
|
-
|
|
2470
|
-
|
|
2471
|
-
|
|
2472
|
-
|
|
2473
|
-
|
|
2474
|
-
|
|
2475
|
-
|
|
2602
|
+
return (
|
|
2603
|
+
<div className="ai-chat-streaming">
|
|
2604
|
+
{/* Show thinking blocks (both completed and active/streaming) */}
|
|
2605
|
+
{(thinkingBlocks.length > 0 || activeThinkingBlock) && renderThinkingBlocks(true)}
|
|
2606
|
+
|
|
2607
|
+
{/* Show streaming content or loading indicator */}
|
|
2608
|
+
{hasStreamingContent ? (
|
|
2609
|
+
markdownClass ? (
|
|
2610
|
+
<div className={markdownClass}>
|
|
2611
|
+
<ReactMarkdown
|
|
2612
|
+
remarkPlugins={[remarkGfm]}
|
|
2613
|
+
rehypePlugins={[rehypeRaw]}
|
|
2614
|
+
components={markdownComponents}
|
|
2615
|
+
>
|
|
2616
|
+
{streamingContent}
|
|
2617
|
+
</ReactMarkdown>
|
|
2618
|
+
</div>
|
|
2619
|
+
) : (
|
|
2620
|
+
<ReactMarkdown
|
|
2621
|
+
remarkPlugins={[remarkGfm]}
|
|
2622
|
+
rehypePlugins={[rehypeRaw]}
|
|
2623
|
+
components={markdownComponents}
|
|
2624
|
+
>
|
|
2625
|
+
{streamingContent}
|
|
2626
|
+
</ReactMarkdown>
|
|
2627
|
+
)
|
|
2628
|
+
) : (
|
|
2629
|
+
<div className="ai-chat-loading">
|
|
2630
|
+
<span>{thinkingBlocks.length > 0 || activeThinkingBlock ? 'Still thinking' : 'Thinking'}</span>
|
|
2631
|
+
<span className="ai-chat-loading__dots">
|
|
2632
|
+
<span className="ai-chat-loading__dot" />
|
|
2633
|
+
<span className="ai-chat-loading__dot" />
|
|
2634
|
+
<span className="ai-chat-loading__dot" />
|
|
2635
|
+
</span>
|
|
2636
|
+
</div>
|
|
2637
|
+
)}
|
|
2476
2638
|
</div>
|
|
2477
|
-
)
|
|
2478
|
-
|
|
2639
|
+
);
|
|
2640
|
+
})()
|
|
2479
2641
|
) : (
|
|
2480
2642
|
<>
|
|
2481
|
-
{
|
|
2643
|
+
{/* Show completed thinking blocks after streaming */}
|
|
2644
|
+
{isLastEntry && thinkingBlocks.length > 0 && renderThinkingBlocks(false)}
|
|
2482
2645
|
{markdownClass ? (
|
|
2483
2646
|
<div className={markdownClass}>
|
|
2484
2647
|
<ReactMarkdown
|