@hef2024/llmasaservice-ui 0.24.4 → 0.24.6
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 +24 -0
- package/dist/index.d.mts +27 -14
- package/dist/index.d.ts +27 -14
- package/dist/index.js +442 -132
- package/dist/index.mjs +442 -132
- package/index.ts +8 -1
- package/package.json +1 -1
- package/src/AIAgentPanel.tsx +213 -41
- package/src/AIChatPanel.css +14 -0
- package/src/AIChatPanel.tsx +333 -129
- package/src/ChatPanel.css +15 -1
- package/src/ChatPanel.tsx +45 -38
- package/src/components/ui/ThinkingBlock.tsx +25 -1
package/src/AIChatPanel.tsx
CHANGED
|
@@ -66,6 +66,33 @@ export type LocalToolExecutor = (
|
|
|
66
66
|
context: LocalToolExecutorContext,
|
|
67
67
|
) => Promise<unknown> | unknown;
|
|
68
68
|
|
|
69
|
+
export type ArtifactBlockType = 'thinking' | 'reasoning' | 'searching' | 'planning';
|
|
70
|
+
export type PlanningStepStatus = 'todo' | 'doing' | 'done';
|
|
71
|
+
|
|
72
|
+
export interface PlanningStep {
|
|
73
|
+
status: PlanningStepStatus;
|
|
74
|
+
title: string;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export interface ResponseArtifactBlock {
|
|
78
|
+
type: ArtifactBlockType;
|
|
79
|
+
content: string;
|
|
80
|
+
index: number;
|
|
81
|
+
signature: string;
|
|
82
|
+
steps?: PlanningStep[];
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
interface HistoryEntry {
|
|
86
|
+
content: string;
|
|
87
|
+
callId: string;
|
|
88
|
+
toolCalls?: any[];
|
|
89
|
+
toolResponses?: any[];
|
|
90
|
+
artifactBlocks?: ResponseArtifactBlock[];
|
|
91
|
+
planningBlocks?: ResponseArtifactBlock[];
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
export type AIChatHistoryEntry = HistoryEntry;
|
|
95
|
+
|
|
69
96
|
export interface AIChatPanelProps {
|
|
70
97
|
project_id: string;
|
|
71
98
|
initialPrompt?: string;
|
|
@@ -80,7 +107,7 @@ export interface AIChatPanelProps {
|
|
|
80
107
|
theme?: 'light' | 'dark';
|
|
81
108
|
url?: string | null;
|
|
82
109
|
service?: string | null;
|
|
83
|
-
historyChangedCallback?: (history: Record<string,
|
|
110
|
+
historyChangedCallback?: (history: Record<string, AIChatHistoryEntry>) => void;
|
|
84
111
|
responseCompleteCallback?: (callId: string, prompt: string, response: string) => void;
|
|
85
112
|
onLoadingChange?: (isLoading: boolean) => void;
|
|
86
113
|
promptTemplate?: string;
|
|
@@ -99,7 +126,7 @@ export interface AIChatPanelProps {
|
|
|
99
126
|
showPoweredBy?: boolean;
|
|
100
127
|
agent?: string | null;
|
|
101
128
|
conversation?: string | null;
|
|
102
|
-
initialHistory?: Record<string,
|
|
129
|
+
initialHistory?: Record<string, AIChatHistoryEntry>;
|
|
103
130
|
hideRagContextInPrompt?: boolean;
|
|
104
131
|
createConversationOnFirstChat?: boolean;
|
|
105
132
|
autoApproveTools?: boolean | string[];
|
|
@@ -167,19 +194,7 @@ export interface ContextSection {
|
|
|
167
194
|
|
|
168
195
|
export type ContextDataFormat = 'json' | 'toon' | 'markdown' | 'text';
|
|
169
196
|
|
|
170
|
-
|
|
171
|
-
content: string;
|
|
172
|
-
callId: string;
|
|
173
|
-
toolCalls?: any[];
|
|
174
|
-
toolResponses?: any[];
|
|
175
|
-
}
|
|
176
|
-
|
|
177
|
-
interface ThinkingBlock {
|
|
178
|
-
type: 'thinking' | 'reasoning' | 'searching';
|
|
179
|
-
content: string;
|
|
180
|
-
index: number;
|
|
181
|
-
signature: string;
|
|
182
|
-
}
|
|
197
|
+
type ThinkingBlock = ResponseArtifactBlock;
|
|
183
198
|
|
|
184
199
|
interface ToolRequestMatch {
|
|
185
200
|
match: string;
|
|
@@ -215,7 +230,7 @@ interface InlineToolMarker {
|
|
|
215
230
|
}
|
|
216
231
|
|
|
217
232
|
interface InlineThinkingMarker {
|
|
218
|
-
type:
|
|
233
|
+
type: ArtifactBlockType;
|
|
219
234
|
signature: string;
|
|
220
235
|
}
|
|
221
236
|
|
|
@@ -1009,7 +1024,7 @@ const hashInlineMarkerValue = (value: string): string => {
|
|
|
1009
1024
|
};
|
|
1010
1025
|
|
|
1011
1026
|
const getThinkingBlockSignature = (
|
|
1012
|
-
type:
|
|
1027
|
+
type: ArtifactBlockType,
|
|
1013
1028
|
content: string,
|
|
1014
1029
|
): string => {
|
|
1015
1030
|
const normalizedContent = String(content || '').trim();
|
|
@@ -1017,16 +1032,112 @@ const getThinkingBlockSignature = (
|
|
|
1017
1032
|
};
|
|
1018
1033
|
|
|
1019
1034
|
const buildThinkingBlockMarker = (
|
|
1020
|
-
type:
|
|
1035
|
+
type: ArtifactBlockType,
|
|
1021
1036
|
signature: string,
|
|
1022
1037
|
): string => {
|
|
1023
|
-
const normalizedType = String(type || 'thinking').trim() as
|
|
1038
|
+
const normalizedType = String(type || 'thinking').trim() as ArtifactBlockType;
|
|
1024
1039
|
const normalizedSignature = String(signature || '').trim() || `${normalizedType}-block`;
|
|
1025
1040
|
return `${INLINE_THINKING_MARKER_PREFIX}${encodeURIComponent(normalizedType)}|${encodeURIComponent(
|
|
1026
1041
|
normalizedSignature,
|
|
1027
1042
|
)}${INLINE_THINKING_MARKER_SUFFIX}`;
|
|
1028
1043
|
};
|
|
1029
1044
|
|
|
1045
|
+
const PLAN_STEP_REGEX = /^\s*\[plan-step\]\s*([a-zA-Z_-]+)\s*:\s*(.+?)\s*$/i;
|
|
1046
|
+
|
|
1047
|
+
const normalizePlanningStatus = (value: string): PlanningStepStatus => {
|
|
1048
|
+
const normalized = String(value || '').trim().toLowerCase();
|
|
1049
|
+
if (normalized === 'doing' || normalized === 'in_progress' || normalized === 'in-progress') {
|
|
1050
|
+
return 'doing';
|
|
1051
|
+
}
|
|
1052
|
+
if (normalized === 'done' || normalized === 'completed') {
|
|
1053
|
+
return 'done';
|
|
1054
|
+
}
|
|
1055
|
+
return 'todo';
|
|
1056
|
+
};
|
|
1057
|
+
|
|
1058
|
+
const formatPlanningStatus = (status: PlanningStepStatus): string => {
|
|
1059
|
+
if (status === 'doing') return 'Doing';
|
|
1060
|
+
if (status === 'done') return 'Done';
|
|
1061
|
+
return 'Todo';
|
|
1062
|
+
};
|
|
1063
|
+
|
|
1064
|
+
const parsePlanningStep = (line: string): PlanningStep | null => {
|
|
1065
|
+
const match = PLAN_STEP_REGEX.exec(String(line || ''));
|
|
1066
|
+
if (!match) return null;
|
|
1067
|
+
|
|
1068
|
+
const title = String(match[2] || '').trim();
|
|
1069
|
+
if (!title) return null;
|
|
1070
|
+
|
|
1071
|
+
return {
|
|
1072
|
+
status: normalizePlanningStatus(match[1] || ''),
|
|
1073
|
+
title,
|
|
1074
|
+
};
|
|
1075
|
+
};
|
|
1076
|
+
|
|
1077
|
+
const buildPlanningBlockContent = (steps: PlanningStep[]): string => {
|
|
1078
|
+
return steps
|
|
1079
|
+
.filter((step) => !!step && typeof step.title === 'string' && step.title.trim().length > 0)
|
|
1080
|
+
.map((step) => `${formatPlanningStatus(step.status)}: ${step.title.trim()}`)
|
|
1081
|
+
.join('\n');
|
|
1082
|
+
};
|
|
1083
|
+
|
|
1084
|
+
const extractPlanningBlocks = (
|
|
1085
|
+
text: string,
|
|
1086
|
+
): { textWithMarkers: string; planningBlocks: ResponseArtifactBlock[] } => {
|
|
1087
|
+
const source = typeof text === 'string' ? text : '';
|
|
1088
|
+
if (!source) {
|
|
1089
|
+
return {
|
|
1090
|
+
textWithMarkers: '',
|
|
1091
|
+
planningBlocks: [],
|
|
1092
|
+
};
|
|
1093
|
+
}
|
|
1094
|
+
|
|
1095
|
+
const lines = source.split('\n');
|
|
1096
|
+
const planningBlocks: ResponseArtifactBlock[] = [];
|
|
1097
|
+
const rebuiltSegments: string[] = [];
|
|
1098
|
+
let currentOffset = 0;
|
|
1099
|
+
let pendingSteps: PlanningStep[] = [];
|
|
1100
|
+
let pendingIndex = 0;
|
|
1101
|
+
|
|
1102
|
+
const flushPendingPlanning = () => {
|
|
1103
|
+
if (pendingSteps.length === 0) return;
|
|
1104
|
+
|
|
1105
|
+
const signature = `planning-${pendingIndex}`;
|
|
1106
|
+
planningBlocks.push({
|
|
1107
|
+
type: 'planning',
|
|
1108
|
+
content: buildPlanningBlockContent(pendingSteps),
|
|
1109
|
+
index: pendingIndex,
|
|
1110
|
+
signature,
|
|
1111
|
+
steps: pendingSteps,
|
|
1112
|
+
});
|
|
1113
|
+
rebuiltSegments.push(`\n\n${buildThinkingBlockMarker('planning', signature)}\n\n`);
|
|
1114
|
+
pendingSteps = [];
|
|
1115
|
+
};
|
|
1116
|
+
|
|
1117
|
+
lines.forEach((line, index) => {
|
|
1118
|
+
const lineWithNewline = index < lines.length - 1 ? `${line}\n` : line;
|
|
1119
|
+
const planningStep = parsePlanningStep(line);
|
|
1120
|
+
if (planningStep) {
|
|
1121
|
+
if (pendingSteps.length === 0) {
|
|
1122
|
+
pendingIndex = currentOffset;
|
|
1123
|
+
}
|
|
1124
|
+
pendingSteps = [...pendingSteps, planningStep];
|
|
1125
|
+
} else {
|
|
1126
|
+
flushPendingPlanning();
|
|
1127
|
+
rebuiltSegments.push(lineWithNewline);
|
|
1128
|
+
}
|
|
1129
|
+
|
|
1130
|
+
currentOffset += lineWithNewline.length;
|
|
1131
|
+
});
|
|
1132
|
+
|
|
1133
|
+
flushPendingPlanning();
|
|
1134
|
+
|
|
1135
|
+
return {
|
|
1136
|
+
textWithMarkers: rebuiltSegments.join(''),
|
|
1137
|
+
planningBlocks,
|
|
1138
|
+
};
|
|
1139
|
+
};
|
|
1140
|
+
|
|
1030
1141
|
const buildInlineToolMarker = (toolName: string, callId: string): string => {
|
|
1031
1142
|
const normalizedToolName = String(toolName || '').trim() || 'tool';
|
|
1032
1143
|
const normalizedCallId = String(callId || '').trim() || `${normalizedToolName}-call`;
|
|
@@ -1110,11 +1221,13 @@ const parseInlineThinkingMarkers = (
|
|
|
1110
1221
|
}
|
|
1111
1222
|
|
|
1112
1223
|
const normalizedType = String(rawType || '').trim().toLowerCase();
|
|
1113
|
-
const type:
|
|
1224
|
+
const type: ArtifactBlockType =
|
|
1114
1225
|
normalizedType === 'reasoning'
|
|
1115
1226
|
? 'reasoning'
|
|
1116
1227
|
: normalizedType === 'searching'
|
|
1117
1228
|
? 'searching'
|
|
1229
|
+
: normalizedType === 'planning'
|
|
1230
|
+
? 'planning'
|
|
1118
1231
|
: 'thinking';
|
|
1119
1232
|
|
|
1120
1233
|
markers.push({
|
|
@@ -1792,6 +1905,7 @@ const AIChatPanel: React.FC<AIChatPanelProps> = ({
|
|
|
1792
1905
|
const [lastPrompt, setLastPrompt] = useState<string | null>(null);
|
|
1793
1906
|
const [lastKey, setLastKey] = useState<string | null>(null);
|
|
1794
1907
|
const [currentConversation, setCurrentConversation] = useState<string | null>(conversation);
|
|
1908
|
+
const ensuredConversationIdsRef = useRef<Set<string>>(new Set());
|
|
1795
1909
|
const [followOnQuestionsState, setFollowOnQuestionsState] = useState(followOnQuestions);
|
|
1796
1910
|
const [thinkingBlocks, setThinkingBlocks] = useState<ThinkingBlock[]>([]);
|
|
1797
1911
|
const [currentThinkingIndex, setCurrentThinkingIndex] = useState(0);
|
|
@@ -2006,7 +2120,7 @@ const AIChatPanel: React.FC<AIChatPanelProps> = ({
|
|
|
2006
2120
|
}
|
|
2007
2121
|
|
|
2008
2122
|
try {
|
|
2009
|
-
const
|
|
2123
|
+
const settled = await Promise.allSettled(
|
|
2010
2124
|
mcpServers.map(async (server) => {
|
|
2011
2125
|
const resolved = await resolveMcpAuthHeaders({
|
|
2012
2126
|
phase: 'list',
|
|
@@ -2025,6 +2139,23 @@ const AIChatPanel: React.FC<AIChatPanelProps> = ({
|
|
|
2025
2139
|
};
|
|
2026
2140
|
})
|
|
2027
2141
|
);
|
|
2142
|
+
|
|
2143
|
+
const enriched = settled.map((result, index) => {
|
|
2144
|
+
if (result.status === 'fulfilled') {
|
|
2145
|
+
return result.value;
|
|
2146
|
+
}
|
|
2147
|
+
|
|
2148
|
+
const failingUrl =
|
|
2149
|
+
typeof mcpServers?.[index]?.url === 'string'
|
|
2150
|
+
? mcpServers[index].url
|
|
2151
|
+
: `mcp[${index}]`;
|
|
2152
|
+
console.error(
|
|
2153
|
+
`[AIChatPanel] Failed to resolve MCP auth headers for ${failingUrl}:`,
|
|
2154
|
+
result.reason
|
|
2155
|
+
);
|
|
2156
|
+
return mcpServers[index];
|
|
2157
|
+
});
|
|
2158
|
+
|
|
2028
2159
|
if (!cancelled) setResolvedMcpServers(enriched);
|
|
2029
2160
|
} catch (error) {
|
|
2030
2161
|
console.error('[AIChatPanel] Failed to resolve MCP auth headers:', error);
|
|
@@ -2142,10 +2273,26 @@ const AIChatPanel: React.FC<AIChatPanelProps> = ({
|
|
|
2142
2273
|
}));
|
|
2143
2274
|
});
|
|
2144
2275
|
|
|
2145
|
-
const
|
|
2146
|
-
const allTools =
|
|
2276
|
+
const settled = await Promise.allSettled(fetchPromises);
|
|
2277
|
+
const allTools = settled.flatMap((result, index) => {
|
|
2278
|
+
if (result.status === 'fulfilled') {
|
|
2279
|
+
return result.value;
|
|
2280
|
+
}
|
|
2281
|
+
|
|
2282
|
+
const failingUrl =
|
|
2283
|
+
typeof resolvedMcpServers?.[index]?.url === 'string'
|
|
2284
|
+
? resolvedMcpServers[index].url
|
|
2285
|
+
: `mcp[${index}]`;
|
|
2286
|
+
console.error(
|
|
2287
|
+
`[AIChatPanel] Failed to load MCP tools from ${failingUrl}:`,
|
|
2288
|
+
result.reason
|
|
2289
|
+
);
|
|
2290
|
+
return [];
|
|
2291
|
+
});
|
|
2292
|
+
|
|
2293
|
+
const failedCount = settled.filter((result) => result.status === 'rejected').length;
|
|
2147
2294
|
setToolList(allTools);
|
|
2148
|
-
setToolsFetchError(
|
|
2295
|
+
setToolsFetchError(failedCount > 0 && allTools.length === 0);
|
|
2149
2296
|
} catch (error) {
|
|
2150
2297
|
console.error('[AIChatPanel] Failed to load MCP tools:', error);
|
|
2151
2298
|
setToolList([]);
|
|
@@ -2238,28 +2385,36 @@ const AIChatPanel: React.FC<AIChatPanelProps> = ({
|
|
|
2238
2385
|
const ensureConversation = useCallback(() => {
|
|
2239
2386
|
const normalizedConversationId =
|
|
2240
2387
|
typeof currentConversation === 'string' ? currentConversation.trim() : '';
|
|
2388
|
+
const hasConversationHistory = Object.keys(latestHistoryRef.current || {}).length > 0;
|
|
2389
|
+
const shouldInitializeProvidedConversation =
|
|
2390
|
+
normalizedConversationId.length > 10 &&
|
|
2391
|
+
createConversationOnFirstChat &&
|
|
2392
|
+
!hasConversationHistory &&
|
|
2393
|
+
!ensuredConversationIdsRef.current.has(normalizedConversationId);
|
|
2241
2394
|
|
|
2242
2395
|
console.log('ensureConversation - called with:', {
|
|
2243
2396
|
currentConversation: normalizedConversationId || null,
|
|
2244
2397
|
createConversationOnFirstChat,
|
|
2398
|
+
shouldInitializeProvidedConversation,
|
|
2245
2399
|
project_id,
|
|
2246
2400
|
publicAPIUrl,
|
|
2247
2401
|
});
|
|
2248
2402
|
|
|
2249
|
-
// Existing conversation supplied by caller:
|
|
2250
|
-
|
|
2403
|
+
// Existing conversation supplied by caller: initialize its record once if needed,
|
|
2404
|
+
// otherwise use it directly.
|
|
2405
|
+
if (normalizedConversationId && !shouldInitializeProvidedConversation) {
|
|
2251
2406
|
console.log('ensureConversation - using existing conversation:', normalizedConversationId);
|
|
2252
2407
|
return Promise.resolve(normalizedConversationId);
|
|
2253
2408
|
}
|
|
2254
2409
|
|
|
2255
2410
|
if (!createConversationOnFirstChat) {
|
|
2256
|
-
return Promise.resolve('');
|
|
2411
|
+
return Promise.resolve(normalizedConversationId || '');
|
|
2257
2412
|
}
|
|
2258
2413
|
|
|
2259
2414
|
// Guard: Don't create/ensure conversations without a project_id
|
|
2260
2415
|
if (!project_id) {
|
|
2261
2416
|
console.error('ensureConversation - Cannot create conversation without project_id');
|
|
2262
|
-
return Promise.resolve('');
|
|
2417
|
+
return Promise.resolve(normalizedConversationId || '');
|
|
2263
2418
|
}
|
|
2264
2419
|
|
|
2265
2420
|
const createConversation = () => {
|
|
@@ -2272,6 +2427,10 @@ const AIChatPanel: React.FC<AIChatPanelProps> = ({
|
|
|
2272
2427
|
language: browserInfo?.userLanguage,
|
|
2273
2428
|
};
|
|
2274
2429
|
|
|
2430
|
+
if (shouldInitializeProvidedConversation) {
|
|
2431
|
+
requestBody.conversationId = normalizedConversationId;
|
|
2432
|
+
}
|
|
2433
|
+
|
|
2275
2434
|
console.log('ensureConversation - Creating conversation with:', requestBody);
|
|
2276
2435
|
console.log('ensureConversation - API URL:', `${publicAPIUrl}/conversations`);
|
|
2277
2436
|
|
|
@@ -2299,21 +2458,40 @@ const AIChatPanel: React.FC<AIChatPanelProps> = ({
|
|
|
2299
2458
|
(typeof newConvo?.conversation_id === 'string' && newConvo.conversation_id.trim()) ||
|
|
2300
2459
|
(typeof newConvo?.conversation?.id === 'string' && newConvo.conversation.id.trim()) ||
|
|
2301
2460
|
'';
|
|
2461
|
+
const resolvedConversationId = normalizedConversationId || createdId;
|
|
2302
2462
|
|
|
2303
2463
|
if (createdId) {
|
|
2304
2464
|
console.log('ensureConversation - New conversation ID:', createdId);
|
|
2465
|
+
}
|
|
2466
|
+
|
|
2467
|
+
if (resolvedConversationId) {
|
|
2468
|
+
ensuredConversationIdsRef.current.add(resolvedConversationId);
|
|
2469
|
+
}
|
|
2470
|
+
|
|
2471
|
+
if (normalizedConversationId && createdId && createdId !== normalizedConversationId) {
|
|
2472
|
+
console.warn(
|
|
2473
|
+
'ensureConversation - API returned a different ID than supplied conversationId; keeping caller-provided ID',
|
|
2474
|
+
{ suppliedConversationId: normalizedConversationId, returnedConversationId: createdId }
|
|
2475
|
+
);
|
|
2476
|
+
}
|
|
2477
|
+
|
|
2478
|
+
if (!normalizedConversationId && createdId) {
|
|
2305
2479
|
setCurrentConversation(createdId);
|
|
2306
2480
|
// NOTE: Don't call onConversationCreated here - it causes a re-render
|
|
2307
2481
|
// before send() is called. The caller should notify after send() starts.
|
|
2308
2482
|
return createdId;
|
|
2309
2483
|
}
|
|
2310
2484
|
|
|
2485
|
+
if (resolvedConversationId) {
|
|
2486
|
+
return resolvedConversationId;
|
|
2487
|
+
}
|
|
2488
|
+
|
|
2311
2489
|
console.warn('ensureConversation - No ID in response');
|
|
2312
|
-
return '';
|
|
2490
|
+
return normalizedConversationId || '';
|
|
2313
2491
|
})
|
|
2314
2492
|
.catch((error) => {
|
|
2315
2493
|
console.error('Error creating new conversation', error);
|
|
2316
|
-
return '';
|
|
2494
|
+
return normalizedConversationId || '';
|
|
2317
2495
|
});
|
|
2318
2496
|
};
|
|
2319
2497
|
|
|
@@ -3267,7 +3445,7 @@ const AIChatPanel: React.FC<AIChatPanelProps> = ({
|
|
|
3267
3445
|
return cleaned || 'Thinking';
|
|
3268
3446
|
}, []);
|
|
3269
3447
|
|
|
3270
|
-
// Process
|
|
3448
|
+
// Process internal response artifacts from the model before rendering transcript text.
|
|
3271
3449
|
const processThinkingTags = useCallback((text: string): {
|
|
3272
3450
|
cleanedText: string;
|
|
3273
3451
|
completedBlocks: ThinkingBlock[];
|
|
@@ -3283,7 +3461,7 @@ const AIChatPanel: React.FC<AIChatPanelProps> = ({
|
|
|
3283
3461
|
};
|
|
3284
3462
|
}
|
|
3285
3463
|
|
|
3286
|
-
// Remove zero-width space characters from keepalive before processing
|
|
3464
|
+
// Remove zero-width space characters from keepalive before processing.
|
|
3287
3465
|
const processedText = text.replace(/\u200B/g, '');
|
|
3288
3466
|
|
|
3289
3467
|
const completedBlocks: ThinkingBlock[] = [];
|
|
@@ -3291,7 +3469,7 @@ const AIChatPanel: React.FC<AIChatPanelProps> = ({
|
|
|
3291
3469
|
/<(thinking|reasoning|searching)>([\s\S]*?)<\/\1>/gi,
|
|
3292
3470
|
(_fullMatch, rawType, rawContent, offset) => {
|
|
3293
3471
|
const normalizedType = String(rawType || '').trim().toLowerCase();
|
|
3294
|
-
const type:
|
|
3472
|
+
const type: ArtifactBlockType =
|
|
3295
3473
|
normalizedType === 'reasoning'
|
|
3296
3474
|
? 'reasoning'
|
|
3297
3475
|
: normalizedType === 'searching'
|
|
@@ -3311,6 +3489,13 @@ const AIChatPanel: React.FC<AIChatPanelProps> = ({
|
|
|
3311
3489
|
return `\n\n${buildThinkingBlockMarker(type, signature)}\n\n`;
|
|
3312
3490
|
},
|
|
3313
3491
|
);
|
|
3492
|
+
const {
|
|
3493
|
+
textWithMarkers: textWithArtifactMarkers,
|
|
3494
|
+
planningBlocks,
|
|
3495
|
+
} = extractPlanningBlocks(textWithCompleteMarkers);
|
|
3496
|
+
if (planningBlocks.length > 0) {
|
|
3497
|
+
completedBlocks.push(...planningBlocks);
|
|
3498
|
+
}
|
|
3314
3499
|
|
|
3315
3500
|
// Check for incomplete (streaming) tags at the end of the text
|
|
3316
3501
|
let activeBlock: { type: 'thinking' | 'reasoning' | 'searching'; content: string } | null = null;
|
|
@@ -3353,8 +3538,8 @@ const AIChatPanel: React.FC<AIChatPanelProps> = ({
|
|
|
3353
3538
|
}
|
|
3354
3539
|
}
|
|
3355
3540
|
|
|
3356
|
-
// Clean the text by removing all thinking-related tags (complete and incomplete)
|
|
3357
|
-
let cleanedText =
|
|
3541
|
+
// Clean the text by removing all thinking-related tags (complete and incomplete).
|
|
3542
|
+
let cleanedText = textWithArtifactMarkers
|
|
3358
3543
|
// Also remove partial opening tags
|
|
3359
3544
|
.replace(/<think(?:i(?:n(?:g)?)?)?$/i, '')
|
|
3360
3545
|
.replace(/<reas(?:o(?:n(?:i(?:n(?:g)?)?)?)?)?$/i, '')
|
|
@@ -3395,6 +3580,16 @@ const AIChatPanel: React.FC<AIChatPanelProps> = ({
|
|
|
3395
3580
|
return incoming;
|
|
3396
3581
|
}
|
|
3397
3582
|
|
|
3583
|
+
const incomingSignaturePrefix =
|
|
3584
|
+
incoming.length >= existing.length &&
|
|
3585
|
+
existing.every((block, index) => {
|
|
3586
|
+
const next = incoming[index];
|
|
3587
|
+
return !!next && next.type === block.type && next.signature === block.signature;
|
|
3588
|
+
});
|
|
3589
|
+
if (incomingSignaturePrefix) {
|
|
3590
|
+
return incoming;
|
|
3591
|
+
}
|
|
3592
|
+
|
|
3398
3593
|
// For follow-on streams (e.g., tool continuations), append only unseen blocks.
|
|
3399
3594
|
const merged = [...existing];
|
|
3400
3595
|
const seen = new Set(existing.map((block) => block.signature || `${block.type}::${block.content}`));
|
|
@@ -3705,7 +3900,6 @@ const AIChatPanel: React.FC<AIChatPanelProps> = ({
|
|
|
3705
3900
|
|
|
3706
3901
|
// Clear any previous errors
|
|
3707
3902
|
setError(null);
|
|
3708
|
-
lastProcessedErrorRef.current = null; // Allow new errors to be processed
|
|
3709
3903
|
|
|
3710
3904
|
// Reset scroll tracking for new message - enable auto-scroll
|
|
3711
3905
|
setUserHasScrolled(false);
|
|
@@ -4100,6 +4294,7 @@ const AIChatPanel: React.FC<AIChatPanelProps> = ({
|
|
|
4100
4294
|
|
|
4101
4295
|
// Keep reasoning blocks visible; users can collapse manually if needed.
|
|
4102
4296
|
// Auto-collapse here made blocks appear and then seem to disappear.
|
|
4297
|
+
const planningBlocks = mergedBlocks.filter((block) => block.type === 'planning');
|
|
4103
4298
|
|
|
4104
4299
|
// Update history state with RAW content (actions applied at render time)
|
|
4105
4300
|
setHistory((prev) => {
|
|
@@ -4121,6 +4316,8 @@ const AIChatPanel: React.FC<AIChatPanelProps> = ({
|
|
|
4121
4316
|
...existingEntry,
|
|
4122
4317
|
content: nextContent, // Store raw content without tool JSON or thinking tags
|
|
4123
4318
|
callId: lastCallId || existingEntry.callId || '',
|
|
4319
|
+
artifactBlocks: mergedBlocks,
|
|
4320
|
+
planningBlocks,
|
|
4124
4321
|
};
|
|
4125
4322
|
// Keep ref in sync for callbacks (this doesn't trigger re-renders)
|
|
4126
4323
|
latestHistoryRef.current = newHistory;
|
|
@@ -4346,106 +4543,113 @@ const AIChatPanel: React.FC<AIChatPanelProps> = ({
|
|
|
4346
4543
|
}
|
|
4347
4544
|
}, [followOnPrompt, continueChat]);
|
|
4348
4545
|
|
|
4349
|
-
// Monitor for errors from useLLM hook
|
|
4546
|
+
// Monitor for new errors from useLLM hook without replaying stale errors on new prompts.
|
|
4350
4547
|
useEffect(() => {
|
|
4351
|
-
|
|
4352
|
-
|
|
4353
|
-
|
|
4354
|
-
|
|
4355
|
-
|
|
4356
|
-
|
|
4357
|
-
|
|
4358
|
-
|
|
4359
|
-
|
|
4360
|
-
|
|
4361
|
-
|
|
4362
|
-
|
|
4363
|
-
|
|
4364
|
-
|
|
4365
|
-
|
|
4366
|
-
|
|
4367
|
-
|
|
4368
|
-
|
|
4369
|
-
|
|
4370
|
-
|
|
4371
|
-
|
|
4372
|
-
|
|
4373
|
-
|
|
4374
|
-
|
|
4375
|
-
|
|
4376
|
-
|
|
4377
|
-
|
|
4378
|
-
//
|
|
4379
|
-
|
|
4380
|
-
|
|
4381
|
-
|
|
4382
|
-
|
|
4548
|
+
const normalizedError = typeof llmError === 'string' ? llmError.trim() : '';
|
|
4549
|
+
|
|
4550
|
+
if (!normalizedError) {
|
|
4551
|
+
lastProcessedErrorRef.current = null;
|
|
4552
|
+
return;
|
|
4553
|
+
}
|
|
4554
|
+
|
|
4555
|
+
// Skip if we've already processed this exact error value from the hook.
|
|
4556
|
+
if (lastProcessedErrorRef.current === normalizedError) {
|
|
4557
|
+
console.log('[AIChatPanel] Skipping duplicate error:', normalizedError);
|
|
4558
|
+
return;
|
|
4559
|
+
}
|
|
4560
|
+
|
|
4561
|
+
console.log('[AIChatPanel] Error detected:', normalizedError);
|
|
4562
|
+
lastProcessedErrorRef.current = normalizedError;
|
|
4563
|
+
|
|
4564
|
+
const errorMessage = normalizedError;
|
|
4565
|
+
const currentLastKey = lastKeyRef.current;
|
|
4566
|
+
const currentLastCallId = lastCallIdRef.current;
|
|
4567
|
+
|
|
4568
|
+
// Check if this is a user-initiated abort
|
|
4569
|
+
const isAbortError =
|
|
4570
|
+
errorMessage.toLowerCase().includes('abort') ||
|
|
4571
|
+
errorMessage.toLowerCase().includes('canceled') ||
|
|
4572
|
+
errorMessage.toLowerCase().includes('cancelled');
|
|
4573
|
+
|
|
4574
|
+
if (isAbortError) {
|
|
4575
|
+
// User canceled the request - don't show error banner
|
|
4576
|
+
console.log('[AIChatPanel] Request was aborted by user (useEffect)');
|
|
4577
|
+
// Don't set error state - no red banner
|
|
4578
|
+
|
|
4579
|
+
// Don't update history here - the error callback in send() already handled it
|
|
4580
|
+
// with the correct promptKey. Updating here with lastKey can affect the wrong entry
|
|
4581
|
+
// if the user has already submitted a new prompt.
|
|
4582
|
+
}
|
|
4583
|
+
// Detect 413 Content Too Large error
|
|
4584
|
+
else if (errorMessage.includes('413') || errorMessage.toLowerCase().includes('content too large')) {
|
|
4585
|
+
setError({
|
|
4586
|
+
message: 'The context is too large to process. Please start a new conversation or reduce the amount of context.',
|
|
4587
|
+
code: '413',
|
|
4588
|
+
});
|
|
4589
|
+
|
|
4590
|
+
// Update history to show error
|
|
4591
|
+
if (currentLastKey) {
|
|
4592
|
+
setHistory((prev) => {
|
|
4593
|
+
const existingEntry = prev[currentLastKey] || { content: '', callId: '' };
|
|
4594
|
+
return {
|
|
4595
|
+
...prev,
|
|
4596
|
+
[currentLastKey]: {
|
|
4597
|
+
...existingEntry,
|
|
4598
|
+
content: `Error: ${errorMessage}`,
|
|
4599
|
+
callId: currentLastCallId || existingEntry.callId || '',
|
|
4600
|
+
},
|
|
4601
|
+
};
|
|
4383
4602
|
});
|
|
4384
|
-
|
|
4385
|
-
// Update history to show error
|
|
4386
|
-
if (lastKey) {
|
|
4387
|
-
setHistory((prev) => {
|
|
4388
|
-
const existingEntry = prev[lastKey] || { content: '', callId: '' };
|
|
4389
|
-
return {
|
|
4390
|
-
...prev,
|
|
4391
|
-
[lastKey]: {
|
|
4392
|
-
...existingEntry,
|
|
4393
|
-
content: `Error: ${errorMessage}`,
|
|
4394
|
-
callId: lastCallId || existingEntry.callId || '',
|
|
4395
|
-
},
|
|
4396
|
-
};
|
|
4397
|
-
});
|
|
4398
|
-
}
|
|
4399
4603
|
}
|
|
4400
|
-
|
|
4401
|
-
|
|
4402
|
-
|
|
4403
|
-
|
|
4404
|
-
|
|
4604
|
+
}
|
|
4605
|
+
// Detect other network errors
|
|
4606
|
+
else if (errorMessage.toLowerCase().includes('network error') || errorMessage.toLowerCase().includes('fetch')) {
|
|
4607
|
+
setError({
|
|
4608
|
+
message: 'Network error. Please check your connection and try again.',
|
|
4609
|
+
code: 'NETWORK_ERROR',
|
|
4610
|
+
});
|
|
4611
|
+
|
|
4612
|
+
// Update history to show error
|
|
4613
|
+
if (currentLastKey) {
|
|
4614
|
+
setHistory((prev) => {
|
|
4615
|
+
const existingEntry = prev[currentLastKey] || { content: '', callId: '' };
|
|
4616
|
+
return {
|
|
4617
|
+
...prev,
|
|
4618
|
+
[currentLastKey]: {
|
|
4619
|
+
...existingEntry,
|
|
4620
|
+
content: `Error: ${errorMessage}`,
|
|
4621
|
+
callId: currentLastCallId || existingEntry.callId || '',
|
|
4622
|
+
},
|
|
4623
|
+
};
|
|
4405
4624
|
});
|
|
4406
|
-
|
|
4407
|
-
// Update history to show error
|
|
4408
|
-
if (lastKey) {
|
|
4409
|
-
setHistory((prev) => {
|
|
4410
|
-
const existingEntry = prev[lastKey] || { content: '', callId: '' };
|
|
4411
|
-
return {
|
|
4412
|
-
...prev,
|
|
4413
|
-
[lastKey]: {
|
|
4414
|
-
...existingEntry,
|
|
4415
|
-
content: `Error: ${errorMessage}`,
|
|
4416
|
-
callId: lastCallId || existingEntry.callId || '',
|
|
4417
|
-
},
|
|
4418
|
-
};
|
|
4419
|
-
});
|
|
4420
|
-
}
|
|
4421
4625
|
}
|
|
4422
|
-
|
|
4423
|
-
|
|
4424
|
-
|
|
4425
|
-
|
|
4426
|
-
|
|
4626
|
+
}
|
|
4627
|
+
// Generic error
|
|
4628
|
+
else {
|
|
4629
|
+
setError({
|
|
4630
|
+
message: errorMessage,
|
|
4631
|
+
code: 'UNKNOWN_ERROR',
|
|
4632
|
+
});
|
|
4633
|
+
|
|
4634
|
+
// Update history to show error
|
|
4635
|
+
if (currentLastKey) {
|
|
4636
|
+
setHistory((prev) => {
|
|
4637
|
+
const existingEntry = prev[currentLastKey] || { content: '', callId: '' };
|
|
4638
|
+
return {
|
|
4639
|
+
...prev,
|
|
4640
|
+
[currentLastKey]: {
|
|
4641
|
+
...existingEntry,
|
|
4642
|
+
content: `Error: ${errorMessage}`,
|
|
4643
|
+
callId: currentLastCallId || existingEntry.callId || '',
|
|
4644
|
+
},
|
|
4645
|
+
};
|
|
4427
4646
|
});
|
|
4428
|
-
|
|
4429
|
-
// Update history to show error
|
|
4430
|
-
if (lastKey) {
|
|
4431
|
-
setHistory((prev) => {
|
|
4432
|
-
const existingEntry = prev[lastKey] || { content: '', callId: '' };
|
|
4433
|
-
return {
|
|
4434
|
-
...prev,
|
|
4435
|
-
[lastKey]: {
|
|
4436
|
-
...existingEntry,
|
|
4437
|
-
content: `Error: ${errorMessage}`,
|
|
4438
|
-
callId: lastCallId || existingEntry.callId || '',
|
|
4439
|
-
},
|
|
4440
|
-
};
|
|
4441
|
-
});
|
|
4442
|
-
}
|
|
4443
4647
|
}
|
|
4444
|
-
|
|
4445
|
-
// Reset loading state
|
|
4446
|
-
setIsLoading(false);
|
|
4447
4648
|
}
|
|
4448
|
-
|
|
4649
|
+
|
|
4650
|
+
// Reset loading state
|
|
4651
|
+
setIsLoading(false);
|
|
4652
|
+
}, [llmError]);
|
|
4449
4653
|
|
|
4450
4654
|
// Dynamic CSS Injection
|
|
4451
4655
|
useEffect(() => {
|
|
@@ -4679,7 +4883,7 @@ const AIChatPanel: React.FC<AIChatPanelProps> = ({
|
|
|
4679
4883
|
|
|
4680
4884
|
const renderThinkingBlockCard = (
|
|
4681
4885
|
entryKey: string,
|
|
4682
|
-
block: { type:
|
|
4886
|
+
block: { type: ArtifactBlockType; content: string },
|
|
4683
4887
|
blockKey: string,
|
|
4684
4888
|
renderKey: string,
|
|
4685
4889
|
isStreaming: boolean,
|