@hef2024/llmasaservice-ui 0.22.10 → 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 +3905 -3488
- package/dist/index.mjs +3837 -3422
- package/package.json +1 -1
- package/src/AIChatPanel.css +365 -0
- package/src/AIChatPanel.tsx +363 -112
- 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,11 +792,17 @@ 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);
|
|
798
803
|
const [feedbackCallId, setFeedbackCallId] = useState<{ callId: string; type: 'up' | 'down' } | null>(null);
|
|
799
804
|
const [error, setError] = useState<{ message: string; code?: string } | null>(null);
|
|
805
|
+
const lastProcessedErrorRef = useRef<string | null>(null);
|
|
800
806
|
|
|
801
807
|
// Email & Save state
|
|
802
808
|
const [emailSent, setEmailSent] = useState(false);
|
|
@@ -1292,34 +1298,132 @@ const AIChatPanel: React.FC<AIChatPanelProps> = ({
|
|
|
1292
1298
|
}, []);
|
|
1293
1299
|
|
|
1294
1300
|
// Process thinking tags from response
|
|
1295
|
-
const processThinkingTags = useCallback((text: string): {
|
|
1296
|
-
|
|
1297
|
-
|
|
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[] = [];
|
|
1298
1320
|
|
|
1299
|
-
// Extract
|
|
1300
|
-
const
|
|
1321
|
+
// Extract complete thinking blocks
|
|
1322
|
+
const thinkingRegex = /<thinking>([\s\S]*?)<\/thinking>/gi;
|
|
1301
1323
|
let match;
|
|
1302
|
-
while ((match =
|
|
1303
|
-
|
|
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
|
+
}
|
|
1304
1329
|
}
|
|
1305
1330
|
|
|
1306
|
-
// Extract
|
|
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
|
+
}
|
|
1338
|
+
}
|
|
1339
|
+
|
|
1340
|
+
// Extract complete searching blocks
|
|
1307
1341
|
const searchingRegex = /<searching>([\s\S]*?)<\/searching>/gi;
|
|
1308
|
-
while ((match = searchingRegex.exec(
|
|
1309
|
-
|
|
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
|
+
}
|
|
1310
1347
|
}
|
|
1311
1348
|
|
|
1312
1349
|
// Sort by position in original text
|
|
1313
|
-
|
|
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;
|
|
1314
1356
|
|
|
1315
|
-
|
|
1316
|
-
|
|
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, '')
|
|
1317
1396
|
.replace(/<reasoning>[\s\S]*?<\/reasoning>/gi, '')
|
|
1318
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, '')
|
|
1319
1405
|
.trim();
|
|
1320
|
-
|
|
1321
|
-
|
|
1322
|
-
|
|
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]);
|
|
1323
1427
|
|
|
1324
1428
|
// Built-in action for agent suggestion cards
|
|
1325
1429
|
// Pattern: [SUGGEST_AGENT:agent-id|Agent Name|Brief reason]
|
|
@@ -1515,11 +1619,16 @@ const AIChatPanel: React.FC<AIChatPanelProps> = ({
|
|
|
1515
1619
|
// promptText is now required - comes from the isolated ChatInput component
|
|
1516
1620
|
const continueChat = useCallback((promptText: string) => {
|
|
1517
1621
|
// Clear thinking blocks for new response
|
|
1622
|
+
// Note: activeThinkingBlock is computed via useMemo from response
|
|
1518
1623
|
setThinkingBlocks([]);
|
|
1519
1624
|
setCurrentThinkingIndex(0);
|
|
1625
|
+
setCollapsedBlocks(new Set());
|
|
1626
|
+
hasAutoCollapsedRef.current = false;
|
|
1627
|
+
prevBlockCountRef.current = 0;
|
|
1520
1628
|
|
|
1521
1629
|
// Clear any previous errors
|
|
1522
1630
|
setError(null);
|
|
1631
|
+
lastProcessedErrorRef.current = null; // Allow new errors to be processed
|
|
1523
1632
|
|
|
1524
1633
|
// Reset scroll tracking for new message - enable auto-scroll
|
|
1525
1634
|
setUserHasScrolled(false);
|
|
@@ -1632,12 +1741,44 @@ const AIChatPanel: React.FC<AIChatPanelProps> = ({
|
|
|
1632
1741
|
// Error callback - handle errors immediately
|
|
1633
1742
|
console.log('[AIChatPanel] Error callback triggered:', errorMsg);
|
|
1634
1743
|
|
|
1744
|
+
// Check if this is a user-initiated abort
|
|
1745
|
+
const isAbortError = errorMsg.toLowerCase().includes('abort') ||
|
|
1746
|
+
errorMsg.toLowerCase().includes('canceled') ||
|
|
1747
|
+
errorMsg.toLowerCase().includes('cancelled');
|
|
1748
|
+
|
|
1749
|
+
if (isAbortError) {
|
|
1750
|
+
// User canceled the request - don't show error banner
|
|
1751
|
+
console.log('[AIChatPanel] Request was aborted by user');
|
|
1752
|
+
// Don't set error state - no red banner
|
|
1753
|
+
|
|
1754
|
+
// Update history to show cancellation
|
|
1755
|
+
if (promptKey) {
|
|
1756
|
+
setHistory((prev) => ({
|
|
1757
|
+
...prev,
|
|
1758
|
+
[promptKey]: {
|
|
1759
|
+
content: 'Response canceled',
|
|
1760
|
+
callId: lastCallId || '',
|
|
1761
|
+
},
|
|
1762
|
+
}));
|
|
1763
|
+
}
|
|
1764
|
+
}
|
|
1635
1765
|
// Detect 413 Content Too Large error
|
|
1636
|
-
if (errorMsg.includes('413') || errorMsg.toLowerCase().includes('content too large')) {
|
|
1766
|
+
else if (errorMsg.includes('413') || errorMsg.toLowerCase().includes('content too large')) {
|
|
1637
1767
|
setError({
|
|
1638
1768
|
message: 'The context is too large to process. Please start a new conversation or reduce the amount of context.',
|
|
1639
1769
|
code: '413',
|
|
1640
1770
|
});
|
|
1771
|
+
|
|
1772
|
+
// Update history to show error
|
|
1773
|
+
if (promptKey) {
|
|
1774
|
+
setHistory((prev) => ({
|
|
1775
|
+
...prev,
|
|
1776
|
+
[promptKey]: {
|
|
1777
|
+
content: `Error: ${errorMsg}`,
|
|
1778
|
+
callId: lastCallId || '',
|
|
1779
|
+
},
|
|
1780
|
+
}));
|
|
1781
|
+
}
|
|
1641
1782
|
}
|
|
1642
1783
|
// Detect other network errors
|
|
1643
1784
|
else if (errorMsg.toLowerCase().includes('network error') || errorMsg.toLowerCase().includes('fetch')) {
|
|
@@ -1645,6 +1786,17 @@ const AIChatPanel: React.FC<AIChatPanelProps> = ({
|
|
|
1645
1786
|
message: 'Network error. Please check your connection and try again.',
|
|
1646
1787
|
code: 'NETWORK_ERROR',
|
|
1647
1788
|
});
|
|
1789
|
+
|
|
1790
|
+
// Update history to show error
|
|
1791
|
+
if (promptKey) {
|
|
1792
|
+
setHistory((prev) => ({
|
|
1793
|
+
...prev,
|
|
1794
|
+
[promptKey]: {
|
|
1795
|
+
content: `Error: ${errorMsg}`,
|
|
1796
|
+
callId: lastCallId || '',
|
|
1797
|
+
},
|
|
1798
|
+
}));
|
|
1799
|
+
}
|
|
1648
1800
|
}
|
|
1649
1801
|
// Generic error
|
|
1650
1802
|
else {
|
|
@@ -1652,21 +1804,21 @@ const AIChatPanel: React.FC<AIChatPanelProps> = ({
|
|
|
1652
1804
|
message: errorMsg,
|
|
1653
1805
|
code: 'UNKNOWN_ERROR',
|
|
1654
1806
|
});
|
|
1807
|
+
|
|
1808
|
+
// Update history to show error
|
|
1809
|
+
if (promptKey) {
|
|
1810
|
+
setHistory((prev) => ({
|
|
1811
|
+
...prev,
|
|
1812
|
+
[promptKey]: {
|
|
1813
|
+
content: `Error: ${errorMsg}`,
|
|
1814
|
+
callId: lastCallId || '',
|
|
1815
|
+
},
|
|
1816
|
+
}));
|
|
1817
|
+
}
|
|
1655
1818
|
}
|
|
1656
1819
|
|
|
1657
1820
|
// Reset loading state
|
|
1658
1821
|
setIsLoading(false);
|
|
1659
|
-
|
|
1660
|
-
// Update history to show error
|
|
1661
|
-
if (promptKey) {
|
|
1662
|
-
setHistory((prev) => ({
|
|
1663
|
-
...prev,
|
|
1664
|
-
[promptKey]: {
|
|
1665
|
-
content: `Error: ${errorMsg}`,
|
|
1666
|
-
callId: lastCallId || '',
|
|
1667
|
-
},
|
|
1668
|
-
}));
|
|
1669
|
-
}
|
|
1670
1822
|
}
|
|
1671
1823
|
);
|
|
1672
1824
|
|
|
@@ -1737,6 +1889,10 @@ const AIChatPanel: React.FC<AIChatPanelProps> = ({
|
|
|
1737
1889
|
setFollowOnQuestionsState(followOnQuestions);
|
|
1738
1890
|
setThinkingBlocks([]);
|
|
1739
1891
|
setCurrentThinkingIndex(0);
|
|
1892
|
+
setCollapsedBlocks(new Set());
|
|
1893
|
+
// Note: activeThinkingBlock is computed via useMemo from response
|
|
1894
|
+
hasAutoCollapsedRef.current = false;
|
|
1895
|
+
prevBlockCountRef.current = 0;
|
|
1740
1896
|
setJustReset(true);
|
|
1741
1897
|
setLastController(new AbortController());
|
|
1742
1898
|
setUserHasScrolled(false);
|
|
@@ -1760,10 +1916,40 @@ const AIChatPanel: React.FC<AIChatPanelProps> = ({
|
|
|
1760
1916
|
useEffect(() => {
|
|
1761
1917
|
if (!response || !lastKey || justReset) return;
|
|
1762
1918
|
|
|
1763
|
-
const { cleanedText,
|
|
1919
|
+
const { cleanedText, completedBlocks } = processThinkingTags(response);
|
|
1764
1920
|
|
|
1765
1921
|
// Update display state
|
|
1766
|
-
|
|
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
|
+
}
|
|
1767
1953
|
|
|
1768
1954
|
// Update history state with RAW content (actions applied at render time)
|
|
1769
1955
|
setHistory((prev) => {
|
|
@@ -1959,17 +2145,49 @@ const AIChatPanel: React.FC<AIChatPanelProps> = ({
|
|
|
1959
2145
|
// Monitor for errors from useLLM hook
|
|
1960
2146
|
useEffect(() => {
|
|
1961
2147
|
if (llmError && llmError.trim()) {
|
|
2148
|
+
// Skip if we've already processed this exact error
|
|
2149
|
+
if (lastProcessedErrorRef.current === llmError) {
|
|
2150
|
+
console.log('[AIChatPanel] Skipping duplicate error:', llmError);
|
|
2151
|
+
return;
|
|
2152
|
+
}
|
|
2153
|
+
|
|
1962
2154
|
console.log('[AIChatPanel] Error detected:', llmError);
|
|
2155
|
+
lastProcessedErrorRef.current = llmError;
|
|
1963
2156
|
|
|
1964
2157
|
// Parse error message to detect specific error types
|
|
1965
2158
|
const errorMessage = llmError;
|
|
1966
2159
|
|
|
2160
|
+
// Check if this is a user-initiated abort
|
|
2161
|
+
const isAbortError = errorMessage.toLowerCase().includes('abort') ||
|
|
2162
|
+
errorMessage.toLowerCase().includes('canceled') ||
|
|
2163
|
+
errorMessage.toLowerCase().includes('cancelled');
|
|
2164
|
+
|
|
2165
|
+
if (isAbortError) {
|
|
2166
|
+
// User canceled the request - don't show error banner
|
|
2167
|
+
console.log('[AIChatPanel] Request was aborted by user (useEffect)');
|
|
2168
|
+
// Don't set error state - no red banner
|
|
2169
|
+
|
|
2170
|
+
// Don't update history here - the error callback in send() already handled it
|
|
2171
|
+
// with the correct promptKey. Updating here with lastKey can affect the wrong entry
|
|
2172
|
+
// if the user has already submitted a new prompt.
|
|
2173
|
+
}
|
|
1967
2174
|
// Detect 413 Content Too Large error
|
|
1968
|
-
if (errorMessage.includes('413') || errorMessage.toLowerCase().includes('content too large')) {
|
|
2175
|
+
else if (errorMessage.includes('413') || errorMessage.toLowerCase().includes('content too large')) {
|
|
1969
2176
|
setError({
|
|
1970
2177
|
message: 'The context is too large to process. Please start a new conversation or reduce the amount of context.',
|
|
1971
2178
|
code: '413',
|
|
1972
2179
|
});
|
|
2180
|
+
|
|
2181
|
+
// Update history to show error
|
|
2182
|
+
if (lastKey) {
|
|
2183
|
+
setHistory((prev) => ({
|
|
2184
|
+
...prev,
|
|
2185
|
+
[lastKey]: {
|
|
2186
|
+
content: `Error: ${errorMessage}`,
|
|
2187
|
+
callId: lastCallId || '',
|
|
2188
|
+
},
|
|
2189
|
+
}));
|
|
2190
|
+
}
|
|
1973
2191
|
}
|
|
1974
2192
|
// Detect other network errors
|
|
1975
2193
|
else if (errorMessage.toLowerCase().includes('network error') || errorMessage.toLowerCase().includes('fetch')) {
|
|
@@ -1977,6 +2195,17 @@ const AIChatPanel: React.FC<AIChatPanelProps> = ({
|
|
|
1977
2195
|
message: 'Network error. Please check your connection and try again.',
|
|
1978
2196
|
code: 'NETWORK_ERROR',
|
|
1979
2197
|
});
|
|
2198
|
+
|
|
2199
|
+
// Update history to show error
|
|
2200
|
+
if (lastKey) {
|
|
2201
|
+
setHistory((prev) => ({
|
|
2202
|
+
...prev,
|
|
2203
|
+
[lastKey]: {
|
|
2204
|
+
content: `Error: ${errorMessage}`,
|
|
2205
|
+
callId: lastCallId || '',
|
|
2206
|
+
},
|
|
2207
|
+
}));
|
|
2208
|
+
}
|
|
1980
2209
|
}
|
|
1981
2210
|
// Generic error
|
|
1982
2211
|
else {
|
|
@@ -1984,21 +2213,21 @@ const AIChatPanel: React.FC<AIChatPanelProps> = ({
|
|
|
1984
2213
|
message: errorMessage,
|
|
1985
2214
|
code: 'UNKNOWN_ERROR',
|
|
1986
2215
|
});
|
|
2216
|
+
|
|
2217
|
+
// Update history to show error
|
|
2218
|
+
if (lastKey) {
|
|
2219
|
+
setHistory((prev) => ({
|
|
2220
|
+
...prev,
|
|
2221
|
+
[lastKey]: {
|
|
2222
|
+
content: `Error: ${errorMessage}`,
|
|
2223
|
+
callId: lastCallId || '',
|
|
2224
|
+
},
|
|
2225
|
+
}));
|
|
2226
|
+
}
|
|
1987
2227
|
}
|
|
1988
2228
|
|
|
1989
2229
|
// Reset loading state
|
|
1990
2230
|
setIsLoading(false);
|
|
1991
|
-
|
|
1992
|
-
// Update history to show error
|
|
1993
|
-
if (lastKey) {
|
|
1994
|
-
setHistory((prev) => ({
|
|
1995
|
-
...prev,
|
|
1996
|
-
[lastKey]: {
|
|
1997
|
-
content: `Error: ${errorMessage}`,
|
|
1998
|
-
callId: lastCallId || '',
|
|
1999
|
-
},
|
|
2000
|
-
}));
|
|
2001
|
-
}
|
|
2002
2231
|
}
|
|
2003
2232
|
}, [llmError, lastKey, lastCallId]);
|
|
2004
2233
|
|
|
@@ -2219,46 +2448,56 @@ const AIChatPanel: React.FC<AIChatPanelProps> = ({
|
|
|
2219
2448
|
},
|
|
2220
2449
|
}), [CodeBlock, AgentSuggestionCard]);
|
|
2221
2450
|
|
|
2222
|
-
// Render thinking blocks
|
|
2223
|
-
const renderThinkingBlocks = useCallback(() => {
|
|
2224
|
-
|
|
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;
|
|
2225
2455
|
|
|
2226
|
-
|
|
2227
|
-
if (!currentBlock) return null;
|
|
2456
|
+
if (!hasActiveBlock && !hasCompletedBlocks) return null;
|
|
2228
2457
|
|
|
2229
|
-
const
|
|
2230
|
-
|
|
2231
|
-
|
|
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
|
+
};
|
|
2232
2469
|
|
|
2233
2470
|
return (
|
|
2234
|
-
|
|
2235
|
-
|
|
2236
|
-
|
|
2237
|
-
|
|
2238
|
-
|
|
2239
|
-
<
|
|
2240
|
-
|
|
2241
|
-
|
|
2242
|
-
|
|
2243
|
-
|
|
2244
|
-
|
|
2245
|
-
|
|
2246
|
-
|
|
2247
|
-
|
|
2248
|
-
|
|
2249
|
-
|
|
2250
|
-
|
|
2251
|
-
|
|
2252
|
-
|
|
2253
|
-
|
|
2254
|
-
|
|
2255
|
-
|
|
2256
|
-
|
|
2257
|
-
|
|
2258
|
-
|
|
2259
|
-
|
|
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
|
+
</>
|
|
2260
2499
|
);
|
|
2261
|
-
}, [thinkingBlocks,
|
|
2500
|
+
}, [thinkingBlocks, activeThinkingBlock, collapsedBlocks]);
|
|
2262
2501
|
|
|
2263
2502
|
// ============================================================================
|
|
2264
2503
|
// Render
|
|
@@ -2354,43 +2593,55 @@ const AIChatPanel: React.FC<AIChatPanelProps> = ({
|
|
|
2354
2593
|
<div className="ai-chat-message__content">
|
|
2355
2594
|
{/* Streaming state */}
|
|
2356
2595
|
{isLastEntry && (isLoading || !idle) && !justReset ? (
|
|
2357
|
-
|
|
2358
|
-
|
|
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;
|
|
2359
2601
|
|
|
2360
|
-
|
|
2361
|
-
|
|
2362
|
-
|
|
2363
|
-
|
|
2364
|
-
|
|
2365
|
-
|
|
2366
|
-
|
|
2367
|
-
|
|
2368
|
-
{
|
|
2369
|
-
|
|
2370
|
-
|
|
2371
|
-
|
|
2372
|
-
|
|
2373
|
-
|
|
2374
|
-
|
|
2375
|
-
|
|
2376
|
-
|
|
2377
|
-
|
|
2378
|
-
|
|
2379
|
-
|
|
2380
|
-
|
|
2381
|
-
|
|
2382
|
-
|
|
2383
|
-
|
|
2384
|
-
|
|
2385
|
-
|
|
2386
|
-
|
|
2387
|
-
|
|
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
|
+
)}
|
|
2388
2638
|
</div>
|
|
2389
|
-
)
|
|
2390
|
-
|
|
2639
|
+
);
|
|
2640
|
+
})()
|
|
2391
2641
|
) : (
|
|
2392
2642
|
<>
|
|
2393
|
-
{
|
|
2643
|
+
{/* Show completed thinking blocks after streaming */}
|
|
2644
|
+
{isLastEntry && thinkingBlocks.length > 0 && renderThinkingBlocks(false)}
|
|
2394
2645
|
{markdownClass ? (
|
|
2395
2646
|
<div className={markdownClass}>
|
|
2396
2647
|
<ReactMarkdown
|