@jazzmind/busibox-app 3.0.38 → 3.0.39
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/components/chat/ChatContainer.d.ts.map +1 -1
- package/dist/components/chat/ChatContainer.js +122 -602
- package/dist/components/chat/ChatContainer.js.map +1 -1
- package/dist/components/chat/ChatInterface.d.ts +0 -1
- package/dist/components/chat/ChatInterface.d.ts.map +1 -1
- package/dist/components/chat/ChatInterface.js +159 -302
- package/dist/components/chat/ChatInterface.js.map +1 -1
- package/dist/components/chat/MessageList.d.ts +2 -1
- package/dist/components/chat/MessageList.d.ts.map +1 -1
- package/dist/components/chat/MessageList.js +33 -17
- package/dist/components/chat/MessageList.js.map +1 -1
- package/dist/components/chat/ThinkingToggle.d.ts.map +1 -1
- package/dist/components/chat/ThinkingToggle.js +15 -1
- package/dist/components/chat/ThinkingToggle.js.map +1 -1
- package/dist/lib/agent/agent-api-base.d.ts.map +1 -1
- package/dist/lib/agent/agent-api-base.js +0 -1
- package/dist/lib/agent/agent-api-base.js.map +1 -1
- package/dist/lib/agent/chat-client.d.ts.map +1 -1
- package/dist/lib/agent/chat-client.js +0 -1
- package/dist/lib/agent/chat-client.js.map +1 -1
- package/dist/lib/agent/index.d.ts +2 -0
- package/dist/lib/agent/index.d.ts.map +1 -1
- package/dist/lib/agent/index.js +2 -0
- package/dist/lib/agent/index.js.map +1 -1
- package/dist/lib/agent/stream-event-processor.d.ts +37 -0
- package/dist/lib/agent/stream-event-processor.d.ts.map +1 -0
- package/dist/lib/agent/stream-event-processor.js +204 -0
- package/dist/lib/agent/stream-event-processor.js.map +1 -0
- package/dist/lib/hooks/useChatStream.d.ts.map +1 -1
- package/dist/lib/hooks/useChatStream.js +27 -173
- package/dist/lib/hooks/useChatStream.js.map +1 -1
- package/package.json +1 -1
|
@@ -3,8 +3,9 @@ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-run
|
|
|
3
3
|
/**
|
|
4
4
|
* Chat Container - Client Component
|
|
5
5
|
*
|
|
6
|
-
*
|
|
7
|
-
* all
|
|
6
|
+
* Multi-conversation chat UI with sidebar, insights panel, and agent selection.
|
|
7
|
+
* Delegates all SSE streaming to the shared useChatStream hook so event handling
|
|
8
|
+
* stays consistent with ChatInterface and SimpleChatInterface.
|
|
8
9
|
*/
|
|
9
10
|
import { useState, useCallback, useMemo, useEffect, useRef } from 'react';
|
|
10
11
|
import { Plus, MessageSquare, Trash2, Search, Brain, X, Sparkles, Bot, ListTodo } from 'lucide-react';
|
|
@@ -14,9 +15,9 @@ import remarkGfm from 'remark-gfm';
|
|
|
14
15
|
import { MessageList } from './MessageList';
|
|
15
16
|
import { MessageInput } from './MessageInput';
|
|
16
17
|
import { AgentSelectionPanel } from './AgentSelectionPanel';
|
|
17
|
-
import { StreamingToolCard } from './StreamingToolCard';
|
|
18
18
|
import { InsightEditModal } from './InsightEditModal';
|
|
19
19
|
import { stripThinkTags } from './chat-utils';
|
|
20
|
+
import { useChatStream } from '../../lib/hooks/useChatStream';
|
|
20
21
|
import { useIsMobile } from '../../lib/hooks/useIsMobile';
|
|
21
22
|
import { useCrossAppApiPath } from '../../contexts/ApiContext';
|
|
22
23
|
/**
|
|
@@ -90,26 +91,21 @@ export function ChatContainer({ initialConversations, initialMessages, initialCo
|
|
|
90
91
|
}
|
|
91
92
|
window.history.replaceState({}, '', url.toString());
|
|
92
93
|
}, [conversationQueryParam]);
|
|
93
|
-
//
|
|
94
|
-
const [isStreaming, setIsStreaming] = useState(false);
|
|
95
|
-
const [streamingContent, setStreamingContent] = useState('');
|
|
96
|
-
const [streamingAgentName, setStreamingAgentName] = useState(undefined);
|
|
97
|
-
const [thoughts, setThoughts] = useState([]);
|
|
94
|
+
// Background stream tracking for non-active conversations
|
|
98
95
|
const [streamingConvIds, setStreamingConvIds] = useState(new Set());
|
|
99
96
|
const [quickReplies, setQuickReplies] = useState([]);
|
|
100
97
|
const [promptActive, setPromptActive] = useState(false);
|
|
101
|
-
const
|
|
102
|
-
const streamMapRef = useRef(new Map());
|
|
98
|
+
const backgroundStreamRef = useRef(new Map());
|
|
103
99
|
const currentConversationRef = useRef(initialConversation?.id ?? null);
|
|
104
100
|
useEffect(() => {
|
|
105
101
|
currentConversationRef.current = currentConversation?.id ?? null;
|
|
106
102
|
}, [currentConversation]);
|
|
107
103
|
useEffect(() => {
|
|
108
104
|
return () => {
|
|
109
|
-
for (const streamState of
|
|
105
|
+
for (const streamState of backgroundStreamRef.current.values()) {
|
|
110
106
|
streamState.controller.abort();
|
|
111
107
|
}
|
|
112
|
-
|
|
108
|
+
backgroundStreamRef.current.clear();
|
|
113
109
|
};
|
|
114
110
|
}, []);
|
|
115
111
|
const setConversationStreamingStatus = useCallback((conversationId, active) => {
|
|
@@ -127,31 +123,58 @@ export function ChatContainer({ initialConversations, initialMessages, initialCo
|
|
|
127
123
|
const isConversationActive = useCallback((conversationId) => {
|
|
128
124
|
return currentConversationRef.current === conversationId;
|
|
129
125
|
}, []);
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
126
|
+
// Resolve the agent API URL for the useChatStream hook.
|
|
127
|
+
// On the portal, this resolves to the cross-app proxy path.
|
|
128
|
+
const agentUrl = useMemo(() => {
|
|
129
|
+
return resolve('agent', '/api/agent');
|
|
130
|
+
}, [resolve]);
|
|
131
|
+
// Set up the shared streaming hook
|
|
132
|
+
const { state: streamState, sendMessage: hookSendMessage, cancel: hookCancel } = useChatStream({
|
|
133
|
+
token: '', // Portal uses cookie auth through the proxy, no bearer token needed
|
|
134
|
+
agentUrl,
|
|
135
|
+
onConversationCreated: (id, title) => {
|
|
136
|
+
if (title) {
|
|
137
|
+
setConversations(prev => {
|
|
138
|
+
const existing = prev.find(c => c.id === id);
|
|
139
|
+
if (existing) {
|
|
140
|
+
return prev.map(c => c.id === id ? { ...c, title } : c);
|
|
141
|
+
}
|
|
142
|
+
const newConv = {
|
|
143
|
+
id,
|
|
144
|
+
userId: '',
|
|
145
|
+
title,
|
|
146
|
+
source,
|
|
147
|
+
createdAt: new Date(),
|
|
148
|
+
updatedAt: new Date(),
|
|
149
|
+
messageCount: 0,
|
|
150
|
+
};
|
|
151
|
+
return [newConv, ...prev];
|
|
152
|
+
});
|
|
153
|
+
}
|
|
154
|
+
currentConversationRef.current = id;
|
|
155
|
+
setCurrentConversation(prev => ({
|
|
156
|
+
id,
|
|
157
|
+
userId: prev?.userId || '',
|
|
158
|
+
title: title || prev?.title || 'New Conversation',
|
|
159
|
+
source: prev?.source ?? source,
|
|
160
|
+
createdAt: prev?.createdAt || new Date(),
|
|
161
|
+
updatedAt: new Date(),
|
|
162
|
+
messageCount: prev?.messageCount ?? 0,
|
|
163
|
+
model: prev?.model,
|
|
164
|
+
metadata: prev?.metadata,
|
|
165
|
+
}));
|
|
166
|
+
updateUrlWithConversation(id);
|
|
167
|
+
},
|
|
168
|
+
onTitleUpdate: (id, title) => {
|
|
169
|
+
setCurrentConversation(prev => prev && prev.id === id ? { ...prev, title } : prev);
|
|
170
|
+
setConversations(prev => prev.map(c => c.id === id ? { ...c, title } : c));
|
|
171
|
+
},
|
|
172
|
+
});
|
|
173
|
+
// Memoize default agent selection
|
|
149
174
|
const defaultAgents = useMemo(() => {
|
|
150
|
-
// Try to find by various possible names
|
|
151
175
|
const chatAgent = availableAgents.find(a => (a.name === 'chat' || a.name === 'chat-agent' || a.name === 'chat_agent') && a.is_active);
|
|
152
176
|
return chatAgent ? [chatAgent.id] : [];
|
|
153
177
|
}, [availableAgents]);
|
|
154
|
-
// Selection state - default to only "chat" agent (stores agent IDs)
|
|
155
178
|
const [selectedAgents, setSelectedAgents] = useState(defaultAgents);
|
|
156
179
|
// Insights state
|
|
157
180
|
const [showInsightsPanel, setShowInsightsPanel] = useState(false);
|
|
@@ -186,10 +209,7 @@ export function ChatContainer({ initialConversations, initialMessages, initialCo
|
|
|
186
209
|
currentConversationRef.current = conversation.id;
|
|
187
210
|
setCurrentConversation(conversation);
|
|
188
211
|
updateUrlWithConversation(conversation.id);
|
|
189
|
-
applyStreamStateForConversation(conversation.id);
|
|
190
212
|
setQuickReplies([]);
|
|
191
|
-
setThoughts([]);
|
|
192
|
-
setInsights([]);
|
|
193
213
|
if (isMobile) {
|
|
194
214
|
setMobileSidebarOpen(false);
|
|
195
215
|
}
|
|
@@ -200,7 +220,6 @@ export function ChatContainer({ initialConversations, initialMessages, initialCo
|
|
|
200
220
|
const mappedMessages = (data.messages || []).map(mapMessage);
|
|
201
221
|
if (currentConversationRef.current === conversation.id) {
|
|
202
222
|
setMessages(mappedMessages);
|
|
203
|
-
applyStreamStateForConversation(conversation.id);
|
|
204
223
|
}
|
|
205
224
|
}
|
|
206
225
|
catch (error) {
|
|
@@ -212,9 +231,7 @@ export function ChatContainer({ initialConversations, initialMessages, initialCo
|
|
|
212
231
|
setIsLoadingMessages(false);
|
|
213
232
|
}
|
|
214
233
|
}
|
|
215
|
-
}, [apiCall, isMobile, updateUrlWithConversation
|
|
216
|
-
/** Create a new conversation and return its ID, or null on failure.
|
|
217
|
-
* Shared by the "New Chat" button, send-message auto-create, and attachment auto-create. */
|
|
234
|
+
}, [apiCall, isMobile, updateUrlWithConversation]);
|
|
218
235
|
const ensureConversation = useCallback(async () => {
|
|
219
236
|
if (currentConversation?.id)
|
|
220
237
|
return currentConversation.id;
|
|
@@ -248,7 +265,6 @@ export function ChatContainer({ initialConversations, initialMessages, initialCo
|
|
|
248
265
|
setConversations(prev => [newConv, ...prev]);
|
|
249
266
|
setCurrentConversation(newConv);
|
|
250
267
|
setMessages([]);
|
|
251
|
-
applyStreamStateForConversation(newConv.id);
|
|
252
268
|
updateUrlWithConversation(newConv.id);
|
|
253
269
|
if (isMobile) {
|
|
254
270
|
setMobileSidebarOpen(false);
|
|
@@ -258,15 +274,15 @@ export function ChatContainer({ initialConversations, initialMessages, initialCo
|
|
|
258
274
|
console.error('Failed to create conversation:', error);
|
|
259
275
|
toast.error('Failed to create conversation');
|
|
260
276
|
}
|
|
261
|
-
}, [apiCall, source, isMobile, updateUrlWithConversation
|
|
277
|
+
}, [apiCall, source, isMobile, updateUrlWithConversation]);
|
|
262
278
|
const handleDeleteConversation = useCallback(async (conversationId) => {
|
|
263
279
|
if (!confirm('Are you sure you want to delete this conversation?'))
|
|
264
280
|
return;
|
|
265
281
|
try {
|
|
266
|
-
const
|
|
267
|
-
if (
|
|
268
|
-
|
|
269
|
-
|
|
282
|
+
const bgStream = backgroundStreamRef.current.get(conversationId);
|
|
283
|
+
if (bgStream) {
|
|
284
|
+
bgStream.controller.abort();
|
|
285
|
+
backgroundStreamRef.current.delete(conversationId);
|
|
270
286
|
setConversationStreamingStatus(conversationId, false);
|
|
271
287
|
}
|
|
272
288
|
await apiCall(`/conversations/${conversationId}`, { method: 'DELETE' });
|
|
@@ -284,17 +300,16 @@ export function ChatContainer({ initialConversations, initialMessages, initialCo
|
|
|
284
300
|
toast.error('Failed to delete conversation');
|
|
285
301
|
}
|
|
286
302
|
}, [apiCall, currentConversation, updateUrlWithConversation, setConversationStreamingStatus]);
|
|
303
|
+
// ── Send message using useChatStream hook ──────────────────────────────
|
|
287
304
|
const handleSendMessage = useCallback(async (content, attachmentIds, attachmentMeta) => {
|
|
288
305
|
if (!content.trim())
|
|
289
306
|
return;
|
|
290
307
|
setQuickReplies([]);
|
|
291
308
|
setPromptActive(false);
|
|
292
|
-
// Create conversation if none exists
|
|
293
309
|
const convId = await ensureConversation();
|
|
294
310
|
if (!convId)
|
|
295
311
|
return;
|
|
296
|
-
|
|
297
|
-
let tempUserMessage = {
|
|
312
|
+
const tempUserMessage = {
|
|
298
313
|
id: `temp-${Date.now()}`,
|
|
299
314
|
conversationId: convId,
|
|
300
315
|
role: 'user',
|
|
@@ -302,537 +317,74 @@ export function ChatContainer({ initialConversations, initialMessages, initialCo
|
|
|
302
317
|
attachments: attachmentMeta,
|
|
303
318
|
createdAt: new Date(),
|
|
304
319
|
};
|
|
305
|
-
|
|
306
|
-
setMessages(prev => [...prev, tempUserMessage]);
|
|
307
|
-
}
|
|
308
|
-
const controller = new AbortController();
|
|
309
|
-
let streamConversationId = convId;
|
|
310
|
-
streamMapRef.current.set(streamConversationId, {
|
|
311
|
-
controller,
|
|
312
|
-
content: '',
|
|
313
|
-
thoughts: [],
|
|
314
|
-
parts: [],
|
|
315
|
-
tempUserMessage,
|
|
316
|
-
hasAddedMessage: false,
|
|
317
|
-
messages: [tempUserMessage],
|
|
318
|
-
});
|
|
319
|
-
setConversationStreamingStatus(streamConversationId, true);
|
|
320
|
-
if (isConversationActive(streamConversationId)) {
|
|
321
|
-
setIsStreaming(true);
|
|
322
|
-
setStreamingContent('');
|
|
323
|
-
setStreamingAgentName(undefined);
|
|
324
|
-
setThoughts([]);
|
|
325
|
-
setStreamingParts([]);
|
|
326
|
-
}
|
|
327
|
-
// Track agent name in outer scope to persist across stream events
|
|
328
|
-
let capturedAgentName;
|
|
329
|
-
let fullContent = '';
|
|
330
|
-
let collectedThoughts = [];
|
|
331
|
-
let collectedParts = [];
|
|
332
|
-
const pendingTools = new Map();
|
|
333
|
-
let hasAddedMessage = false;
|
|
320
|
+
setMessages(prev => [...prev, tempUserMessage]);
|
|
334
321
|
try {
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
conversation_id: convId,
|
|
342
|
-
model: 'auto',
|
|
343
|
-
selected_agents: selectedAgents,
|
|
344
|
-
attachment_ids: attachmentIds,
|
|
345
|
-
}),
|
|
322
|
+
const result = await hookSendMessage({
|
|
323
|
+
message: content,
|
|
324
|
+
conversation_id: convId,
|
|
325
|
+
model: 'auto',
|
|
326
|
+
selected_agents: selectedAgents,
|
|
327
|
+
attachment_ids: attachmentIds,
|
|
346
328
|
});
|
|
347
|
-
|
|
348
|
-
|
|
329
|
+
// Handle quick replies / prompt from final state
|
|
330
|
+
if (streamState.promptActive && streamState.quickReplies.length > 0) {
|
|
331
|
+
setQuickReplies(streamState.quickReplies);
|
|
332
|
+
setPromptActive(true);
|
|
349
333
|
}
|
|
350
|
-
|
|
351
|
-
const
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
const
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
source: parsed.source,
|
|
376
|
-
message: parsed.message,
|
|
377
|
-
data: parsed.data || parsed,
|
|
378
|
-
timestamp: new Date(),
|
|
379
|
-
};
|
|
380
|
-
// Extract agent name from source if available
|
|
381
|
-
if (parsed.source && !parsed.source.includes('dispatcher')) {
|
|
382
|
-
capturedAgentName = parsed.source;
|
|
383
|
-
const streamState = streamMapRef.current.get(streamConversationId);
|
|
384
|
-
if (streamState) {
|
|
385
|
-
streamState.agentName = capturedAgentName;
|
|
386
|
-
}
|
|
387
|
-
if (isConversationActive(streamConversationId)) {
|
|
388
|
-
setStreamingAgentName(capturedAgentName);
|
|
389
|
-
}
|
|
390
|
-
}
|
|
391
|
-
switch (eventType) {
|
|
392
|
-
case 'conversation_created':
|
|
393
|
-
if (parsed.conversation_id) {
|
|
394
|
-
const newConversationId = parsed.conversation_id;
|
|
395
|
-
const previousConversationId = streamConversationId;
|
|
396
|
-
if (newConversationId !== previousConversationId) {
|
|
397
|
-
const currentStream = streamMapRef.current.get(previousConversationId);
|
|
398
|
-
if (currentStream) {
|
|
399
|
-
streamMapRef.current.delete(previousConversationId);
|
|
400
|
-
setConversationStreamingStatus(previousConversationId, false);
|
|
401
|
-
tempUserMessage = {
|
|
402
|
-
...currentStream.tempUserMessage,
|
|
403
|
-
conversationId: newConversationId,
|
|
404
|
-
};
|
|
405
|
-
const migratedMessages = currentStream.messages.map(msg => msg.id === currentStream.tempUserMessage.id
|
|
406
|
-
? tempUserMessage
|
|
407
|
-
: msg.conversationId === previousConversationId
|
|
408
|
-
? { ...msg, conversationId: newConversationId }
|
|
409
|
-
: msg);
|
|
410
|
-
streamMapRef.current.set(newConversationId, {
|
|
411
|
-
...currentStream,
|
|
412
|
-
tempUserMessage,
|
|
413
|
-
messages: migratedMessages,
|
|
414
|
-
});
|
|
415
|
-
setConversationStreamingStatus(newConversationId, true);
|
|
416
|
-
}
|
|
417
|
-
streamConversationId = newConversationId;
|
|
418
|
-
}
|
|
419
|
-
if (parsed.title) {
|
|
420
|
-
setConversations(prev => {
|
|
421
|
-
const existing = prev.find(c => c.id === newConversationId);
|
|
422
|
-
if (existing) {
|
|
423
|
-
return prev.map(c => c.id === newConversationId ? { ...c, title: parsed.title } : c);
|
|
424
|
-
}
|
|
425
|
-
const newConv = {
|
|
426
|
-
id: newConversationId,
|
|
427
|
-
userId: '',
|
|
428
|
-
title: parsed.title,
|
|
429
|
-
source,
|
|
430
|
-
createdAt: new Date(),
|
|
431
|
-
updatedAt: new Date(),
|
|
432
|
-
messageCount: 0,
|
|
433
|
-
};
|
|
434
|
-
return [newConv, ...prev];
|
|
435
|
-
});
|
|
436
|
-
}
|
|
437
|
-
if (isConversationActive(previousConversationId) || isConversationActive(newConversationId)) {
|
|
438
|
-
currentConversationRef.current = newConversationId;
|
|
439
|
-
setCurrentConversation(prev => prev && prev.id !== previousConversationId
|
|
440
|
-
? prev
|
|
441
|
-
: {
|
|
442
|
-
id: newConversationId,
|
|
443
|
-
userId: prev?.userId || '',
|
|
444
|
-
title: parsed.title || prev?.title || 'New Conversation',
|
|
445
|
-
source: prev?.source ?? source,
|
|
446
|
-
createdAt: prev?.createdAt || new Date(),
|
|
447
|
-
updatedAt: new Date(),
|
|
448
|
-
messageCount: prev?.messageCount ?? 0,
|
|
449
|
-
model: prev?.model,
|
|
450
|
-
metadata: prev?.metadata,
|
|
451
|
-
});
|
|
452
|
-
updateUrlWithConversation(newConversationId);
|
|
453
|
-
}
|
|
454
|
-
}
|
|
455
|
-
break;
|
|
456
|
-
case 'title_update':
|
|
457
|
-
// Update conversation title
|
|
458
|
-
if (parsed.conversation_id && parsed.title) {
|
|
459
|
-
setCurrentConversation(prev => prev && prev.id === parsed.conversation_id ? { ...prev, title: parsed.title } : prev);
|
|
460
|
-
setConversations(prev => prev.map(c => c.id === parsed.conversation_id ? { ...c, title: parsed.title } : c));
|
|
461
|
-
}
|
|
462
|
-
break;
|
|
463
|
-
case 'thought':
|
|
464
|
-
collectedThoughts = [...collectedThoughts, newThought];
|
|
465
|
-
{
|
|
466
|
-
const streamState = streamMapRef.current.get(streamConversationId);
|
|
467
|
-
if (streamState) {
|
|
468
|
-
streamState.thoughts = collectedThoughts;
|
|
469
|
-
}
|
|
470
|
-
}
|
|
471
|
-
if (isConversationActive(streamConversationId)) {
|
|
472
|
-
setThoughts(collectedThoughts);
|
|
473
|
-
}
|
|
474
|
-
break;
|
|
475
|
-
case 'tool_start':
|
|
476
|
-
collectedThoughts = [...collectedThoughts, newThought];
|
|
477
|
-
{
|
|
478
|
-
const toolSource = parsed.source || 'tool';
|
|
479
|
-
const toolName = String(parsed.data?.tool_name || parsed.data?.display_name || toolSource);
|
|
480
|
-
const toolPart = {
|
|
481
|
-
type: 'tool_call',
|
|
482
|
-
id: `tool-${Date.now()}-${toolName}`,
|
|
483
|
-
name: toolName,
|
|
484
|
-
displayName: String(parsed.data?.display_name || parsed.message || toolName),
|
|
485
|
-
status: 'running',
|
|
486
|
-
input: (parsed.data || undefined),
|
|
487
|
-
startedAt: new Date(),
|
|
488
|
-
};
|
|
489
|
-
pendingTools.set(toolSource, collectedParts.length);
|
|
490
|
-
collectedParts = [...collectedParts, toolPart];
|
|
491
|
-
const streamState = streamMapRef.current.get(streamConversationId);
|
|
492
|
-
if (streamState) {
|
|
493
|
-
streamState.thoughts = collectedThoughts;
|
|
494
|
-
streamState.parts = collectedParts;
|
|
495
|
-
}
|
|
496
|
-
if (isConversationActive(streamConversationId)) {
|
|
497
|
-
setThoughts(collectedThoughts);
|
|
498
|
-
setStreamingParts(collectedParts);
|
|
499
|
-
}
|
|
500
|
-
}
|
|
501
|
-
break;
|
|
502
|
-
case 'tool_result':
|
|
503
|
-
collectedThoughts = [...collectedThoughts, newThought];
|
|
504
|
-
{
|
|
505
|
-
const resultSource = parsed.source || 'tool';
|
|
506
|
-
const idx = pendingTools.get(resultSource);
|
|
507
|
-
if (idx !== undefined && collectedParts[idx]?.type === 'tool_call') {
|
|
508
|
-
const existing = collectedParts[idx];
|
|
509
|
-
collectedParts = [...collectedParts];
|
|
510
|
-
collectedParts[idx] = {
|
|
511
|
-
...existing,
|
|
512
|
-
status: parsed.data?.success === false ? 'error' : 'completed',
|
|
513
|
-
output: parsed.message || undefined,
|
|
514
|
-
error: parsed.data?.success === false ? String(parsed.message || 'Failed') : undefined,
|
|
515
|
-
completedAt: new Date(),
|
|
516
|
-
};
|
|
517
|
-
pendingTools.delete(resultSource);
|
|
518
|
-
}
|
|
519
|
-
else {
|
|
520
|
-
const toolName = String(parsed.data?.tool_name || parsed.data?.display_name || resultSource);
|
|
521
|
-
collectedParts = [...collectedParts, {
|
|
522
|
-
type: 'tool_call',
|
|
523
|
-
id: `tool-${Date.now()}-${toolName}`,
|
|
524
|
-
name: toolName,
|
|
525
|
-
displayName: String(parsed.data?.display_name || toolName),
|
|
526
|
-
status: parsed.data?.success === false ? 'error' : 'completed',
|
|
527
|
-
output: parsed.message || undefined,
|
|
528
|
-
completedAt: new Date(),
|
|
529
|
-
}];
|
|
530
|
-
}
|
|
531
|
-
const streamState = streamMapRef.current.get(streamConversationId);
|
|
532
|
-
if (streamState) {
|
|
533
|
-
streamState.thoughts = collectedThoughts;
|
|
534
|
-
streamState.parts = collectedParts;
|
|
535
|
-
}
|
|
536
|
-
if (isConversationActive(streamConversationId)) {
|
|
537
|
-
setThoughts(collectedThoughts);
|
|
538
|
-
setStreamingParts(collectedParts);
|
|
539
|
-
}
|
|
540
|
-
}
|
|
541
|
-
break;
|
|
542
|
-
case 'content':
|
|
543
|
-
// Stream content to message area
|
|
544
|
-
const contentData = parsed.data || {};
|
|
545
|
-
const messageText = parsed.message || '';
|
|
546
|
-
if (contentData.streaming && contentData.partial) {
|
|
547
|
-
fullContent += messageText;
|
|
548
|
-
}
|
|
549
|
-
else if (contentData.complete) {
|
|
550
|
-
// Final marker - content already accumulated
|
|
551
|
-
}
|
|
552
|
-
else if (messageText) {
|
|
553
|
-
fullContent = messageText;
|
|
554
|
-
}
|
|
555
|
-
{
|
|
556
|
-
const cleanContent = stripThinkTags(fullContent);
|
|
557
|
-
const streamState = streamMapRef.current.get(streamConversationId);
|
|
558
|
-
if (streamState) {
|
|
559
|
-
streamState.content = cleanContent;
|
|
560
|
-
}
|
|
561
|
-
if (isConversationActive(streamConversationId)) {
|
|
562
|
-
setStreamingContent(cleanContent);
|
|
563
|
-
}
|
|
564
|
-
}
|
|
565
|
-
break;
|
|
566
|
-
case 'content_chunk':
|
|
567
|
-
// Legacy content chunk event (fallback)
|
|
568
|
-
fullContent += parsed.chunk || parsed.content || '';
|
|
569
|
-
{
|
|
570
|
-
const streamState = streamMapRef.current.get(streamConversationId);
|
|
571
|
-
if (streamState) {
|
|
572
|
-
streamState.content = fullContent;
|
|
573
|
-
}
|
|
574
|
-
}
|
|
575
|
-
if (isConversationActive(streamConversationId)) {
|
|
576
|
-
setStreamingContent(fullContent);
|
|
577
|
-
}
|
|
578
|
-
if (parsed.agent_name && !capturedAgentName) {
|
|
579
|
-
capturedAgentName = parsed.agent_name;
|
|
580
|
-
const streamState = streamMapRef.current.get(streamConversationId);
|
|
581
|
-
if (streamState) {
|
|
582
|
-
streamState.agentName = capturedAgentName;
|
|
583
|
-
}
|
|
584
|
-
if (isConversationActive(streamConversationId)) {
|
|
585
|
-
setStreamingAgentName(capturedAgentName);
|
|
586
|
-
}
|
|
587
|
-
}
|
|
588
|
-
break;
|
|
589
|
-
case 'prompt':
|
|
590
|
-
{
|
|
591
|
-
const promptOptions = parsed.options || parsed.data?.options;
|
|
592
|
-
if (promptOptions && Array.isArray(promptOptions)) {
|
|
593
|
-
setQuickReplies(promptOptions);
|
|
594
|
-
setPromptActive(true);
|
|
595
|
-
const promptType = (parsed.data?.prompt_type || 'choice');
|
|
596
|
-
collectedParts = [...collectedParts, { type: 'prompt', options: promptOptions, promptType }];
|
|
597
|
-
// Finalize accumulated content as a completed message so the user can respond
|
|
598
|
-
const cleanedSoFar = stripThinkTags(fullContent);
|
|
599
|
-
if (cleanedSoFar && !hasAddedMessage) {
|
|
600
|
-
const finalParts = [...collectedParts];
|
|
601
|
-
const assistantMessage = {
|
|
602
|
-
id: `assistant-prompt-${Date.now()}`,
|
|
603
|
-
conversationId: streamConversationId,
|
|
604
|
-
role: 'assistant',
|
|
605
|
-
content: cleanedSoFar,
|
|
606
|
-
agentName: capturedAgentName,
|
|
607
|
-
thoughts: collectedThoughts.length > 0 ? collectedThoughts : undefined,
|
|
608
|
-
parts: finalParts,
|
|
609
|
-
createdAt: new Date(),
|
|
610
|
-
};
|
|
611
|
-
if (isConversationActive(streamConversationId)) {
|
|
612
|
-
setMessages(prev => {
|
|
613
|
-
const withoutTemp = prev.filter(m => m.id !== tempUserMessage.id);
|
|
614
|
-
return [
|
|
615
|
-
...withoutTemp,
|
|
616
|
-
{ ...tempUserMessage, id: `user-${Date.now()}` },
|
|
617
|
-
assistantMessage,
|
|
618
|
-
];
|
|
619
|
-
});
|
|
620
|
-
setStreamingContent('');
|
|
621
|
-
setThoughts([]);
|
|
622
|
-
setStreamingParts([]);
|
|
623
|
-
}
|
|
624
|
-
hasAddedMessage = true;
|
|
625
|
-
const streamState = streamMapRef.current.get(streamConversationId);
|
|
626
|
-
if (streamState) {
|
|
627
|
-
streamState.hasAddedMessage = true;
|
|
628
|
-
}
|
|
629
|
-
}
|
|
630
|
-
}
|
|
631
|
-
}
|
|
632
|
-
break;
|
|
633
|
-
case 'message_complete':
|
|
634
|
-
// Only add message if we haven't already
|
|
635
|
-
if (!hasAddedMessage) {
|
|
636
|
-
const completedConversationId = parsed.conversation_id || streamConversationId;
|
|
637
|
-
const cleanedFinalContent = stripThinkTags(fullContent);
|
|
638
|
-
if (cleanedFinalContent) {
|
|
639
|
-
const finalParts = [
|
|
640
|
-
...collectedParts,
|
|
641
|
-
{ type: 'text', content: cleanedFinalContent },
|
|
642
|
-
];
|
|
643
|
-
const assistantMessage = {
|
|
644
|
-
id: parsed.message_id || `assistant-${Date.now()}`,
|
|
645
|
-
conversationId: completedConversationId,
|
|
646
|
-
role: 'assistant',
|
|
647
|
-
content: cleanedFinalContent,
|
|
648
|
-
model: parsed.model,
|
|
649
|
-
agentName: parsed.agent_name || capturedAgentName,
|
|
650
|
-
thoughts: collectedThoughts.length > 0 ? collectedThoughts : undefined,
|
|
651
|
-
parts: finalParts,
|
|
652
|
-
createdAt: new Date(),
|
|
653
|
-
};
|
|
654
|
-
if (isConversationActive(completedConversationId)) {
|
|
655
|
-
setMessages(prev => {
|
|
656
|
-
const withoutTemp = prev.filter(m => m.id !== tempUserMessage.id);
|
|
657
|
-
return [
|
|
658
|
-
...withoutTemp,
|
|
659
|
-
{ ...tempUserMessage, id: `user-${Date.now()}` },
|
|
660
|
-
assistantMessage,
|
|
661
|
-
];
|
|
662
|
-
});
|
|
663
|
-
}
|
|
664
|
-
else {
|
|
665
|
-
const streamState = streamMapRef.current.get(completedConversationId);
|
|
666
|
-
if (streamState) {
|
|
667
|
-
const withoutTemp = streamState.messages.filter(m => m.id !== tempUserMessage.id);
|
|
668
|
-
streamState.messages = [
|
|
669
|
-
...withoutTemp,
|
|
670
|
-
{ ...tempUserMessage, id: `user-${Date.now()}` },
|
|
671
|
-
assistantMessage,
|
|
672
|
-
];
|
|
673
|
-
}
|
|
674
|
-
}
|
|
675
|
-
hasAddedMessage = true;
|
|
676
|
-
const streamState = streamMapRef.current.get(completedConversationId);
|
|
677
|
-
if (streamState) {
|
|
678
|
-
streamState.hasAddedMessage = true;
|
|
679
|
-
}
|
|
680
|
-
}
|
|
681
|
-
streamMapRef.current.delete(completedConversationId);
|
|
682
|
-
setConversationStreamingStatus(completedConversationId, false);
|
|
683
|
-
if (isConversationActive(completedConversationId)) {
|
|
684
|
-
setStreamingContent('');
|
|
685
|
-
setThoughts([]);
|
|
686
|
-
setStreamingParts([]);
|
|
687
|
-
setStreamingAgentName(undefined);
|
|
688
|
-
setIsStreaming(false);
|
|
689
|
-
}
|
|
690
|
-
}
|
|
691
|
-
break;
|
|
692
|
-
case 'error':
|
|
693
|
-
const errorMessage = parsed.message || parsed.error || 'An error occurred';
|
|
694
|
-
const errorSource = parsed.data?.source || parsed.source || '';
|
|
695
|
-
const isToolError = errorSource && !errorSource.includes('agent') && !errorSource.includes('dispatcher');
|
|
696
|
-
if (isToolError) {
|
|
697
|
-
collectedThoughts = [...collectedThoughts, {
|
|
698
|
-
type: 'error',
|
|
699
|
-
source: errorSource,
|
|
700
|
-
message: `Tool error (${errorSource}): ${errorMessage}`,
|
|
701
|
-
timestamp: new Date(),
|
|
702
|
-
}];
|
|
703
|
-
// Update matching pending tool part to error status
|
|
704
|
-
const errIdx = pendingTools.get(errorSource);
|
|
705
|
-
if (errIdx !== undefined && collectedParts[errIdx]?.type === 'tool_call') {
|
|
706
|
-
const existing = collectedParts[errIdx];
|
|
707
|
-
collectedParts = [...collectedParts];
|
|
708
|
-
collectedParts[errIdx] = { ...existing, status: 'error', error: errorMessage, completedAt: new Date() };
|
|
709
|
-
pendingTools.delete(errorSource);
|
|
710
|
-
}
|
|
711
|
-
if (isConversationActive(streamConversationId)) {
|
|
712
|
-
setThoughts(collectedThoughts);
|
|
713
|
-
setStreamingParts(collectedParts);
|
|
714
|
-
}
|
|
715
|
-
}
|
|
716
|
-
else {
|
|
717
|
-
// Fatal error
|
|
718
|
-
if (isConversationActive(streamConversationId)) {
|
|
719
|
-
toast.error(errorMessage);
|
|
720
|
-
}
|
|
721
|
-
if (!hasAddedMessage) {
|
|
722
|
-
const errorAssistantMessage = {
|
|
723
|
-
id: `error-${Date.now()}`,
|
|
724
|
-
conversationId: streamConversationId,
|
|
725
|
-
role: 'assistant',
|
|
726
|
-
content: fullContent.trim()
|
|
727
|
-
? `${fullContent}\n\n⚠️ **Error:** ${errorMessage}`
|
|
728
|
-
: `⚠️ **Error:** ${errorMessage}`,
|
|
729
|
-
thoughts: collectedThoughts.length > 0 ? collectedThoughts : undefined,
|
|
730
|
-
parts: collectedParts,
|
|
731
|
-
createdAt: new Date(),
|
|
732
|
-
};
|
|
733
|
-
if (isConversationActive(streamConversationId)) {
|
|
734
|
-
setMessages(prev => {
|
|
735
|
-
const withoutTemp = prev.filter(m => m.id !== tempUserMessage.id);
|
|
736
|
-
return [
|
|
737
|
-
...withoutTemp,
|
|
738
|
-
{ ...tempUserMessage, id: `user-${Date.now()}` },
|
|
739
|
-
errorAssistantMessage,
|
|
740
|
-
];
|
|
741
|
-
});
|
|
742
|
-
}
|
|
743
|
-
else {
|
|
744
|
-
const streamState = streamMapRef.current.get(streamConversationId);
|
|
745
|
-
if (streamState) {
|
|
746
|
-
const withoutTemp = streamState.messages.filter(m => m.id !== tempUserMessage.id);
|
|
747
|
-
streamState.messages = [
|
|
748
|
-
...withoutTemp,
|
|
749
|
-
{ ...tempUserMessage, id: `user-${Date.now()}` },
|
|
750
|
-
errorAssistantMessage,
|
|
751
|
-
];
|
|
752
|
-
}
|
|
753
|
-
}
|
|
754
|
-
hasAddedMessage = true;
|
|
755
|
-
const streamState = streamMapRef.current.get(streamConversationId);
|
|
756
|
-
if (streamState) {
|
|
757
|
-
streamState.hasAddedMessage = true;
|
|
758
|
-
}
|
|
759
|
-
}
|
|
760
|
-
streamMapRef.current.delete(streamConversationId);
|
|
761
|
-
setConversationStreamingStatus(streamConversationId, false);
|
|
762
|
-
if (isConversationActive(streamConversationId)) {
|
|
763
|
-
setStreamingContent('');
|
|
764
|
-
setThoughts([]);
|
|
765
|
-
setStreamingParts([]);
|
|
766
|
-
setStreamingAgentName(undefined);
|
|
767
|
-
setIsStreaming(false);
|
|
768
|
-
setPromptActive(false);
|
|
769
|
-
}
|
|
770
|
-
}
|
|
771
|
-
break;
|
|
772
|
-
}
|
|
773
|
-
}
|
|
774
|
-
catch (e) {
|
|
775
|
-
// Ignore parse errors for partial data
|
|
776
|
-
}
|
|
777
|
-
}
|
|
778
|
-
}
|
|
779
|
-
}
|
|
780
|
-
}
|
|
781
|
-
// Reload conversations to update timestamps (filter by source if set)
|
|
782
|
-
if (!controller.signal.aborted) {
|
|
783
|
-
const convUrl = source ? `/conversations?source=${encodeURIComponent(source)}` : '/conversations';
|
|
784
|
-
const convResponse = await apiCall(convUrl);
|
|
785
|
-
const convData = await convResponse.json();
|
|
786
|
-
const rawConversations = convData.conversations || convData || [];
|
|
787
|
-
setConversations(rawConversations.map(mapConversation));
|
|
334
|
+
// Build the final assistant message
|
|
335
|
+
const cleanedContent = stripThinkTags(result.content);
|
|
336
|
+
if (cleanedContent) {
|
|
337
|
+
const finalParts = [
|
|
338
|
+
...result.parts,
|
|
339
|
+
{ type: 'text', content: cleanedContent },
|
|
340
|
+
];
|
|
341
|
+
const assistantMessage = {
|
|
342
|
+
id: `assistant-${Date.now()}`,
|
|
343
|
+
conversationId: result.conversationId || convId,
|
|
344
|
+
role: 'assistant',
|
|
345
|
+
content: cleanedContent,
|
|
346
|
+
agentName: result.agentName,
|
|
347
|
+
thoughts: result.thoughts.length > 0 ? result.thoughts : undefined,
|
|
348
|
+
parts: finalParts,
|
|
349
|
+
createdAt: new Date(),
|
|
350
|
+
};
|
|
351
|
+
setMessages(prev => {
|
|
352
|
+
const withoutTemp = prev.filter(m => m.id !== tempUserMessage.id);
|
|
353
|
+
return [
|
|
354
|
+
...withoutTemp,
|
|
355
|
+
{ ...tempUserMessage, id: `user-${Date.now()}` },
|
|
356
|
+
assistantMessage,
|
|
357
|
+
];
|
|
358
|
+
});
|
|
788
359
|
}
|
|
360
|
+
// Reload conversations to update timestamps
|
|
361
|
+
const convUrl = source ? `/conversations?source=${encodeURIComponent(source)}` : '/conversations';
|
|
362
|
+
const convResponse = await apiCall(convUrl);
|
|
363
|
+
const convData = await convResponse.json();
|
|
364
|
+
const rawConversations = convData.conversations || convData || [];
|
|
365
|
+
setConversations(rawConversations.map(mapConversation));
|
|
789
366
|
}
|
|
790
367
|
catch (error) {
|
|
791
|
-
if (error.name === 'AbortError'
|
|
792
|
-
// Aborted by user or conversation switch — not an error
|
|
368
|
+
if (error.name === 'AbortError')
|
|
793
369
|
return;
|
|
794
|
-
}
|
|
795
370
|
console.error('Failed to send message:', error);
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
setMessages(prev => prev.filter(m => m.id !== tempUserMessage.id));
|
|
799
|
-
}
|
|
800
|
-
}
|
|
801
|
-
finally {
|
|
802
|
-
const currentStream = streamMapRef.current.get(streamConversationId);
|
|
803
|
-
if (currentStream?.controller === controller) {
|
|
804
|
-
streamMapRef.current.delete(streamConversationId);
|
|
805
|
-
setConversationStreamingStatus(streamConversationId, false);
|
|
806
|
-
}
|
|
807
|
-
if (isConversationActive(streamConversationId)) {
|
|
808
|
-
applyStreamStateForConversation(streamConversationId);
|
|
809
|
-
}
|
|
371
|
+
toast.error(error.message || 'Failed to send message');
|
|
372
|
+
setMessages(prev => prev.filter(m => m.id !== tempUserMessage.id));
|
|
810
373
|
}
|
|
811
374
|
}, [
|
|
812
375
|
apiCall,
|
|
813
376
|
ensureConversation,
|
|
814
377
|
selectedAgents,
|
|
815
378
|
source,
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
updateUrlWithConversation,
|
|
379
|
+
hookSendMessage,
|
|
380
|
+
streamState.promptActive,
|
|
381
|
+
streamState.quickReplies,
|
|
820
382
|
]);
|
|
821
383
|
const handleStopStreaming = useCallback(() => {
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
return;
|
|
825
|
-
const streamState = streamMapRef.current.get(conversationId);
|
|
826
|
-
if (!streamState)
|
|
827
|
-
return;
|
|
828
|
-
streamState.controller.abort();
|
|
829
|
-
streamMapRef.current.delete(conversationId);
|
|
830
|
-
setConversationStreamingStatus(conversationId, false);
|
|
831
|
-
applyStreamStateForConversation(conversationId);
|
|
832
|
-
}, [setConversationStreamingStatus, applyStreamStateForConversation]);
|
|
384
|
+
hookCancel();
|
|
385
|
+
}, [hookCancel]);
|
|
833
386
|
const handleDeleteMessage = useCallback(async (messageId) => {
|
|
834
387
|
if (!currentConversation) {
|
|
835
|
-
// No conversation - just remove from local state
|
|
836
388
|
setMessages(prev => prev.filter(m => m.id !== messageId));
|
|
837
389
|
return;
|
|
838
390
|
}
|
|
@@ -849,7 +401,6 @@ export function ChatContainer({ initialConversations, initialMessages, initialCo
|
|
|
849
401
|
}
|
|
850
402
|
}, [apiCall, currentConversation]);
|
|
851
403
|
const handleRetryMessage = useCallback(async (messageContent, attachmentIds) => {
|
|
852
|
-
// Capture attachment metadata from the original message before removing it
|
|
853
404
|
let attachmentMeta;
|
|
854
405
|
if (attachmentIds && attachmentIds.length > 0) {
|
|
855
406
|
const userMsg = [...messages].reverse().find(m => m.role === 'user' && m.attachments?.length);
|
|
@@ -857,11 +408,9 @@ export function ChatContainer({ initialConversations, initialMessages, initialCo
|
|
|
857
408
|
attachmentMeta = userMsg.attachments.filter(a => attachmentIds.includes(a.id));
|
|
858
409
|
}
|
|
859
410
|
}
|
|
860
|
-
// Delete the last assistant message from local state
|
|
861
411
|
if (messages.length > 0 && messages[messages.length - 1].role === 'assistant') {
|
|
862
412
|
setMessages(prev => prev.slice(0, -1));
|
|
863
413
|
}
|
|
864
|
-
// Delete the user message that we're retrying from local state
|
|
865
414
|
if (messages.length > 0) {
|
|
866
415
|
const lastUserMsgIndex = [...messages].reverse().findIndex(m => m.role === 'user');
|
|
867
416
|
if (lastUserMsgIndex !== -1) {
|
|
@@ -869,11 +418,9 @@ export function ChatContainer({ initialConversations, initialMessages, initialCo
|
|
|
869
418
|
setMessages(prev => prev.filter(m => m.id !== lastUserMsg.id));
|
|
870
419
|
}
|
|
871
420
|
}
|
|
872
|
-
// Re-send with both attachment IDs (for the backend) and metadata (for the UI)
|
|
873
421
|
await handleSendMessage(messageContent, attachmentIds, attachmentMeta);
|
|
874
422
|
}, [messages, handleSendMessage]);
|
|
875
|
-
//
|
|
876
|
-
// Passes conversation_id so thread-scoped insights (goal, context) are filtered
|
|
423
|
+
// ── Insights ───────────────────────────────────────────────────────────
|
|
877
424
|
const loadInsights = useCallback(async (category, offset, append = false) => {
|
|
878
425
|
setIsLoadingInsights(true);
|
|
879
426
|
try {
|
|
@@ -908,20 +455,17 @@ export function ChatContainer({ initialConversations, initialMessages, initialCo
|
|
|
908
455
|
setIsLoadingInsights(false);
|
|
909
456
|
}
|
|
910
457
|
}, [apiCall]);
|
|
911
|
-
// Refresh insights when conversation changes (if panel is open)
|
|
912
458
|
useEffect(() => {
|
|
913
459
|
if (showInsightsPanel && currentConversation?.id) {
|
|
914
460
|
loadInsights(insightCategoryFilter, 0, false);
|
|
915
461
|
}
|
|
916
462
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
917
463
|
}, [currentConversation?.id]);
|
|
918
|
-
// Load more insights (infinite scroll)
|
|
919
464
|
const loadMoreInsights = useCallback(() => {
|
|
920
465
|
if (!isLoadingInsights && hasMoreInsights) {
|
|
921
466
|
loadInsights(insightCategoryFilter, insightOffset + INSIGHT_PAGE_SIZE, true);
|
|
922
467
|
}
|
|
923
468
|
}, [isLoadingInsights, hasMoreInsights, insightCategoryFilter, insightOffset, loadInsights]);
|
|
924
|
-
// Handle category filter click
|
|
925
469
|
const handleCategoryFilter = useCallback((category) => {
|
|
926
470
|
setInsightCategoryFilter(category);
|
|
927
471
|
setInsightOffset(0);
|
|
@@ -949,7 +493,6 @@ export function ChatContainer({ initialConversations, initialMessages, initialCo
|
|
|
949
493
|
else {
|
|
950
494
|
toast('No insights could be extracted from this conversation');
|
|
951
495
|
}
|
|
952
|
-
// Reload insights list and stats
|
|
953
496
|
await loadInsights(insightCategoryFilter, 0, false);
|
|
954
497
|
const statsResponse = await apiCall('/insights/stats/me');
|
|
955
498
|
setInsightStats(await statsResponse.json());
|
|
@@ -964,7 +507,6 @@ export function ChatContainer({ initialConversations, initialMessages, initialCo
|
|
|
964
507
|
}, [apiCall, currentConversation, insightCategoryFilter, loadInsights]);
|
|
965
508
|
const handleSearchInsights = useCallback(async () => {
|
|
966
509
|
if (!insightSearchQuery.trim()) {
|
|
967
|
-
// If no search query, load all insights
|
|
968
510
|
loadInsights(insightCategoryFilter, 0, false);
|
|
969
511
|
return;
|
|
970
512
|
}
|
|
@@ -981,7 +523,7 @@ export function ChatContainer({ initialConversations, initialMessages, initialCo
|
|
|
981
523
|
const data = await response.json();
|
|
982
524
|
setInsights(data.results || []);
|
|
983
525
|
setInsightTotal(data.results?.length || 0);
|
|
984
|
-
setHasMoreInsights(false);
|
|
526
|
+
setHasMoreInsights(false);
|
|
985
527
|
if (!data.results?.length) {
|
|
986
528
|
toast('No insights found');
|
|
987
529
|
}
|
|
@@ -994,9 +536,7 @@ export function ChatContainer({ initialConversations, initialMessages, initialCo
|
|
|
994
536
|
setIsLoadingInsights(false);
|
|
995
537
|
}
|
|
996
538
|
}, [apiCall, insightSearchQuery, currentConversation, insightCategoryFilter, loadInsights]);
|
|
997
|
-
// Handle insight click - open modal
|
|
998
539
|
const handleInsightClick = useCallback((result) => {
|
|
999
|
-
// Convert Date to string if needed for InsightData type
|
|
1000
540
|
const createdAt = result.insight?.createdAt;
|
|
1001
541
|
const createdAtStr = createdAt instanceof Date
|
|
1002
542
|
? createdAt.toISOString()
|
|
@@ -1009,7 +549,6 @@ export function ChatContainer({ initialConversations, initialMessages, initialCo
|
|
|
1009
549
|
createdAt: createdAtStr,
|
|
1010
550
|
});
|
|
1011
551
|
}, []);
|
|
1012
|
-
// Handle insight save
|
|
1013
552
|
const handleSaveInsight = useCallback(async (id, content, category) => {
|
|
1014
553
|
try {
|
|
1015
554
|
await apiCall(`/insights/${id}`, {
|
|
@@ -1017,9 +556,7 @@ export function ChatContainer({ initialConversations, initialMessages, initialCo
|
|
|
1017
556
|
body: JSON.stringify({ content, category }),
|
|
1018
557
|
});
|
|
1019
558
|
toast.success('Insight updated');
|
|
1020
|
-
// Refresh insights list
|
|
1021
559
|
await loadInsights(insightCategoryFilter, 0, false);
|
|
1022
|
-
// Update the selected insight
|
|
1023
560
|
setSelectedInsight(prev => prev ? { ...prev, content, category } : null);
|
|
1024
561
|
}
|
|
1025
562
|
catch (error) {
|
|
@@ -1028,14 +565,12 @@ export function ChatContainer({ initialConversations, initialMessages, initialCo
|
|
|
1028
565
|
throw error;
|
|
1029
566
|
}
|
|
1030
567
|
}, [apiCall, insightCategoryFilter, loadInsights]);
|
|
1031
|
-
// Handle insight delete
|
|
1032
568
|
const handleDeleteInsight = useCallback(async (id) => {
|
|
1033
569
|
try {
|
|
1034
570
|
await apiCall(`/insights/${id}`, {
|
|
1035
571
|
method: 'DELETE',
|
|
1036
572
|
});
|
|
1037
573
|
toast.success('Insight deleted');
|
|
1038
|
-
// Refresh insights list and stats
|
|
1039
574
|
await loadInsights(insightCategoryFilter, 0, false);
|
|
1040
575
|
const statsResponse = await apiCall('/insights/stats/me');
|
|
1041
576
|
setInsightStats(await statsResponse.json());
|
|
@@ -1068,7 +603,6 @@ export function ChatContainer({ initialConversations, initialMessages, initialCo
|
|
|
1068
603
|
});
|
|
1069
604
|
const result = await response.json();
|
|
1070
605
|
toast.success(`Added ${result.count || 1} insight`);
|
|
1071
|
-
// Reload insights list and stats
|
|
1072
606
|
await loadInsights(insightCategoryFilter, 0, false);
|
|
1073
607
|
const statsResponse = await apiCall('/insights/stats/me');
|
|
1074
608
|
setInsightStats(await statsResponse.json());
|
|
@@ -1078,6 +612,8 @@ export function ChatContainer({ initialConversations, initialMessages, initialCo
|
|
|
1078
612
|
toast.error('Failed to add insight');
|
|
1079
613
|
}
|
|
1080
614
|
}, [apiCall, currentConversation, insightCategoryFilter, loadInsights]);
|
|
615
|
+
// ── Render ─────────────────────────────────────────────────────────────
|
|
616
|
+
const isStreaming = streamState.isStreaming;
|
|
1081
617
|
const conversationSidebar = (_jsxs("div", { className: "w-64 h-full bg-white dark:bg-gray-800 border-r border-gray-200 dark:border-gray-700 flex flex-col", children: [_jsxs("div", { className: "p-4 border-b border-gray-200 dark:border-gray-700", children: [_jsx("h2", { className: "text-lg font-semibold text-gray-900 dark:text-white mb-3", children: "Conversations" }), allowConversationManagement && (_jsxs("button", { onClick: handleCreateConversation, className: "w-full flex items-center justify-center gap-2 px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 transition-colors", children: [_jsx(Plus, { className: "w-4 h-4" }), "New Chat"] }))] }), _jsx("div", { className: "flex-1 overflow-y-auto", children: conversations.length === 0 ? (_jsxs("div", { className: "p-4 text-center text-gray-500 dark:text-gray-400", children: [_jsx(MessageSquare, { className: "w-8 h-8 mx-auto mb-2 text-gray-300 dark:text-gray-600" }), _jsx("p", { className: "text-sm", children: "No conversations yet" })] })) : (_jsx("div", { className: "space-y-1 p-2", children: conversations.map((conv) => (_jsxs("div", { className: `group flex items-center gap-2 p-3 rounded-lg cursor-pointer transition-colors ${currentConversation?.id === conv.id
|
|
1082
618
|
? 'bg-blue-50 dark:bg-blue-900/50 text-blue-700 dark:text-blue-300'
|
|
1083
619
|
: 'hover:bg-gray-50 dark:hover:bg-gray-700'}`, onClick: () => handleSelectConversation(conv), children: [_jsx(MessageSquare, { className: "w-4 h-4 flex-shrink-0" }), _jsxs("div", { className: "flex-1 min-w-0", children: [_jsx("p", { className: "text-sm font-medium truncate", children: conv.title || 'Untitled' }), _jsxs("p", { className: "text-xs text-gray-500 dark:text-gray-400", children: [conv.messageCount || 0, " messages"] })] }), streamingConvIds.has(conv.id) && (_jsx("span", { className: "w-2 h-2 bg-blue-500 rounded-full animate-pulse flex-shrink-0", title: "Thinking in background" })), allowConversationManagement && (_jsx("button", { onClick: (e) => {
|
|
@@ -1107,7 +643,6 @@ export function ChatContainer({ initialConversations, initialMessages, initialCo
|
|
|
1107
643
|
if (willOpen) {
|
|
1108
644
|
setShowAgentPanel(false);
|
|
1109
645
|
setShowTasksPanel(false);
|
|
1110
|
-
// Load insights when panel opens
|
|
1111
646
|
loadInsights(null, 0, false);
|
|
1112
647
|
}
|
|
1113
648
|
}, className: `flex items-center gap-2 px-3 py-2 text-sm border rounded-lg transition-colors ${showInsightsPanel
|
|
@@ -1120,24 +655,12 @@ export function ChatContainer({ initialConversations, initialMessages, initialCo
|
|
|
1120
655
|
}
|
|
1121
656
|
}, className: `flex items-center gap-2 px-3 py-2 text-sm border rounded-lg transition-colors ${showTasksPanel
|
|
1122
657
|
? 'bg-amber-100 dark:bg-amber-900/50 text-amber-700 dark:text-amber-300 border-amber-300 dark:border-amber-600'
|
|
1123
|
-
: 'bg-white dark:bg-gray-700 text-gray-600 dark:text-gray-300 border-gray-300 dark:border-gray-600 hover:bg-gray-50 dark:hover:bg-gray-600'}`, title: "View and manage tasks", children: [_jsx(ListTodo, { className: "w-4 h-4" }), _jsx("span", { className: "font-medium hidden sm:inline", children: "Tasks" })] })] })] }) }), _jsx("div", { className: "flex-1 overflow-y-auto bg-white dark:bg-gray-800", children: isLoadingMessages ? (_jsx("div", { className: "flex items-center justify-center h-full", children: _jsx("div", { className: "text-gray-500 dark:text-gray-400", children: "Loading messages..." }) })) : (_jsx(MessageList, { messages: messages, streamingContent:
|
|
1124
|
-
.filter((p) => p.type === 'tool_call')
|
|
1125
|
-
.map((part) => (_jsx(StreamingToolCard, { part: part }, part.id))) })), quickReplies.length > 0 && (_jsx("div", { className: "flex-shrink-0 px-3 pt-2 pb-1 flex flex-wrap gap-2 justify-center border-t border-gray-200 dark:border-gray-700 bg-white dark:bg-gray-900", children: quickReplies.map((reply) => (_jsx("button", { type: "button", onClick: () => {
|
|
658
|
+
: 'bg-white dark:bg-gray-700 text-gray-600 dark:text-gray-300 border-gray-300 dark:border-gray-600 hover:bg-gray-50 dark:hover:bg-gray-600'}`, title: "View and manage tasks", children: [_jsx(ListTodo, { className: "w-4 h-4" }), _jsx("span", { className: "font-medium hidden sm:inline", children: "Tasks" })] })] })] }) }), _jsx("div", { className: "flex-1 overflow-y-auto bg-white dark:bg-gray-800", children: isLoadingMessages ? (_jsx("div", { className: "flex items-center justify-center h-full", children: _jsx("div", { className: "text-gray-500 dark:text-gray-400", children: "Loading messages..." }) })) : (_jsx(MessageList, { messages: messages, streamingContent: streamState.content || undefined, streamingAgentName: streamState.agentName, streamingThoughts: streamState.thoughts, streamingParts: streamState.parts, isLoading: isStreaming, onDeleteMessage: handleDeleteMessage, onRetryMessage: handleRetryMessage, onSuggestedAction: (action) => handleSendMessage(action) })) }), (quickReplies.length > 0 || (streamState.quickReplies.length > 0 && streamState.promptActive)) && (_jsx("div", { className: "flex-shrink-0 px-3 pt-2 pb-1 flex flex-wrap gap-2 justify-center border-t border-gray-200 dark:border-gray-700 bg-white dark:bg-gray-900", children: (quickReplies.length > 0 ? quickReplies : streamState.quickReplies).map((reply) => (_jsx("button", { type: "button", onClick: () => {
|
|
1126
659
|
setQuickReplies([]);
|
|
1127
660
|
setPromptActive(false);
|
|
1128
|
-
|
|
1129
|
-
const convId = currentConversationRef.current;
|
|
1130
|
-
if (convId) {
|
|
1131
|
-
const streamState = streamMapRef.current.get(convId);
|
|
1132
|
-
if (streamState) {
|
|
1133
|
-
streamState.controller.abort();
|
|
1134
|
-
streamMapRef.current.delete(convId);
|
|
1135
|
-
setConversationStreamingStatus(convId, false);
|
|
1136
|
-
}
|
|
1137
|
-
}
|
|
1138
|
-
setIsStreaming(false);
|
|
661
|
+
hookCancel();
|
|
1139
662
|
setTimeout(() => handleSendMessage(reply), 50);
|
|
1140
|
-
}, className: "px-4 py-1.5 text-sm font-medium rounded-full border border-blue-500 dark:border-blue-400 text-blue-600 dark:text-blue-400 hover:bg-blue-50 dark:hover:bg-blue-900/30 transition-colors", children: reply }, reply))) })), _jsx(MessageInput, { onSend: handleSendMessage, onStop: handleStopStreaming, disabled:
|
|
663
|
+
}, className: "px-4 py-1.5 text-sm font-medium rounded-full border border-blue-500 dark:border-blue-400 text-blue-600 dark:text-blue-400 hover:bg-blue-50 dark:hover:bg-blue-900/30 transition-colors", children: reply }, reply))) })), _jsx(MessageInput, { onSend: handleSendMessage, onStop: handleStopStreaming, disabled: false, isStreaming: isStreaming, conversationId: currentConversation?.id, onEnsureConversation: ensureConversation })] }), showInsights && showInsightsPanel && (_jsxs("div", { className: "w-80 bg-white dark:bg-gray-800 border-l border-gray-200 dark:border-gray-700 flex flex-col", children: [_jsxs("div", { className: "p-4 border-b border-gray-200 dark:border-gray-700", children: [_jsxs("div", { className: "flex items-center justify-between mb-3", children: [_jsx("h3", { className: "text-lg font-semibold text-gray-900 dark:text-white", children: "Insights" }), _jsx("button", { onClick: () => setShowInsightsPanel(false), className: "p-1 text-gray-500 hover:text-gray-700 dark:hover:text-gray-300", children: _jsx(X, { className: "w-5 h-5" }) })] }), currentConversation && (_jsxs("div", { className: "flex gap-2 mb-3", children: [_jsxs("button", { onClick: handleForceInsight, className: "flex-1 flex items-center justify-center gap-2 px-3 py-1.5 text-sm bg-amber-100 dark:bg-amber-900 text-amber-700 dark:text-amber-300 rounded-lg hover:bg-amber-200 dark:hover:bg-amber-800 transition-colors", title: "Manually add an insight", children: [_jsx(Sparkles, { className: "w-4 h-4" }), "Add"] }), _jsxs("button", { onClick: handleGenerateInsights, disabled: isLoadingInsights, className: "flex-1 flex items-center justify-center gap-2 px-3 py-1.5 text-sm bg-purple-100 dark:bg-purple-900 text-purple-700 dark:text-purple-300 rounded-lg hover:bg-purple-200 dark:hover:bg-purple-800 transition-colors disabled:opacity-50", children: [_jsx(Brain, { className: "w-4 h-4" }), "Generate"] })] })), insightStats && (_jsxs("div", { className: "mb-3", children: [_jsxs("div", { className: "flex items-center justify-between mb-2", children: [_jsxs("span", { className: "text-sm font-medium text-gray-700 dark:text-gray-300", children: [insightStats.total, " insights"] }), insightCategoryFilter && (_jsx("button", { onClick: () => handleCategoryFilter(null), className: "text-xs text-purple-600 dark:text-purple-400 hover:underline", children: "Clear filter" }))] }), _jsx("div", { className: "flex flex-wrap gap-1", children: Object.entries(insightStats.by_category || {}).map(([category, count]) => (_jsxs("button", { onClick: () => handleCategoryFilter(insightCategoryFilter === category ? null : category), className: `text-xs px-2 py-1 rounded-full transition-colors ${insightCategoryFilter === category
|
|
1141
664
|
? category === 'preference'
|
|
1142
665
|
? 'bg-blue-500 text-white'
|
|
1143
666
|
: category === 'fact'
|
|
@@ -1164,7 +687,6 @@ export function ChatContainer({ initialConversations, initialMessages, initialCo
|
|
|
1164
687
|
}
|
|
1165
688
|
}, placeholder: "Search insights...", className: "flex-1 px-3 py-2 text-sm border border-gray-300 dark:border-gray-600 rounded-lg focus:outline-none focus:ring-2 focus:ring-purple-500 bg-white dark:bg-gray-700 text-gray-900 dark:text-white" }), _jsx("button", { onClick: handleSearchInsights, disabled: isLoadingInsights, className: "p-2 bg-purple-600 text-white rounded-lg hover:bg-purple-700 transition-colors disabled:opacity-50", children: _jsx(Search, { className: "w-4 h-4" }) })] })] }), _jsx("div", { className: "flex-1 overflow-y-auto p-4 space-y-3", onScroll: (e) => {
|
|
1166
689
|
const target = e.target;
|
|
1167
|
-
// Load more when scrolled near bottom
|
|
1168
690
|
if (target.scrollHeight - target.scrollTop - target.clientHeight < 100) {
|
|
1169
691
|
loadMoreInsights();
|
|
1170
692
|
}
|
|
@@ -1180,16 +702,14 @@ export function ChatContainer({ initialConversations, initialMessages, initialCo
|
|
|
1180
702
|
: result.insight?.category === 'context'
|
|
1181
703
|
? 'bg-amber-100 text-amber-700 dark:bg-amber-900 dark:text-amber-300'
|
|
1182
704
|
: 'bg-gray-100 text-gray-700 dark:bg-gray-600 dark:text-gray-300'}`, children: result.insight?.category || 'other' }), result.score < 1 && (_jsxs("span", { className: "text-xs text-gray-500 dark:text-gray-400", children: [(result.score * 100).toFixed(0), "% match"] }))] }), _jsx("div", { className: "prose prose-sm dark:prose-invert max-w-none text-sm text-gray-900 dark:text-white break-words overflow-hidden", children: _jsx(ReactMarkdown, { remarkPlugins: [remarkGfm], components: {
|
|
1183
|
-
// Ensure links wrap properly
|
|
1184
705
|
a: ({ children, href }) => (_jsx("a", { href: href, target: "_blank", rel: "noopener noreferrer", className: "text-purple-600 dark:text-purple-400 hover:underline break-all", onClick: (e) => e.stopPropagation(), children: children })),
|
|
1185
|
-
// Ensure paragraphs don't have excessive margins
|
|
1186
706
|
p: ({ children }) => _jsx("p", { className: "mb-1 last:mb-0", children: children }),
|
|
1187
707
|
}, children: result.insight?.content || '' }) }), result.insight?.createdAt && (_jsx("p", { className: "text-xs text-gray-400 dark:text-gray-500 mt-2", children: new Date(result.insight.createdAt).toLocaleDateString() }))] }, result.insight?.id || index))), isLoadingInsights && (_jsx("div", { className: "text-center text-gray-500 dark:text-gray-400 py-2", children: "Loading..." })), hasMoreInsights && !isLoadingInsights && (_jsx("button", { onClick: loadMoreInsights, className: "w-full py-2 text-sm text-purple-600 dark:text-purple-400 hover:underline", children: "Load more..." }))] })) })] })), selectedInsight && (_jsx(InsightEditModal, { insight: selectedInsight, onClose: () => setSelectedInsight(null), onSave: handleSaveInsight, onDelete: handleDeleteInsight })), showAgentSelector && showAgentPanel && (_jsx(AgentSelectionPanel, { selectedAgents: selectedAgents, onAgentsChange: setSelectedAgents, availableAgents: availableAgents.filter(a => a.is_active).map(a => ({
|
|
1188
708
|
id: a.id,
|
|
1189
709
|
name: a.display_name || a.name,
|
|
1190
710
|
description: a.description || '',
|
|
1191
711
|
enabled: a.is_active,
|
|
1192
|
-
capabilities: [],
|
|
712
|
+
capabilities: [],
|
|
1193
713
|
})), onClose: () => setShowAgentPanel(false) })), showTasksPanel && (_jsxs("div", { className: "w-80 bg-white dark:bg-gray-800 border-l border-gray-200 dark:border-gray-700 flex flex-col", children: [_jsxs("div", { className: "p-4 border-b border-gray-200 dark:border-gray-700", children: [_jsxs("div", { className: "flex items-center justify-between mb-3", children: [_jsx("h3", { className: "text-lg font-semibold text-gray-900 dark:text-white", children: "Tasks" }), _jsx("button", { onClick: () => setShowTasksPanel(false), className: "p-1 text-gray-500 hover:text-gray-700 dark:hover:text-gray-300", children: _jsx(X, { className: "w-5 h-5" }) })] }), _jsx("p", { className: "text-sm text-gray-500 dark:text-gray-400", children: "Manage background tasks and scheduled operations." })] }), _jsx("div", { className: "flex-1 overflow-y-auto p-4", children: _jsxs("div", { className: "text-center text-gray-500 dark:text-gray-400", children: [_jsx(ListTodo, { className: "w-12 h-12 mx-auto mb-3 text-gray-300 dark:text-gray-600" }), _jsx("p", { className: "text-sm font-medium", children: "Coming Soon" }), _jsx("p", { className: "text-xs mt-2", children: "Agent tasks feature is under development. Soon you'll be able to:" }), _jsxs("ul", { className: "text-xs mt-3 space-y-1 text-left max-w-[200px] mx-auto", children: [_jsxs("li", { className: "flex items-center gap-2", children: [_jsx("span", { className: "w-1.5 h-1.5 bg-amber-500 rounded-full" }), "Schedule recurring tasks"] }), _jsxs("li", { className: "flex items-center gap-2", children: [_jsx("span", { className: "w-1.5 h-1.5 bg-amber-500 rounded-full" }), "Create background research jobs"] }), _jsxs("li", { className: "flex items-center gap-2", children: [_jsx("span", { className: "w-1.5 h-1.5 bg-amber-500 rounded-full" }), "Set up automated reports"] })] })] }) })] }))] }));
|
|
1194
714
|
}
|
|
1195
715
|
//# sourceMappingURL=ChatContainer.js.map
|