@jazzmind/busibox-app 3.0.29 → 3.0.31

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 (49) hide show
  1. package/dist/components/chat/ChatContainer.d.ts.map +1 -1
  2. package/dist/components/chat/ChatContainer.js +155 -14
  3. package/dist/components/chat/ChatContainer.js.map +1 -1
  4. package/dist/components/chat/ChatInterface.d.ts +37 -14
  5. package/dist/components/chat/ChatInterface.d.ts.map +1 -1
  6. package/dist/components/chat/ChatInterface.js +645 -83
  7. package/dist/components/chat/ChatInterface.js.map +1 -1
  8. package/dist/components/chat/FullChatInterface.d.ts.map +1 -1
  9. package/dist/components/chat/FullChatInterface.js +4 -0
  10. package/dist/components/chat/FullChatInterface.js.map +1 -1
  11. package/dist/components/chat/LegacyChatInterface.d.ts +16 -0
  12. package/dist/components/chat/LegacyChatInterface.d.ts.map +1 -0
  13. package/dist/components/chat/LegacyChatInterface.js +134 -0
  14. package/dist/components/chat/LegacyChatInterface.js.map +1 -0
  15. package/dist/components/chat/MessageList.d.ts +4 -1
  16. package/dist/components/chat/MessageList.d.ts.map +1 -1
  17. package/dist/components/chat/MessageList.js +64 -2
  18. package/dist/components/chat/MessageList.js.map +1 -1
  19. package/dist/components/chat/SimpleChatInterface.d.ts +9 -38
  20. package/dist/components/chat/SimpleChatInterface.d.ts.map +1 -1
  21. package/dist/components/chat/SimpleChatInterface.js +1 -596
  22. package/dist/components/chat/SimpleChatInterface.js.map +1 -1
  23. package/dist/components/chat/StreamingToolCard.d.ts +11 -0
  24. package/dist/components/chat/StreamingToolCard.d.ts.map +1 -0
  25. package/dist/components/chat/StreamingToolCard.js +20 -0
  26. package/dist/components/chat/StreamingToolCard.js.map +1 -0
  27. package/dist/components/chat/ThinkingToggle.d.ts +4 -5
  28. package/dist/components/chat/ThinkingToggle.d.ts.map +1 -1
  29. package/dist/components/chat/ThinkingToggle.js +62 -12
  30. package/dist/components/chat/ThinkingToggle.js.map +1 -1
  31. package/dist/components/chat/chat-utils.d.ts +297 -0
  32. package/dist/components/chat/chat-utils.d.ts.map +1 -0
  33. package/dist/components/chat/chat-utils.js +43 -0
  34. package/dist/components/chat/chat-utils.js.map +1 -0
  35. package/dist/components/index.d.ts +5 -0
  36. package/dist/components/index.d.ts.map +1 -1
  37. package/dist/components/index.js +6 -1
  38. package/dist/components/index.js.map +1 -1
  39. package/dist/index.d.ts +1 -1
  40. package/dist/index.d.ts.map +1 -1
  41. package/dist/lib/agent/chat-client.d.ts.map +1 -1
  42. package/dist/lib/agent/chat-client.js +7 -1
  43. package/dist/lib/agent/chat-client.js.map +1 -1
  44. package/dist/lib/data/documents.d.ts.map +1 -1
  45. package/dist/lib/data/documents.js +21 -3
  46. package/dist/lib/data/documents.js.map +1 -1
  47. package/dist/types/chat.d.ts +24 -0
  48. package/dist/types/chat.d.ts.map +1 -1
  49. package/package.json +1 -1
@@ -1,117 +1,679 @@
1
1
  'use client';
2
- import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
+ import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
3
+ /**
4
+ * ChatInterface - Core Chat Component
5
+ *
6
+ * The primary chat component for all Busibox apps. Supports:
7
+ * - Agentic streaming with real-time thoughts, tool cards, and prompts
8
+ * - Standard streaming and non-streaming fallback modes
9
+ * - Message parts architecture (text, thinking, tool calls, prompts)
10
+ * - Quick replies and bracket-based suggested actions
11
+ * - Progressive disclosure for agent thinking
12
+ * - Optional file attachments
13
+ * - Single conversation UI (no sidebar)
14
+ *
15
+ * For the portal-level chat with conversation sidebar, insights, and
16
+ * multi-agent selection, see ChatContainer.
17
+ *
18
+ * Usage:
19
+ * ```typescript
20
+ * <ChatInterface
21
+ * token="bearer-token"
22
+ * enableWebSearch={true}
23
+ * enableDocSearch={false}
24
+ * allowAttachments={false}
25
+ * placeholder="Ask me anything..."
26
+ * useAgenticStreaming={true}
27
+ * />
28
+ * ```
29
+ */
3
30
  import { useState, useRef, useEffect } from 'react';
4
- import { Send, Bot, User, Loader2, Trash2 } from 'lucide-react';
31
+ import { Send, Bot, Loader2, Paperclip, Brain, CheckCircle, AlertCircle, Plus, Trash2, Volume2, X } from 'lucide-react';
32
+ import toast from 'react-hot-toast';
5
33
  import ReactMarkdown from 'react-markdown';
6
34
  import remarkGfm from 'remark-gfm';
7
- import { useBusiboxApi } from '../../contexts/ApiContext';
8
- import { fetchServiceFirstFallbackNext } from '../../lib/http/fetch-with-fallback';
9
- export function ChatInterface({ availableModels, nextChatPath = '/api/chat', serviceChatPath = '/api/chat' }) {
10
- const api = useBusiboxApi();
35
+ import remarkMath from 'remark-math';
36
+ import rehypeKatex from 'rehype-katex';
37
+ import 'katex/dist/katex.min.css';
38
+ import { MessageList } from './MessageList';
39
+ import { ThinkingToggle } from './ThinkingToggle';
40
+ import { StreamingToolCard } from './StreamingToolCard';
41
+ import { stripThinkTags, preprocessLatex, streamingMarkdownComponents } from './chat-utils';
42
+ import { sendChatMessage, streamChatMessage, streamChatMessageAgentic, getConversationHistory } from '../../lib/agent/chat-client';
43
+ // Real-time execution status display (legacy, for non-agentic streaming)
44
+ function ExecutionStatus({ events, isActive }) {
45
+ if (events.length === 0)
46
+ return null;
47
+ // Only show the last few relevant events
48
+ const relevantEvents = events.filter(e => ['planning', 'tool_start', 'tool_result', 'agent_start', 'agent_result', 'synthesis_start'].includes(e.type)).slice(-4);
49
+ if (relevantEvents.length === 0)
50
+ return null;
51
+ return (_jsxs("div", { className: "flex gap-3 justify-start mb-4", children: [_jsx("div", { className: "bg-blue-600 dark:bg-blue-500 rounded-full p-2 h-8 w-8 flex items-center justify-center flex-shrink-0", children: _jsx(Bot, { className: "w-4 h-4 text-white" }) }), _jsx("div", { className: "max-w-[80%] rounded-lg p-3 bg-gray-100 dark:bg-gray-800 text-gray-900 dark:text-gray-100", children: _jsxs("div", { className: "space-y-1.5", children: [relevantEvents.map((event, idx) => (_jsxs("div", { className: "flex items-center gap-2 text-sm", children: [event.type === 'planning' && (_jsxs(_Fragment, { children: [_jsx(Brain, { className: "w-3.5 h-3.5 text-purple-500 flex-shrink-0" }), _jsx("span", { className: "text-purple-700 dark:text-purple-400", children: event.message || 'Analyzing...' })] })), event.type === 'tool_start' && (_jsxs(_Fragment, { children: [_jsx(Loader2, { className: "w-3.5 h-3.5 text-blue-500 animate-spin flex-shrink-0" }), _jsx("span", { className: "text-blue-700 dark:text-blue-400", children: event.message || `Running ${event.data?.display_name || event.data?.tool}...` })] })), event.type === 'tool_result' && (_jsxs(_Fragment, { children: [event.data?.success !== false ? (_jsx(CheckCircle, { className: "w-3.5 h-3.5 text-green-500 flex-shrink-0" })) : (_jsx(AlertCircle, { className: "w-3.5 h-3.5 text-red-500 flex-shrink-0" })), _jsx("span", { className: event.data?.success !== false ? 'text-green-700 dark:text-green-400' : 'text-red-700 dark:text-red-400', children: event.message || `${event.data?.display_name || event.data?.tool_name} done` })] })), event.type === 'agent_start' && (_jsxs(_Fragment, { children: [_jsx(Loader2, { className: "w-3.5 h-3.5 text-indigo-500 animate-spin flex-shrink-0" }), _jsx("span", { className: "text-indigo-700 dark:text-indigo-400", children: event.message || `Consulting ${event.data?.display_name || event.data?.agent}...` })] })), event.type === 'agent_result' && (_jsxs(_Fragment, { children: [event.data?.success !== false ? (_jsx(CheckCircle, { className: "w-3.5 h-3.5 text-green-500 flex-shrink-0" })) : (_jsx(AlertCircle, { className: "w-3.5 h-3.5 text-red-500 flex-shrink-0" })), _jsx("span", { className: event.data?.success !== false ? 'text-green-700 dark:text-green-400' : 'text-red-700 dark:text-red-400', children: event.message || 'Agent responded' })] })), event.type === 'synthesis_start' && (_jsxs(_Fragment, { children: [_jsx(Loader2, { className: "w-3.5 h-3.5 text-purple-500 animate-spin flex-shrink-0" }), _jsx("span", { className: "text-purple-700 dark:text-purple-400", children: event.message || 'Combining results...' })] }))] }, idx))), isActive && (_jsxs("div", { className: "flex items-center gap-2 text-xs text-gray-500 dark:text-gray-400 mt-1", children: [_jsx(Loader2, { className: "w-3 h-3 animate-spin" }), _jsx("span", { children: "Processing..." })] }))] }) })] }));
52
+ }
53
+ export function ChatInterface({ token, agentUrl, agentId, enableWebSearch = false, enableDocSearch = false, allowAttachments = false, placeholder = 'Type your message...', welcomeMessage, model = 'auto', useStreaming = true, useAgenticStreaming = false, className = '', onMessageSent, onResponseReceived, initialConversationId, metadata, onConversationDeleted, }) {
11
54
  const [messages, setMessages] = useState([]);
12
55
  const [input, setInput] = useState('');
13
56
  const [isLoading, setIsLoading] = useState(false);
14
- const [selectedModel, setSelectedModel] = useState(availableModels.length > 0 ? availableModels[0].id : '');
57
+ const [streamingContent, setStreamingContent] = useState('');
58
+ const [executionEvents, setExecutionEvents] = useState([]);
59
+ const [thoughts, setThoughts] = useState([]); // For agentic streaming
60
+ const [interimMessages, setInterimMessages] = useState([]);
61
+ const [streamingAgentName, setStreamingAgentName] = useState(undefined);
62
+ const [conversationId, setConversationId] = useState(initialConversationId);
63
+ const [attachments, setAttachments] = useState([]);
64
+ const [abortController, setAbortController] = useState(null);
65
+ const [loadingHistory, setLoadingHistory] = useState(false);
66
+ const [showVoiceComingSoon, setShowVoiceComingSoon] = useState(false);
67
+ const [quickReplies, setQuickReplies] = useState([]);
68
+ const [promptActive, setPromptActive] = useState(false);
69
+ const [streamingParts, setStreamingParts] = useState([]);
15
70
  const messagesEndRef = useRef(null);
71
+ const fileInputRef = useRef(null);
16
72
  const textareaRef = useRef(null);
17
73
  const scrollToBottom = () => {
18
74
  messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' });
19
75
  };
20
76
  useEffect(() => {
21
77
  scrollToBottom();
22
- }, [messages]);
23
- const handleSubmit = async (e) => {
24
- e.preventDefault();
25
- if (!input.trim() || isLoading)
78
+ }, [messages, streamingContent]);
79
+ // Load conversation history when initialConversationId is provided
80
+ useEffect(() => {
81
+ if (initialConversationId && token) {
82
+ loadConversationHistory(initialConversationId);
83
+ }
84
+ }, [initialConversationId, token]);
85
+ const loadConversationHistory = async (convId) => {
86
+ setLoadingHistory(true);
87
+ try {
88
+ const history = await getConversationHistory(convId, { token, agentUrl });
89
+ // Convert to DisplayMessage format
90
+ const displayMessages = history.map(msg => ({
91
+ id: msg.id,
92
+ role: msg.role,
93
+ content: msg.content,
94
+ createdAt: new Date(msg.created_at || msg.createdAt || Date.now()),
95
+ }));
96
+ setMessages(displayMessages);
97
+ setConversationId(convId);
98
+ }
99
+ catch (error) {
100
+ console.error('Failed to load conversation history:', error);
101
+ toast.error('Failed to load conversation history');
102
+ }
103
+ finally {
104
+ setLoadingHistory(false);
105
+ }
106
+ };
107
+ const handleFileSelect = async (e) => {
108
+ const files = e.target.files;
109
+ if (!files || files.length === 0)
110
+ return;
111
+ // For simplicity, just store file info
112
+ // In a real implementation, you'd upload to data API first
113
+ const newAttachments = Array.from(files).map((file) => ({
114
+ name: file.name,
115
+ type: file.type,
116
+ url: URL.createObjectURL(file),
117
+ size: file.size,
118
+ }));
119
+ setAttachments((prev) => [...prev, ...newAttachments]);
120
+ toast.success(`${files.length} file(s) attached`);
121
+ // Reset input
122
+ if (fileInputRef.current) {
123
+ fileInputRef.current.value = '';
124
+ }
125
+ };
126
+ const handleRemoveAttachment = (index) => {
127
+ setAttachments((prev) => prev.filter((_, i) => i !== index));
128
+ };
129
+ const handleSubmit = async (e, overrideMessage) => {
130
+ e?.preventDefault();
131
+ const messageText = overrideMessage ?? input.trim();
132
+ // Allow submission when promptActive even if isLoading is still true
133
+ if (!messageText || (isLoading && !promptActive))
26
134
  return;
27
- const userMessage = { role: 'user', content: input };
28
- setMessages((prev) => [...prev, userMessage]);
135
+ // If we're submitting during a prompt-active state, abort the lingering stream
136
+ if (isLoading && promptActive && abortController) {
137
+ abortController.abort();
138
+ }
139
+ const userMessage = messageText;
29
140
  setInput('');
141
+ setPromptActive(false);
30
142
  if (textareaRef.current) {
31
143
  textareaRef.current.style.height = 'auto';
32
144
  }
145
+ // Add user message to display
146
+ const userDisplayMessage = {
147
+ role: 'user',
148
+ content: userMessage,
149
+ timestamp: new Date(),
150
+ };
151
+ setMessages((prev) => [...prev, userDisplayMessage]);
152
+ // Callback
153
+ onMessageSent?.(userMessage);
33
154
  setIsLoading(true);
34
- setMessages((prev) => [...prev, { role: 'assistant', content: '' }]);
155
+ setQuickReplies([]);
156
+ // Create abort controller for cancellation
157
+ const controller = new AbortController();
158
+ setAbortController(controller);
35
159
  try {
36
- const body = JSON.stringify({
37
- messages: [...messages, userMessage],
38
- model: selectedModel,
39
- });
40
- const response = await fetchServiceFirstFallbackNext({
41
- service: {
42
- baseUrl: api.services?.agentApiUrl,
43
- path: serviceChatPath,
44
- init: {
45
- method: 'POST',
46
- headers: { 'Content-Type': 'application/json' },
47
- body,
48
- },
49
- },
50
- next: {
51
- nextApiBasePath: api.nextApiBasePath,
52
- path: nextChatPath,
53
- init: {
54
- method: 'POST',
55
- headers: { 'Content-Type': 'application/json' },
56
- body,
57
- },
58
- },
59
- fallback: {
60
- fallbackOnNetworkError: api.fallback?.fallbackOnNetworkError ?? true,
61
- fallbackStatuses: [
62
- ...(api.fallback?.fallbackStatuses ?? [404, 405, 501, 502, 503, 504]),
63
- 400,
64
- 401,
65
- 403,
66
- 422,
67
- ],
68
- },
69
- serviceHeaders: api.serviceRequestHeaders,
70
- });
71
- if (!response.ok || !response.body) {
72
- const errorData = await response.json().catch(() => ({ error: response.statusText }));
73
- throw new Error(errorData.error || response.statusText);
160
+ const request = {
161
+ message: userMessage,
162
+ conversation_id: conversationId,
163
+ model,
164
+ enable_web_search: enableWebSearch,
165
+ enable_doc_search: enableDocSearch,
166
+ attachments: attachments.length > 0 ? attachments : undefined,
167
+ selected_agents: agentId ? [agentId] : undefined,
168
+ metadata: metadata || undefined,
169
+ };
170
+ if (useAgenticStreaming) {
171
+ // Agentic streaming with real-time thoughts and message parts
172
+ setStreamingContent('');
173
+ setThoughts([]);
174
+ setInterimMessages([]);
175
+ setStreamingAgentName(undefined);
176
+ setStreamingParts([]);
177
+ let fullContent = '';
178
+ let collectedThoughts = [];
179
+ let collectedParts = [];
180
+ let hasAddedMessage = false;
181
+ // Track in-flight tool calls by source so we can update their status
182
+ const pendingTools = new Map(); // source -> index in collectedParts
183
+ for await (const event of streamChatMessageAgentic(request, { token, agentUrl, signal: controller.signal })) {
184
+ const newEvent = {
185
+ type: event.type,
186
+ source: event.data?.source,
187
+ message: event.data?.message,
188
+ data: event.data,
189
+ timestamp: new Date(),
190
+ };
191
+ if (event.data?.source && !event.data.source.includes('dispatcher')) {
192
+ setStreamingAgentName(event.data.source);
193
+ }
194
+ switch (event.type) {
195
+ case 'conversation_created':
196
+ setConversationId(event.data.conversation_id);
197
+ break;
198
+ case 'thought':
199
+ case 'plan':
200
+ case 'progress':
201
+ collectedThoughts = [...collectedThoughts, newEvent];
202
+ setThoughts(collectedThoughts);
203
+ break;
204
+ case 'tool_start':
205
+ {
206
+ collectedThoughts = [...collectedThoughts, newEvent];
207
+ setThoughts(collectedThoughts);
208
+ const toolSource = event.data?.source || 'tool';
209
+ const toolName = String(event.data?.data?.tool_name || event.data?.data?.display_name || toolSource);
210
+ const toolPart = {
211
+ type: 'tool_call',
212
+ id: `tool-${Date.now()}-${toolName}`,
213
+ name: toolName,
214
+ displayName: String(event.data?.data?.display_name || event.data?.message || toolName),
215
+ status: 'running',
216
+ input: (event.data?.data || undefined),
217
+ startedAt: new Date(),
218
+ };
219
+ pendingTools.set(toolSource, collectedParts.length);
220
+ collectedParts = [...collectedParts, toolPart];
221
+ setStreamingParts(collectedParts);
222
+ }
223
+ break;
224
+ case 'tool_result':
225
+ {
226
+ collectedThoughts = [...collectedThoughts, newEvent];
227
+ setThoughts(collectedThoughts);
228
+ const resultSource = event.data?.source || 'tool';
229
+ const idx = pendingTools.get(resultSource);
230
+ if (idx !== undefined && collectedParts[idx]?.type === 'tool_call') {
231
+ const existing = collectedParts[idx];
232
+ const updated = {
233
+ ...existing,
234
+ status: event.data?.data?.success === false ? 'error' : 'completed',
235
+ output: event.data?.message || undefined,
236
+ error: event.data?.data?.success === false ? String(event.data?.message || 'Failed') : undefined,
237
+ completedAt: new Date(),
238
+ };
239
+ collectedParts = [...collectedParts];
240
+ collectedParts[idx] = updated;
241
+ pendingTools.delete(resultSource);
242
+ }
243
+ else {
244
+ // No matching tool_start; append as a standalone completed tool
245
+ const toolName = String(event.data?.data?.tool_name || event.data?.data?.display_name || resultSource);
246
+ collectedParts = [...collectedParts, {
247
+ type: 'tool_call',
248
+ id: `tool-${Date.now()}-${toolName}`,
249
+ name: toolName,
250
+ displayName: String(event.data?.data?.display_name || toolName),
251
+ status: event.data?.data?.success === false ? 'error' : 'completed',
252
+ output: event.data?.message || undefined,
253
+ completedAt: new Date(),
254
+ }];
255
+ }
256
+ setStreamingParts(collectedParts);
257
+ }
258
+ break;
259
+ case 'interim':
260
+ {
261
+ const payload = event.data || {};
262
+ const nested = payload.data || {};
263
+ const interimMessage = String(payload.message || '').trim();
264
+ const audioUrl = typeof nested.audio_url === 'string' ? nested.audio_url : '';
265
+ const rendered = audioUrl
266
+ ? `${interimMessage || 'Voice output ready'} (${audioUrl})`
267
+ : interimMessage;
268
+ if (rendered) {
269
+ setInterimMessages(prev => [...prev, rendered]);
270
+ }
271
+ }
272
+ break;
273
+ case 'content':
274
+ {
275
+ const contentData = event.data?.data || {};
276
+ const msgText = event.data?.message || '';
277
+ if (contentData.streaming && contentData.partial) {
278
+ fullContent += msgText;
279
+ }
280
+ else if (contentData.complete) {
281
+ // Final marker
282
+ }
283
+ else if (msgText) {
284
+ fullContent = msgText;
285
+ }
286
+ setStreamingContent(stripThinkTags(fullContent));
287
+ }
288
+ break;
289
+ case 'prompt':
290
+ {
291
+ const promptOptions = event.data?.data?.options || event.data?.options;
292
+ if (promptOptions && Array.isArray(promptOptions)) {
293
+ setQuickReplies(promptOptions);
294
+ setPromptActive(true);
295
+ // Add a prompt part for rendering
296
+ const promptType = (event.data?.data?.prompt_type || 'choice');
297
+ collectedParts = [...collectedParts, { type: 'prompt', options: promptOptions, promptType }];
298
+ setStreamingParts(collectedParts);
299
+ // Finalize accumulated content as a completed message
300
+ const cleanedSoFar = stripThinkTags(fullContent);
301
+ if (cleanedSoFar && !hasAddedMessage) {
302
+ const assistantMessage = {
303
+ role: 'assistant',
304
+ content: cleanedSoFar,
305
+ timestamp: new Date(),
306
+ thoughts: collectedThoughts.length > 0 ? collectedThoughts : undefined,
307
+ agentName: streamingAgentName,
308
+ parts: collectedParts,
309
+ };
310
+ setMessages((prev) => [...prev, assistantMessage]);
311
+ hasAddedMessage = true;
312
+ }
313
+ setStreamingContent('');
314
+ setThoughts([]);
315
+ setInterimMessages([]);
316
+ setStreamingParts([]);
317
+ }
318
+ }
319
+ break;
320
+ case 'complete':
321
+ break;
322
+ case 'message_complete':
323
+ if (!hasAddedMessage) {
324
+ setConversationId(event.data.conversation_id);
325
+ const cleanedContent = stripThinkTags(fullContent);
326
+ if (cleanedContent) {
327
+ // Build final text part if there's content not yet in parts
328
+ const finalParts = [
329
+ ...collectedParts,
330
+ { type: 'text', content: cleanedContent },
331
+ ];
332
+ const assistantMessage = {
333
+ role: 'assistant',
334
+ content: cleanedContent,
335
+ timestamp: new Date(),
336
+ thoughts: collectedThoughts.length > 0 ? collectedThoughts : undefined,
337
+ agentName: streamingAgentName,
338
+ parts: finalParts,
339
+ };
340
+ setMessages((prev) => [...prev, assistantMessage]);
341
+ hasAddedMessage = true;
342
+ }
343
+ setStreamingContent('');
344
+ setThoughts([]);
345
+ setInterimMessages([]);
346
+ setStreamingAgentName(undefined);
347
+ setStreamingParts([]);
348
+ setIsLoading(false);
349
+ if (cleanedContent) {
350
+ onResponseReceived?.(cleanedContent);
351
+ }
352
+ }
353
+ break;
354
+ case 'error':
355
+ {
356
+ const errorMessage = event.data?.message || event.data?.error || 'An error occurred';
357
+ const errorSource = event.data?.source || event.data?.data?.source || '';
358
+ const isToolError = errorSource && !errorSource.includes('agent') && !errorSource.includes('dispatcher');
359
+ if (isToolError) {
360
+ collectedThoughts = [...collectedThoughts, {
361
+ type: 'error',
362
+ source: errorSource,
363
+ message: `Tool error (${errorSource}): ${errorMessage}`,
364
+ data: event.data,
365
+ timestamp: new Date(),
366
+ }];
367
+ setThoughts(collectedThoughts);
368
+ // Update matching pending tool part to error status
369
+ const errIdx = pendingTools.get(errorSource);
370
+ if (errIdx !== undefined && collectedParts[errIdx]?.type === 'tool_call') {
371
+ const existing = collectedParts[errIdx];
372
+ collectedParts = [...collectedParts];
373
+ collectedParts[errIdx] = { ...existing, status: 'error', error: errorMessage, completedAt: new Date() };
374
+ pendingTools.delete(errorSource);
375
+ setStreamingParts(collectedParts);
376
+ }
377
+ }
378
+ else {
379
+ toast.error(errorMessage);
380
+ if (!hasAddedMessage) {
381
+ const errorContent = fullContent.trim()
382
+ ? `${fullContent}\n\n**Error:** ${errorMessage}`
383
+ : `**Error:** ${errorMessage}`;
384
+ const errorAssistantMessage = {
385
+ role: 'assistant',
386
+ content: errorContent,
387
+ timestamp: new Date(),
388
+ thoughts: collectedThoughts.length > 0 ? collectedThoughts : undefined,
389
+ agentName: streamingAgentName,
390
+ parts: collectedParts,
391
+ };
392
+ setMessages((prev) => [...prev, errorAssistantMessage]);
393
+ hasAddedMessage = true;
394
+ }
395
+ setStreamingContent('');
396
+ setThoughts([]);
397
+ setInterimMessages([]);
398
+ setStreamingAgentName(undefined);
399
+ setStreamingParts([]);
400
+ }
401
+ }
402
+ break;
403
+ }
404
+ }
405
+ }
406
+ else if (useStreaming) {
407
+ // Standard streaming response
408
+ setStreamingContent('');
409
+ setExecutionEvents([]);
410
+ let fullContent = '';
411
+ let collectedEvents = [];
412
+ for await (const event of streamChatMessage(request, { token, agentUrl, signal: controller.signal })) {
413
+ const newEvent = {
414
+ type: event.type,
415
+ message: event.data?.message,
416
+ data: event.data,
417
+ timestamp: new Date(),
418
+ };
419
+ switch (event.type) {
420
+ case 'conversation_created':
421
+ setConversationId(event.data.conversation_id);
422
+ break;
423
+ case 'planning':
424
+ case 'tool_start':
425
+ case 'agent_start':
426
+ case 'agent_response_start':
427
+ case 'synthesis_start':
428
+ collectedEvents = [...collectedEvents, newEvent];
429
+ setExecutionEvents(collectedEvents);
430
+ break;
431
+ case 'tool_result':
432
+ case 'agent_result':
433
+ case 'routing_decision':
434
+ case 'model_selected':
435
+ collectedEvents = [...collectedEvents, newEvent];
436
+ setExecutionEvents(collectedEvents);
437
+ break;
438
+ case 'content_chunk':
439
+ // Only append if chunk is defined and not empty
440
+ if (event.data.chunk !== undefined && event.data.chunk !== null) {
441
+ fullContent += event.data.chunk;
442
+ setStreamingContent(fullContent);
443
+ }
444
+ break;
445
+ case 'execution_complete':
446
+ // Clear execution events when content starts flowing
447
+ break;
448
+ case 'message_complete':
449
+ setConversationId(event.data.conversation_id);
450
+ // Add assistant message to display
451
+ const assistantMessage = {
452
+ role: 'assistant',
453
+ content: fullContent,
454
+ timestamp: new Date(),
455
+ };
456
+ setMessages((prev) => [...prev, assistantMessage]);
457
+ setStreamingContent('');
458
+ setExecutionEvents([]);
459
+ // Callback
460
+ onResponseReceived?.(fullContent);
461
+ break;
462
+ case 'error':
463
+ const streamErrorMessage = event.data?.error || 'An error occurred';
464
+ toast.error(streamErrorMessage);
465
+ // Add error message to chat
466
+ const streamErrorAssistantMessage = {
467
+ role: 'assistant',
468
+ content: `⚠️ **Error:** ${streamErrorMessage}`,
469
+ timestamp: new Date(),
470
+ };
471
+ setMessages((prev) => [...prev, streamErrorAssistantMessage]);
472
+ setStreamingContent('');
473
+ setExecutionEvents([]);
474
+ break;
475
+ }
476
+ }
74
477
  }
75
- const reader = response.body.getReader();
76
- const decoder = new TextDecoder();
77
- let assistantMessage = '';
78
- while (true) {
79
- const { done, value } = await reader.read();
80
- if (done)
81
- break;
82
- const chunk = decoder.decode(value, { stream: true });
83
- assistantMessage += chunk;
84
- setMessages((prev) => {
85
- const newMessages = [...prev];
86
- const lastMsg = newMessages[newMessages.length - 1];
87
- if (lastMsg && lastMsg.role === 'assistant')
88
- lastMsg.content = assistantMessage;
89
- return [...newMessages];
90
- });
478
+ else {
479
+ // Non-streaming response
480
+ const response = await sendChatMessage(request, { token, agentUrl });
481
+ setConversationId(response.conversation_id);
482
+ // Add assistant message to display
483
+ const assistantMessage = {
484
+ role: 'assistant',
485
+ content: response.content,
486
+ timestamp: new Date(),
487
+ };
488
+ setMessages((prev) => [...prev, assistantMessage]);
489
+ // Callback
490
+ onResponseReceived?.(response.content);
91
491
  }
492
+ // Clear attachments after successful send
493
+ setAttachments([]);
92
494
  }
93
495
  catch (error) {
94
- console.error('Chat error:', error);
95
- setMessages((prev) => {
96
- const newMessages = prev.slice(0, -1);
97
- return [
98
- ...newMessages,
99
- {
496
+ if (error.name === 'AbortError') {
497
+ // Request was cancelled - add partial response if any
498
+ if (streamingContent) {
499
+ const partialMessage = {
100
500
  role: 'assistant',
101
- content: `❌ Error: ${error.message || 'Could not get a response. Please check that liteLLM is running.'}`,
102
- },
103
- ];
104
- });
501
+ content: streamingContent + '\n\n*[Response interrupted]*',
502
+ timestamp: new Date(),
503
+ thoughts: thoughts.length > 0 ? thoughts : undefined,
504
+ };
505
+ setMessages((prev) => [...prev, partialMessage]);
506
+ }
507
+ toast('Response cancelled', { icon: '⏹️' });
508
+ }
509
+ else {
510
+ console.error('Chat error:', error);
511
+ toast.error(error.message || 'Failed to send message');
512
+ // Add error message
513
+ const errorMessage = {
514
+ role: 'assistant',
515
+ content: `❌ Error: ${error.message || 'Failed to send message'}`,
516
+ timestamp: new Date(),
517
+ };
518
+ setMessages((prev) => [...prev, errorMessage]);
519
+ }
105
520
  }
106
521
  finally {
107
522
  setIsLoading(false);
523
+ setStreamingContent('');
524
+ setExecutionEvents([]);
525
+ setThoughts([]);
526
+ setInterimMessages([]);
527
+ setAbortController(null);
528
+ setPromptActive(false);
529
+ setStreamingParts([]);
530
+ }
531
+ };
532
+ const handleCancel = () => {
533
+ if (abortController) {
534
+ abortController.abort();
535
+ }
536
+ };
537
+ const handleQuickReply = (reply) => {
538
+ setQuickReplies([]);
539
+ setPromptActive(false);
540
+ // Abort lingering stream before sending the reply as a new message
541
+ if (isLoading && abortController) {
542
+ abortController.abort();
543
+ }
544
+ // Small delay to let the abort settle before we fire a new request
545
+ setTimeout(() => handleSubmit(null, reply), 50);
546
+ };
547
+ const handleNewChat = () => {
548
+ // Cancel any ongoing request
549
+ if (abortController) {
550
+ abortController.abort();
108
551
  }
552
+ // Reset all state
553
+ setMessages([]);
554
+ setConversationId(undefined);
555
+ setStreamingContent('');
556
+ setExecutionEvents([]);
557
+ setThoughts([]);
558
+ setInterimMessages([]);
559
+ setIsLoading(false);
560
+ setAttachments([]);
561
+ setStreamingAgentName(undefined);
562
+ setQuickReplies([]);
563
+ setPromptActive(false);
564
+ setInput('');
565
+ toast.success('Started new chat');
109
566
  };
110
- const handleClearChat = () => {
111
- if (confirm('Clear all messages?'))
567
+ const handleDeleteMessage = async (messageId) => {
568
+ if (!conversationId) {
569
+ // No conversation - just remove from local state
570
+ setMessages(prev => prev.filter(m => m.id !== messageId));
571
+ return;
572
+ }
573
+ try {
574
+ const response = await fetch(`${agentUrl || ''}/chat/${conversationId}/messages/${messageId}`, {
575
+ method: 'DELETE',
576
+ headers: {
577
+ 'Authorization': `Bearer ${token}`,
578
+ 'Content-Type': 'application/json',
579
+ },
580
+ });
581
+ if (!response.ok) {
582
+ throw new Error('Failed to delete message');
583
+ }
584
+ setMessages(prev => prev.filter(m => m.id !== messageId));
585
+ toast.success('Message deleted');
586
+ }
587
+ catch (error) {
588
+ console.error('Failed to delete message:', error);
589
+ toast.error('Failed to delete message');
590
+ }
591
+ };
592
+ const handleDeleteConversation = async () => {
593
+ if (!conversationId) {
594
+ // No conversation to delete, just clear local messages
595
+ handleNewChat();
596
+ return;
597
+ }
598
+ if (!confirm('Are you sure you want to delete this conversation?'))
599
+ return;
600
+ const deletedId = conversationId;
601
+ try {
602
+ const response = await fetch(`${agentUrl || ''}/conversations/${conversationId}`, {
603
+ method: 'DELETE',
604
+ headers: {
605
+ 'Authorization': `Bearer ${token}`,
606
+ 'Content-Type': 'application/json',
607
+ },
608
+ });
609
+ if (!response.ok) {
610
+ throw new Error('Failed to delete conversation');
611
+ }
612
+ // Clear local state
112
613
  setMessages([]);
614
+ setConversationId(undefined);
615
+ setStreamingContent('');
616
+ setThoughts([]);
617
+ setInterimMessages([]);
618
+ setIsLoading(false);
619
+ setAttachments([]);
620
+ setStreamingAgentName(undefined);
621
+ setInput('');
622
+ // Notify parent about the deletion
623
+ onConversationDeleted?.(deletedId);
624
+ toast.success('Conversation deleted');
625
+ }
626
+ catch (error) {
627
+ console.error('Failed to delete conversation:', error);
628
+ toast.error('Failed to delete conversation');
629
+ }
630
+ };
631
+ const handleRetryMessage = async (messageContent, attachmentIds) => {
632
+ // Delete the last assistant message if it exists
633
+ if (messages.length > 0 && messages[messages.length - 1].role === 'assistant') {
634
+ setMessages(prev => prev.slice(0, -1));
635
+ }
636
+ // Delete the user message that we're retrying
637
+ if (messages.length > 0 && messages[messages.length - 1].role === 'user') {
638
+ setMessages(prev => prev.slice(0, -1));
639
+ }
640
+ // Re-send the message by setting input and triggering submit
641
+ setInput(messageContent);
642
+ // Small delay to ensure state is updated, then trigger submit
643
+ setTimeout(() => {
644
+ const form = document.querySelector('form');
645
+ if (form) {
646
+ form.requestSubmit();
647
+ }
648
+ }, 50);
113
649
  };
114
- return (_jsxs("div", { className: "bg-white rounded-lg shadow-lg flex flex-col h-[calc(100vh-200px)]", children: [_jsxs("div", { className: "flex items-center justify-between p-4 border-b border-gray-200", children: [_jsxs("div", { className: "flex items-center gap-3", children: [_jsx(Bot, { className: "w-6 h-6 text-indigo-600" }), _jsxs("div", { children: [_jsx("h3", { className: "text-lg font-semibold text-gray-900", children: "AI Chat" }), _jsx("p", { className: "text-xs text-gray-500", children: "Powered by liteLLM" })] })] }), _jsxs("div", { className: "flex items-center gap-3", children: [_jsx("select", { value: selectedModel, onChange: (e) => setSelectedModel(e.target.value), disabled: isLoading, className: "px-3 py-1.5 text-sm border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-indigo-500 disabled:opacity-50", children: availableModels.length === 0 ? (_jsx("option", { value: "", children: "No models available" })) : (availableModels.map((model) => (_jsx("option", { value: model.id, title: model.description, children: model.name }, model.id)))) }), _jsx("button", { onClick: handleClearChat, className: "p-2 text-gray-500 hover:text-red-600 hover:bg-red-50 rounded-md transition-colors", title: "Clear chat", disabled: messages.length === 0 || isLoading, children: _jsx(Trash2, { className: "w-5 h-5" }) })] })] }), _jsxs("div", { className: "flex-1 overflow-y-auto p-4 space-y-4", children: [messages.length === 0 && (_jsxs("div", { className: "text-center text-gray-500 mt-8", children: [_jsx(Bot, { className: "w-12 h-12 mx-auto mb-4 text-gray-300" }), _jsx("p", { className: "mb-4", children: "Start a conversation with the AI!" }), _jsxs("div", { className: "text-sm space-y-2 max-w-md mx-auto text-left", children: [_jsx("p", { className: "font-medium", children: "Try asking:" }), _jsxs("ul", { className: "list-disc list-inside space-y-1 text-gray-600", children: [_jsx("li", { children: "Explain quantum computing in simple terms" }), _jsx("li", { children: "Write a Python function to sort a list" }), _jsx("li", { children: "What are the benefits of microservices?" }), _jsx("li", { children: "How does blockchain technology work?" })] })] })] })), messages.map((message, index) => (_jsxs("div", { className: `flex gap-3 ${message.role === 'user' ? 'justify-end' : 'justify-start'}`, children: [message.role === 'assistant' && (_jsx("div", { className: "bg-indigo-600 rounded-full p-2 h-8 w-8 flex items-center justify-center flex-shrink-0", children: _jsx(Bot, { className: "w-4 h-4 text-white" }) })), _jsx("div", { className: `max-w-[80%] rounded-lg p-4 ${message.role === 'user' ? 'bg-indigo-600 text-white' : 'bg-gray-100 text-gray-900'}`, children: message.content ? (message.role === 'assistant' ? (_jsx("div", { className: "prose prose-sm max-w-none prose-p:my-2 prose-ul:my-2 prose-ol:my-2 prose-li:my-0 prose-headings:my-2 prose-pre:my-2 prose-code:text-sm", children: _jsx(ReactMarkdown, { remarkPlugins: [remarkGfm], children: message.content }) })) : (_jsx("div", { className: "prose prose-sm prose-invert max-w-none prose-p:my-1 prose-p:leading-relaxed prose-ul:my-2 prose-ol:my-2 prose-li:my-0 prose-headings:my-2 prose-pre:my-2 prose-code:text-sm prose-code:text-indigo-100 prose-pre:bg-indigo-700/50 prose-a:text-indigo-200 prose-a:underline prose-strong:text-white", children: _jsx(ReactMarkdown, { remarkPlugins: [remarkGfm], children: message.content }) }))) : (_jsxs("div", { className: "flex items-center gap-2", children: [_jsx(Loader2, { className: "w-4 h-4 animate-spin" }), _jsx("span", { className: "text-sm", children: "Thinking..." })] })) }), message.role === 'user' && (_jsx("div", { className: "bg-gray-300 rounded-full p-2 h-8 w-8 flex items-center justify-center flex-shrink-0", children: _jsx(User, { className: "w-4 h-4 text-gray-700" }) }))] }, index))), _jsx("div", { ref: messagesEndRef })] }), _jsx("form", { onSubmit: handleSubmit, className: "p-4 border-t border-gray-200", children: _jsxs("div", { className: "flex gap-2 items-end", children: [_jsx("textarea", { ref: textareaRef, value: input, onChange: (e) => {
650
+ return (_jsxs("div", { className: `flex flex-col h-full min-h-0 bg-white dark:bg-gray-900 ${className}`, children: [_jsxs("div", { className: "flex-shrink-0 flex items-center gap-2 px-4 py-2 border-b border-gray-200 dark:border-gray-700 bg-gray-50 dark:bg-gray-800", children: [_jsx(Bot, { className: "w-5 h-5 text-blue-600 dark:text-blue-400" }), _jsx("div", { className: "flex-1 min-w-0", children: _jsx("h3", { className: "text-sm font-semibold text-gray-900 dark:text-gray-100 truncate", children: "AI Assistant" }) }), _jsxs("div", { className: "flex items-center gap-1", children: [_jsxs("button", { onClick: () => setShowVoiceComingSoon(true), className: "flex items-center gap-1.5 px-2.5 py-1.5 text-xs font-medium text-gray-600 dark:text-gray-300 hover:text-gray-900 dark:hover:text-white bg-white dark:bg-gray-700 hover:bg-gray-100 dark:hover:bg-gray-600 border border-gray-200 dark:border-gray-600 rounded-lg transition-colors", title: "Voice mode (coming soon)", children: [_jsx(Volume2, { className: "w-3.5 h-3.5" }), _jsx("span", { children: "Voice" })] }), _jsxs("button", { onClick: handleNewChat, className: "flex items-center gap-1.5 px-2.5 py-1.5 text-xs font-medium text-gray-600 dark:text-gray-300 hover:text-gray-900 dark:hover:text-white bg-white dark:bg-gray-700 hover:bg-gray-100 dark:hover:bg-gray-600 border border-gray-200 dark:border-gray-600 rounded-lg transition-colors", title: "Start new chat", children: [_jsx(Plus, { className: "w-3.5 h-3.5" }), _jsx("span", { children: "New Chat" })] }), conversationId && (_jsx("button", { onClick: handleDeleteConversation, className: "flex items-center gap-1.5 p-1.5 text-xs font-medium text-gray-500 dark:text-gray-400 hover:text-red-600 dark:hover:text-red-400 hover:bg-red-50 dark:hover:bg-red-900/20 rounded-lg transition-colors", title: "Delete this conversation", children: _jsx(Trash2, { className: "w-4 h-4" }) }))] })] }), _jsxs("div", { className: "flex-1 min-h-0 overflow-y-auto p-4 space-y-4 bg-white dark:bg-gray-900", children: [loadingHistory && (_jsxs("div", { className: "flex items-center justify-center py-8", children: [_jsx(Loader2, { className: "w-8 h-8 animate-spin text-blue-600 dark:text-blue-400" }), _jsx("span", { className: "ml-3 text-gray-600 dark:text-gray-400", children: "Loading conversation..." })] })), !loadingHistory && (_jsx(MessageList, { messages: (() => {
651
+ // Build messages array, optionally including welcome message
652
+ const displayMessages = messages.map(m => ({
653
+ id: m.id || `msg-${Math.random()}`,
654
+ role: m.role,
655
+ content: m.content,
656
+ createdAt: m.createdAt || new Date(),
657
+ agentName: m.agentName,
658
+ thoughts: m.thoughts,
659
+ parts: m.parts,
660
+ }));
661
+ // Add welcome message as first assistant message if no messages yet
662
+ if (welcomeMessage && displayMessages.length === 0) {
663
+ displayMessages.unshift({
664
+ id: 'welcome',
665
+ role: 'assistant',
666
+ content: welcomeMessage,
667
+ createdAt: new Date(),
668
+ agentName: undefined,
669
+ thoughts: undefined,
670
+ parts: undefined,
671
+ });
672
+ }
673
+ return displayMessages;
674
+ })(), streamingContent: !useAgenticStreaming ? streamingContent : undefined, streamingAgentName: streamingAgentName, isLoading: !useAgenticStreaming && isLoading && messages.length === 0, onDeleteMessage: handleDeleteMessage, onRetryMessage: handleRetryMessage, onSuggestedAction: (action) => handleSubmit(null, action) })), useAgenticStreaming && (isLoading || streamingContent) && !promptActive && (_jsxs("div", { className: "flex gap-4 justify-start", children: [_jsxs("div", { className: "flex flex-col items-center gap-1", children: [_jsx("div", { className: "w-8 h-8 rounded-full bg-gradient-to-br from-blue-500 to-purple-600 flex items-center justify-center flex-shrink-0 text-white font-semibold text-sm", children: streamingAgentName?.charAt(0).toUpperCase() || 'A' }), streamingAgentName && (_jsx("span", { className: "text-[10px] text-gray-500 dark:text-gray-400 font-medium capitalize", children: streamingAgentName }))] }), _jsx("div", { className: "max-w-3xl flex-1", children: _jsxs("div", { className: "rounded-lg px-4 py-3 bg-gray-100 dark:bg-gray-800 text-gray-900 dark:text-gray-100", children: [thoughts.length > 0 && (_jsx("div", { className: "flex flex-wrap gap-2 mb-2 text-xs", children: _jsx(ThinkingToggle, { thoughts: thoughts, isActive: isLoading && !streamingContent }) })), streamingParts.filter(p => p.type === 'tool_call').length > 0 && (_jsx("div", { className: "mb-2", children: streamingParts
675
+ .filter((p) => p.type === 'tool_call')
676
+ .map((part) => (_jsx(StreamingToolCard, { part: part }, part.id))) })), interimMessages.length > 0 && (_jsx("div", { className: "mb-3 space-y-1", children: interimMessages.map((msg, idx) => (_jsx("div", { className: "text-xs rounded border border-amber-200 dark:border-amber-800 bg-amber-50 dark:bg-amber-900/20 px-2 py-1 text-amber-800 dark:text-amber-200", children: msg }, `interim-${idx}`))) })), streamingContent ? (_jsxs("div", { className: "prose prose-sm dark:prose-invert max-w-none prose-headings:font-semibold prose-h1:text-xl prose-h1:mt-4 prose-h1:mb-3 prose-h2:text-lg prose-h2:mt-4 prose-h2:mb-2 prose-h3:text-base prose-h3:mt-3 prose-h3:mb-2 prose-p:my-2 prose-p:leading-relaxed prose-ul:my-3 prose-ol:my-3 prose-li:my-0.5 prose-hr:my-6 prose-strong:font-semibold prose-code:px-1 prose-code:py-0.5 prose-code:rounded prose-code:text-sm prose-pre:border prose-blockquote:border-l-4 prose-blockquote:pl-4 prose-blockquote:italic prose-a:text-blue-600 dark:prose-a:text-blue-400 prose-a:no-underline hover:prose-a:underline", children: [_jsx(ReactMarkdown, { remarkPlugins: [remarkGfm, remarkMath], rehypePlugins: [rehypeKatex], components: streamingMarkdownComponents, children: preprocessLatex(streamingContent) }), _jsx("span", { className: "inline-block w-2 h-4 bg-gray-400 dark:bg-gray-500 animate-pulse ml-1" })] })) : (_jsxs("div", { className: "flex items-center gap-2 text-gray-500 dark:text-gray-400", children: [_jsx(Loader2, { className: "w-4 h-4 animate-spin" }), _jsx("span", { children: "Thinking..." })] }))] }) })] })), !useAgenticStreaming && isLoading && executionEvents.length > 0 && !streamingContent && (_jsx(ExecutionStatus, { events: executionEvents, isActive: isLoading })), !useAgenticStreaming && isLoading && !streamingContent && executionEvents.length === 0 && (_jsxs("div", { className: "flex gap-3 justify-start", children: [_jsx("div", { className: "bg-blue-600 dark:bg-blue-500 rounded-full p-2 h-8 w-8 flex items-center justify-center flex-shrink-0", children: _jsx(Bot, { className: "w-4 h-4 text-white" }) }), _jsx("div", { className: "max-w-[80%] rounded-lg p-4 bg-gray-100 dark:bg-gray-800 text-gray-900 dark:text-gray-100", children: _jsxs("div", { className: "flex items-center gap-2", children: [_jsx(Loader2, { className: "w-4 h-4 animate-spin" }), _jsx("span", { className: "text-sm", children: "Thinking..." })] }) })] })), _jsx("div", { ref: messagesEndRef })] }), attachments.length > 0 && (_jsx("div", { className: "flex-shrink-0 px-4 py-2 border-t border-gray-200 dark:border-gray-700 bg-gray-50 dark:bg-gray-800", children: _jsx("div", { className: "flex flex-wrap gap-2", children: attachments.map((attachment, index) => (_jsxs("div", { className: "flex items-center gap-2 bg-white dark:bg-gray-700 px-3 py-1.5 rounded-lg border border-gray-200 dark:border-gray-600", children: [_jsx("span", { className: "text-sm text-gray-700 dark:text-gray-200", children: attachment.name }), _jsx("button", { onClick: () => handleRemoveAttachment(index), className: "text-gray-400 dark:text-gray-500 hover:text-red-600 dark:hover:text-red-400", children: "\u00D7" })] }, index))) }) })), 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: () => handleQuickReply(reply), 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("form", { onSubmit: handleSubmit, className: "flex-shrink-0 p-3 border-t border-gray-200 dark:border-gray-700 bg-white dark:bg-gray-900", children: _jsxs("div", { className: "flex gap-2 items-end", children: [allowAttachments && (_jsxs(_Fragment, { children: [_jsx("input", { ref: fileInputRef, type: "file", multiple: true, onChange: handleFileSelect, className: "hidden" }), _jsx("button", { type: "button", onClick: () => fileInputRef.current?.click(), disabled: isLoading, className: "p-2 text-gray-500 dark:text-gray-400 hover:text-gray-700 dark:hover:text-gray-200 disabled:opacity-50 disabled:cursor-not-allowed flex-shrink-0", title: "Attach files", children: _jsx(Paperclip, { className: "w-5 h-5" }) })] })), _jsx("textarea", { ref: textareaRef, value: input, onChange: (e) => {
115
677
  setInput(e.target.value);
116
678
  if (textareaRef.current) {
117
679
  textareaRef.current.style.height = 'auto';
@@ -120,10 +682,10 @@ export function ChatInterface({ availableModels, nextChatPath = '/api/chat', ser
120
682
  }, onKeyDown: (e) => {
121
683
  if (e.key === 'Enter' && !e.shiftKey) {
122
684
  e.preventDefault();
123
- if (input.trim() && !isLoading) {
685
+ if (input.trim() && (!isLoading || promptActive)) {
124
686
  handleSubmit(e);
125
687
  }
126
688
  }
127
- }, placeholder: "Ask a question...", rows: 1, className: "flex-1 px-4 py-3 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:border-transparent resize-none", style: { minHeight: '52px', maxHeight: '200px' }, disabled: isLoading }), _jsx("button", { type: "submit", disabled: isLoading || !input.trim(), className: "bg-indigo-600 hover:bg-indigo-700 disabled:bg-gray-400 text-white px-6 py-3 rounded-lg transition-colors flex items-center gap-2 font-medium disabled:cursor-not-allowed flex-shrink-0", children: isLoading ? (_jsx(Loader2, { className: "w-5 h-5 animate-spin" })) : (_jsxs(_Fragment, { children: [_jsx(Send, { className: "w-5 h-5" }), "Send"] })) })] }) })] }));
689
+ }, placeholder: promptActive ? 'Type your reply...' : placeholder, rows: 1, className: "flex-1 px-4 py-2 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-800 text-gray-900 dark:text-gray-100 placeholder-gray-400 dark:placeholder-gray-500 focus:outline-none focus:ring-2 focus:ring-blue-500 dark:focus:ring-blue-400 focus:border-transparent resize-none", style: { minHeight: '40px', maxHeight: '200px' }, disabled: isLoading && !promptActive }), isLoading && !promptActive ? (_jsx("button", { type: "button", onClick: handleCancel, className: "bg-red-600 dark:bg-red-500 hover:bg-red-700 dark:hover:bg-red-600 text-white px-4 py-2 rounded-lg transition-colors flex items-center gap-2 font-medium flex-shrink-0", title: "Cancel", children: _jsx("span", { className: "w-5 h-5 flex items-center justify-center", children: "\u23F9" }) })) : (_jsx("button", { type: "submit", disabled: !input.trim(), className: "bg-blue-600 dark:bg-blue-500 hover:bg-blue-700 dark:hover:bg-blue-600 disabled:bg-gray-400 dark:disabled:bg-gray-600 text-white px-4 py-2 rounded-lg transition-colors flex items-center gap-2 font-medium disabled:cursor-not-allowed flex-shrink-0", children: _jsx(Send, { className: "w-5 h-5" }) }))] }) }), showVoiceComingSoon && (_jsx("div", { className: "fixed inset-0 z-50 flex items-center justify-center bg-black/50 px-4", children: _jsxs("div", { className: "w-full max-w-sm rounded-xl bg-white dark:bg-gray-800 border border-gray-200 dark:border-gray-700 shadow-xl p-4", children: [_jsxs("div", { className: "flex items-start justify-between gap-3", children: [_jsxs("div", { children: [_jsx("h4", { className: "text-sm font-semibold text-gray-900 dark:text-gray-100", children: "Voice Mode" }), _jsx("p", { className: "mt-2 text-sm text-gray-600 dark:text-gray-300", children: "Voice mode is coming soon. Chat remains text-only for now." })] }), _jsx("button", { type: "button", onClick: () => setShowVoiceComingSoon(false), className: "p-1 rounded-md text-gray-500 dark:text-gray-400 hover:bg-gray-100 dark:hover:bg-gray-700", "aria-label": "Close voice mode modal", children: _jsx(X, { className: "w-4 h-4" }) })] }), _jsx("div", { className: "mt-4 flex justify-end", children: _jsx("button", { type: "button", onClick: () => setShowVoiceComingSoon(false), className: "px-3 py-1.5 text-sm font-medium rounded-lg bg-blue-600 text-white hover:bg-blue-700 dark:bg-blue-500 dark:hover:bg-blue-600", children: "Got it" }) })] }) }))] }));
128
690
  }
129
691
  //# sourceMappingURL=ChatInterface.js.map