@jazzmind/busibox-app 3.0.37 → 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.
Files changed (41) hide show
  1. package/dist/components/chat/ChatContainer.d.ts.map +1 -1
  2. package/dist/components/chat/ChatContainer.js +122 -602
  3. package/dist/components/chat/ChatContainer.js.map +1 -1
  4. package/dist/components/chat/ChatInterface.d.ts +3 -4
  5. package/dist/components/chat/ChatInterface.d.ts.map +1 -1
  6. package/dist/components/chat/ChatInterface.js +159 -361
  7. package/dist/components/chat/ChatInterface.js.map +1 -1
  8. package/dist/components/chat/MessageList.d.ts +2 -1
  9. package/dist/components/chat/MessageList.d.ts.map +1 -1
  10. package/dist/components/chat/MessageList.js +33 -17
  11. package/dist/components/chat/MessageList.js.map +1 -1
  12. package/dist/components/chat/StepTimeline.d.ts +9 -0
  13. package/dist/components/chat/StepTimeline.d.ts.map +1 -0
  14. package/dist/components/chat/StepTimeline.js +110 -0
  15. package/dist/components/chat/StepTimeline.js.map +1 -0
  16. package/dist/components/chat/ThinkingStream.d.ts +13 -0
  17. package/dist/components/chat/ThinkingStream.d.ts.map +1 -0
  18. package/dist/components/chat/ThinkingStream.js +41 -0
  19. package/dist/components/chat/ThinkingStream.js.map +1 -0
  20. package/dist/components/chat/ThinkingToggle.d.ts.map +1 -1
  21. package/dist/components/chat/ThinkingToggle.js +15 -1
  22. package/dist/components/chat/ThinkingToggle.js.map +1 -1
  23. package/dist/lib/agent/agent-api-base.d.ts.map +1 -1
  24. package/dist/lib/agent/agent-api-base.js +0 -1
  25. package/dist/lib/agent/agent-api-base.js.map +1 -1
  26. package/dist/lib/agent/chat-client.d.ts.map +1 -1
  27. package/dist/lib/agent/chat-client.js +0 -1
  28. package/dist/lib/agent/chat-client.js.map +1 -1
  29. package/dist/lib/agent/index.d.ts +2 -0
  30. package/dist/lib/agent/index.d.ts.map +1 -1
  31. package/dist/lib/agent/index.js +2 -0
  32. package/dist/lib/agent/index.js.map +1 -1
  33. package/dist/lib/agent/stream-event-processor.d.ts +37 -0
  34. package/dist/lib/agent/stream-event-processor.d.ts.map +1 -0
  35. package/dist/lib/agent/stream-event-processor.js +204 -0
  36. package/dist/lib/agent/stream-event-processor.js.map +1 -0
  37. package/dist/lib/hooks/useChatStream.d.ts +34 -0
  38. package/dist/lib/hooks/useChatStream.d.ts.map +1 -0
  39. package/dist/lib/hooks/useChatStream.js +105 -0
  40. package/dist/lib/hooks/useChatStream.js.map +1 -0
  41. 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
- * Interactive wrapper that receives server-rendered data and handles
7
- * all client-side state and interactions.
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
- // Chat state
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 [streamingParts, setStreamingParts] = useState([]);
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 streamMapRef.current.values()) {
105
+ for (const streamState of backgroundStreamRef.current.values()) {
110
106
  streamState.controller.abort();
111
107
  }
112
- streamMapRef.current.clear();
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
- const applyStreamStateForConversation = useCallback((conversationId) => {
131
- const streamState = streamMapRef.current.get(conversationId);
132
- if (!streamState) {
133
- setIsStreaming(false);
134
- setStreamingContent('');
135
- setStreamingAgentName(undefined);
136
- setThoughts([]);
137
- setStreamingParts([]);
138
- return;
139
- }
140
- setIsStreaming(true);
141
- setStreamingContent(streamState.content);
142
- setStreamingAgentName(streamState.agentName);
143
- setThoughts(streamState.thoughts);
144
- setStreamingParts(streamState.parts);
145
- }, []);
146
- // Memoize default agent selection to prevent infinite loop
147
- // Use agent IDs (UUIDs) not names for backend compatibility
148
- // Chat agent is the versatile general-purpose agent that should be the default
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, applyStreamStateForConversation]);
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, applyStreamStateForConversation]);
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 streamState = streamMapRef.current.get(conversationId);
267
- if (streamState) {
268
- streamState.controller.abort();
269
- streamMapRef.current.delete(conversationId);
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
- // Add user message optimistically
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
- if (isConversationActive(convId)) {
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
- // Use agentic streaming endpoint for real-time thinking updates
336
- const response = await apiCall('/chat/message/stream/agentic', {
337
- method: 'POST',
338
- signal: controller.signal,
339
- body: JSON.stringify({
340
- message: content,
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
- if (!response.body) {
348
- throw new Error('No response body');
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
- const reader = response.body.getReader();
351
- const decoder = new TextDecoder();
352
- let eventType = '';
353
- let buffer = '';
354
- while (true) {
355
- if (controller.signal.aborted)
356
- break;
357
- const { done, value } = await reader.read();
358
- if (done)
359
- break;
360
- buffer += decoder.decode(value, { stream: true });
361
- const lines = buffer.split('\n');
362
- buffer = lines.pop() || '';
363
- for (const line of lines) {
364
- if (line.startsWith('event:')) {
365
- eventType = line.slice(6).trim();
366
- }
367
- else if (line.startsWith('data:')) {
368
- const data = line.slice(5).trim();
369
- if (data && eventType) {
370
- try {
371
- const parsed = JSON.parse(data);
372
- // Create thought event for tracking
373
- const newThought = {
374
- type: eventType,
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' || controller.signal.aborted) {
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
- if (isConversationActive(streamConversationId)) {
797
- toast.error(error.message || 'Failed to send message');
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
- isConversationActive,
817
- setConversationStreamingStatus,
818
- applyStreamStateForConversation,
819
- updateUrlWithConversation,
379
+ hookSendMessage,
380
+ streamState.promptActive,
381
+ streamState.quickReplies,
820
382
  ]);
821
383
  const handleStopStreaming = useCallback(() => {
822
- const conversationId = currentConversationRef.current;
823
- if (!conversationId)
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
- // Load insights list (paginated, with optional category filter)
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); // Search results don't paginate
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: streamingContent, streamingAgentName: streamingAgentName, streamingThoughts: thoughts, isLoading: isStreaming, onDeleteMessage: handleDeleteMessage, onRetryMessage: handleRetryMessage, onSuggestedAction: (action) => handleSendMessage(action) })) }), isStreaming && streamingParts.filter(p => p.type === 'tool_call').length > 0 && (_jsx("div", { className: "flex-shrink-0 px-4 py-2 border-t border-gray-200 dark:border-gray-700 bg-white dark:bg-gray-800", children: streamingParts
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
- // Abort lingering stream before sending the reply
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: isStreaming && !promptActive, isStreaming: isStreaming && !promptActive, 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
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: [], // Could add capabilities from agent definition if available
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