@jazzmind/busibox-app 3.0.29 → 3.0.30
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/components/chat/ChatContainer.d.ts.map +1 -1
- package/dist/components/chat/ChatContainer.js +155 -14
- package/dist/components/chat/ChatContainer.js.map +1 -1
- package/dist/components/chat/ChatInterface.d.ts +37 -14
- package/dist/components/chat/ChatInterface.d.ts.map +1 -1
- package/dist/components/chat/ChatInterface.js +645 -83
- package/dist/components/chat/ChatInterface.js.map +1 -1
- package/dist/components/chat/FullChatInterface.d.ts.map +1 -1
- package/dist/components/chat/FullChatInterface.js +4 -0
- package/dist/components/chat/FullChatInterface.js.map +1 -1
- package/dist/components/chat/LegacyChatInterface.d.ts +16 -0
- package/dist/components/chat/LegacyChatInterface.d.ts.map +1 -0
- package/dist/components/chat/LegacyChatInterface.js +134 -0
- package/dist/components/chat/LegacyChatInterface.js.map +1 -0
- package/dist/components/chat/MessageList.d.ts +4 -1
- package/dist/components/chat/MessageList.d.ts.map +1 -1
- package/dist/components/chat/MessageList.js +64 -2
- package/dist/components/chat/MessageList.js.map +1 -1
- package/dist/components/chat/SimpleChatInterface.d.ts +9 -38
- package/dist/components/chat/SimpleChatInterface.d.ts.map +1 -1
- package/dist/components/chat/SimpleChatInterface.js +1 -596
- package/dist/components/chat/SimpleChatInterface.js.map +1 -1
- package/dist/components/chat/StreamingToolCard.d.ts +11 -0
- package/dist/components/chat/StreamingToolCard.d.ts.map +1 -0
- package/dist/components/chat/StreamingToolCard.js +20 -0
- package/dist/components/chat/StreamingToolCard.js.map +1 -0
- package/dist/components/chat/ThinkingToggle.d.ts +4 -5
- package/dist/components/chat/ThinkingToggle.d.ts.map +1 -1
- package/dist/components/chat/ThinkingToggle.js +62 -12
- package/dist/components/chat/ThinkingToggle.js.map +1 -1
- package/dist/components/chat/chat-utils.d.ts +297 -0
- package/dist/components/chat/chat-utils.d.ts.map +1 -0
- package/dist/components/chat/chat-utils.js +43 -0
- package/dist/components/chat/chat-utils.js.map +1 -0
- package/dist/components/index.d.ts +5 -0
- package/dist/components/index.d.ts.map +1 -1
- package/dist/components/index.js +6 -1
- package/dist/components/index.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/lib/agent/chat-client.d.ts.map +1 -1
- package/dist/lib/agent/chat-client.js +7 -1
- package/dist/lib/agent/chat-client.js.map +1 -1
- package/dist/types/chat.d.ts +24 -0
- package/dist/types/chat.d.ts.map +1 -1
- package/package.json +1 -1
|
@@ -1,117 +1,679 @@
|
|
|
1
1
|
'use client';
|
|
2
|
-
import { jsx as _jsx,
|
|
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,
|
|
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
|
|
8
|
-
import
|
|
9
|
-
|
|
10
|
-
|
|
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 [
|
|
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
|
-
|
|
24
|
-
|
|
25
|
-
if (
|
|
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
|
-
|
|
28
|
-
|
|
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
|
-
|
|
155
|
+
setQuickReplies([]);
|
|
156
|
+
// Create abort controller for cancellation
|
|
157
|
+
const controller = new AbortController();
|
|
158
|
+
setAbortController(controller);
|
|
35
159
|
try {
|
|
36
|
-
const
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
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
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
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
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
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:
|
|
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
|
|
111
|
-
if (
|
|
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:
|
|
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:
|
|
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
|