nitrostack 1.0.1 → 1.0.2
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/CHANGELOG.md +15 -0
- package/dist/cli/index.js +4 -1
- package/dist/cli/index.js.map +1 -1
- package/package.json +1 -1
- package/src/studio/README.md +140 -0
- package/src/studio/app/api/auth/fetch-metadata/route.ts +71 -0
- package/src/studio/app/api/auth/register-client/route.ts +67 -0
- package/src/studio/app/api/chat/route.ts +123 -0
- package/src/studio/app/api/health/checks/route.ts +42 -0
- package/src/studio/app/api/health/route.ts +13 -0
- package/src/studio/app/api/init/route.ts +85 -0
- package/src/studio/app/api/ping/route.ts +13 -0
- package/src/studio/app/api/prompts/[name]/route.ts +21 -0
- package/src/studio/app/api/prompts/route.ts +13 -0
- package/src/studio/app/api/resources/[...uri]/route.ts +18 -0
- package/src/studio/app/api/resources/route.ts +13 -0
- package/src/studio/app/api/roots/route.ts +13 -0
- package/src/studio/app/api/sampling/route.ts +14 -0
- package/src/studio/app/api/tools/[name]/call/route.ts +41 -0
- package/src/studio/app/api/tools/route.ts +23 -0
- package/src/studio/app/api/widget-examples/route.ts +44 -0
- package/src/studio/app/auth/callback/page.tsx +160 -0
- package/src/studio/app/auth/page.tsx +543 -0
- package/src/studio/app/chat/page.tsx +530 -0
- package/src/studio/app/chat/page.tsx.backup +390 -0
- package/src/studio/app/globals.css +410 -0
- package/src/studio/app/health/page.tsx +177 -0
- package/src/studio/app/layout.tsx +48 -0
- package/src/studio/app/page.tsx +337 -0
- package/src/studio/app/page.tsx.backup +346 -0
- package/src/studio/app/ping/page.tsx +204 -0
- package/src/studio/app/prompts/page.tsx +228 -0
- package/src/studio/app/resources/page.tsx +313 -0
- package/src/studio/components/EnlargeModal.tsx +116 -0
- package/src/studio/components/Sidebar.tsx +133 -0
- package/src/studio/components/ToolCard.tsx +108 -0
- package/src/studio/components/WidgetRenderer.tsx +99 -0
- package/src/studio/lib/api.ts +207 -0
- package/src/studio/lib/llm-service.ts +361 -0
- package/src/studio/lib/mcp-client.ts +168 -0
- package/src/studio/lib/store.ts +192 -0
- package/src/studio/lib/theme-provider.tsx +50 -0
- package/src/studio/lib/types.ts +107 -0
- package/src/studio/lib/widget-loader.ts +90 -0
- package/src/studio/middleware.ts +27 -0
- package/src/studio/next.config.js +16 -0
- package/src/studio/package-lock.json +2696 -0
- package/src/studio/package.json +34 -0
- package/src/studio/postcss.config.mjs +10 -0
- package/src/studio/tailwind.config.ts +67 -0
- package/src/studio/tsconfig.json +41 -0
|
@@ -0,0 +1,530 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { useEffect, useRef, useState } from 'react';
|
|
4
|
+
import { useStudioStore } from '@/lib/store';
|
|
5
|
+
import { api } from '@/lib/api';
|
|
6
|
+
import { WidgetRenderer } from '@/components/WidgetRenderer';
|
|
7
|
+
import type { ChatMessage, Tool, ToolCall } from '@/lib/types';
|
|
8
|
+
import {
|
|
9
|
+
Bot,
|
|
10
|
+
Settings,
|
|
11
|
+
Trash2,
|
|
12
|
+
Image as ImageIcon,
|
|
13
|
+
Send,
|
|
14
|
+
Wrench,
|
|
15
|
+
Save,
|
|
16
|
+
X
|
|
17
|
+
} from 'lucide-react';
|
|
18
|
+
|
|
19
|
+
export default function ChatPage() {
|
|
20
|
+
const {
|
|
21
|
+
chatMessages,
|
|
22
|
+
addChatMessage,
|
|
23
|
+
clearChat,
|
|
24
|
+
currentProvider,
|
|
25
|
+
setCurrentProvider,
|
|
26
|
+
currentImage,
|
|
27
|
+
setCurrentImage,
|
|
28
|
+
jwtToken,
|
|
29
|
+
apiKey: mcpApiKey, // Rename to avoid conflict with LLM apiKey
|
|
30
|
+
tools,
|
|
31
|
+
setTools,
|
|
32
|
+
} = useStudioStore();
|
|
33
|
+
|
|
34
|
+
const [inputValue, setInputValue] = useState('');
|
|
35
|
+
const [loading, setLoading] = useState(false);
|
|
36
|
+
const [showSettings, setShowSettings] = useState(false);
|
|
37
|
+
const messagesEndRef = useRef<HTMLDivElement>(null);
|
|
38
|
+
const fileInputRef = useRef<HTMLInputElement>(null);
|
|
39
|
+
|
|
40
|
+
useEffect(() => {
|
|
41
|
+
loadTools();
|
|
42
|
+
}, []);
|
|
43
|
+
|
|
44
|
+
useEffect(() => {
|
|
45
|
+
messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' });
|
|
46
|
+
}, [chatMessages]);
|
|
47
|
+
|
|
48
|
+
const loadTools = async () => {
|
|
49
|
+
try {
|
|
50
|
+
const data = await api.getTools();
|
|
51
|
+
setTools(data.tools || []);
|
|
52
|
+
} catch (error) {
|
|
53
|
+
console.error('Failed to load tools:', error);
|
|
54
|
+
}
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
const handleImageUpload = (e: React.ChangeEvent<HTMLInputElement>) => {
|
|
58
|
+
const file = e.target.files?.[0];
|
|
59
|
+
if (!file) return;
|
|
60
|
+
|
|
61
|
+
if (file.size > 20 * 1024 * 1024) {
|
|
62
|
+
alert('Image too large (max 20MB)');
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const reader = new FileReader();
|
|
67
|
+
reader.onload = (event) => {
|
|
68
|
+
setCurrentImage({
|
|
69
|
+
data: event.target?.result as string,
|
|
70
|
+
type: file.type,
|
|
71
|
+
name: file.name,
|
|
72
|
+
});
|
|
73
|
+
};
|
|
74
|
+
reader.readAsDataURL(file);
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
const handleSend = async () => {
|
|
78
|
+
if (!inputValue.trim() && !currentImage) return;
|
|
79
|
+
|
|
80
|
+
const apiKey = localStorage.getItem(`${currentProvider}_api_key`);
|
|
81
|
+
if (!apiKey) {
|
|
82
|
+
setShowSettings(true);
|
|
83
|
+
alert('Please set your API key first');
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const userMessage: ChatMessage = {
|
|
88
|
+
role: 'user',
|
|
89
|
+
content: inputValue,
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
if (currentImage) {
|
|
93
|
+
userMessage.image = currentImage;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
addChatMessage(userMessage);
|
|
97
|
+
setInputValue('');
|
|
98
|
+
setCurrentImage(null);
|
|
99
|
+
setLoading(true);
|
|
100
|
+
|
|
101
|
+
try {
|
|
102
|
+
const messagesToSend = [...chatMessages, userMessage];
|
|
103
|
+
|
|
104
|
+
// Clean messages to ensure they're serializable
|
|
105
|
+
const cleanedMessages = messagesToSend.map(msg => {
|
|
106
|
+
const cleaned: any = {
|
|
107
|
+
role: msg.role,
|
|
108
|
+
content: msg.content || '',
|
|
109
|
+
};
|
|
110
|
+
|
|
111
|
+
if (msg.toolCalls && msg.toolCalls.length > 0) {
|
|
112
|
+
cleaned.toolCalls = msg.toolCalls;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
if (msg.toolCallId) {
|
|
116
|
+
cleaned.toolCallId = msg.toolCallId;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// Skip image property for now (not supported by OpenAI chat completions)
|
|
120
|
+
// if (msg.image) {
|
|
121
|
+
// cleaned.image = msg.image;
|
|
122
|
+
// }
|
|
123
|
+
|
|
124
|
+
return cleaned;
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
console.log('Sending messages to API:', cleanedMessages);
|
|
128
|
+
console.log('Original messages:', messagesToSend);
|
|
129
|
+
console.log('Cleaned messages JSON:', JSON.stringify(cleanedMessages));
|
|
130
|
+
|
|
131
|
+
const response = await api.chat({
|
|
132
|
+
provider: currentProvider,
|
|
133
|
+
messages: cleanedMessages,
|
|
134
|
+
apiKey, // LLM API key (OpenAI/Gemini)
|
|
135
|
+
jwtToken: jwtToken || undefined,
|
|
136
|
+
mcpApiKey: mcpApiKey || undefined, // MCP server API key
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
// Handle tool calls FIRST (before adding the message)
|
|
140
|
+
if (response.toolCalls && response.toolResults) {
|
|
141
|
+
// Attach results to tool calls for widget rendering
|
|
142
|
+
const toolCallsWithResults = response.toolCalls.map((tc, i) => {
|
|
143
|
+
const toolResult = response.toolResults[i];
|
|
144
|
+
|
|
145
|
+
// Parse the result content
|
|
146
|
+
let parsedResult;
|
|
147
|
+
if (toolResult.content) {
|
|
148
|
+
try {
|
|
149
|
+
parsedResult = JSON.parse(toolResult.content);
|
|
150
|
+
|
|
151
|
+
// Unwrap if response was wrapped by TransformInterceptor
|
|
152
|
+
if (parsedResult.success !== undefined && parsedResult.data !== undefined) {
|
|
153
|
+
parsedResult = parsedResult.data;
|
|
154
|
+
}
|
|
155
|
+
} catch {
|
|
156
|
+
parsedResult = { content: toolResult.content };
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
return {
|
|
161
|
+
...tc,
|
|
162
|
+
result: parsedResult,
|
|
163
|
+
};
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
// Add assistant message with tool calls
|
|
167
|
+
if (response.message) {
|
|
168
|
+
response.message.toolCalls = toolCallsWithResults;
|
|
169
|
+
addChatMessage(response.message);
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
// Extract JWT token from ANY tool response (not just 'login')
|
|
173
|
+
for (let i = 0; i < response.toolCalls.length; i++) {
|
|
174
|
+
const toolCall = response.toolCalls[i];
|
|
175
|
+
const toolResult = response.toolResults[i];
|
|
176
|
+
|
|
177
|
+
if (toolResult.content) {
|
|
178
|
+
try {
|
|
179
|
+
const parsed = JSON.parse(toolResult.content);
|
|
180
|
+
// Check for token in multiple possible locations
|
|
181
|
+
const token = parsed.token || parsed.access_token || parsed.jwt || parsed.data?.token;
|
|
182
|
+
if (token) {
|
|
183
|
+
console.log('🔐 Token received from tool in chat, saving to global state');
|
|
184
|
+
useStudioStore.getState().setJwtToken(token);
|
|
185
|
+
break; // Stop after first token found
|
|
186
|
+
}
|
|
187
|
+
} catch (e) {
|
|
188
|
+
// Ignore parsing errors
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
// Add tool results to messages
|
|
194
|
+
const toolResultMessages: ChatMessage[] = [];
|
|
195
|
+
for (const result of response.toolResults) {
|
|
196
|
+
addChatMessage(result);
|
|
197
|
+
toolResultMessages.push(result);
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
// Continue conversation and WAIT for it to complete before allowing new messages
|
|
201
|
+
// Build the full message history to pass to continuation
|
|
202
|
+
const messagesForContinuation = [
|
|
203
|
+
...chatMessages,
|
|
204
|
+
response.message!, // Assistant message with tool calls
|
|
205
|
+
...toolResultMessages, // Tool result messages
|
|
206
|
+
];
|
|
207
|
+
await continueChatWithToolResults(apiKey, messagesForContinuation);
|
|
208
|
+
} else {
|
|
209
|
+
// No tool calls, just add the message
|
|
210
|
+
if (response.message) {
|
|
211
|
+
addChatMessage(response.message);
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
// Set loading to false AFTER all async operations complete
|
|
216
|
+
setLoading(false);
|
|
217
|
+
} catch (error) {
|
|
218
|
+
console.error('Chat error:', error);
|
|
219
|
+
addChatMessage({
|
|
220
|
+
role: 'assistant',
|
|
221
|
+
content: 'Sorry, I encountered an error. Please try again.',
|
|
222
|
+
});
|
|
223
|
+
setLoading(false);
|
|
224
|
+
}
|
|
225
|
+
};
|
|
226
|
+
|
|
227
|
+
const continueChatWithToolResults = async (apiKey: string, messages?: Message[]) => {
|
|
228
|
+
try {
|
|
229
|
+
// Use provided messages or fall back to store (for recursive calls)
|
|
230
|
+
const messagesToUse = messages || chatMessages;
|
|
231
|
+
|
|
232
|
+
// Clean messages before sending
|
|
233
|
+
const cleanedMessages = messagesToUse.map(msg => {
|
|
234
|
+
const cleaned: any = {
|
|
235
|
+
role: msg.role,
|
|
236
|
+
content: msg.content || '',
|
|
237
|
+
};
|
|
238
|
+
|
|
239
|
+
if (msg.toolCalls && msg.toolCalls.length > 0) {
|
|
240
|
+
cleaned.toolCalls = msg.toolCalls;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
if (msg.toolCallId) {
|
|
244
|
+
cleaned.toolCallId = msg.toolCallId;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
return cleaned;
|
|
248
|
+
});
|
|
249
|
+
|
|
250
|
+
console.log('Continue with cleaned messages:', JSON.stringify(cleanedMessages));
|
|
251
|
+
|
|
252
|
+
const response = await api.chat({
|
|
253
|
+
provider: currentProvider,
|
|
254
|
+
messages: cleanedMessages,
|
|
255
|
+
apiKey, // LLM API key (OpenAI/Gemini)
|
|
256
|
+
jwtToken: jwtToken || undefined,
|
|
257
|
+
mcpApiKey: mcpApiKey || undefined, // MCP server API key
|
|
258
|
+
});
|
|
259
|
+
|
|
260
|
+
if (response.message) {
|
|
261
|
+
addChatMessage(response.message);
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
// Recursive tool calls
|
|
265
|
+
if (response.toolCalls && response.toolResults) {
|
|
266
|
+
const newToolResults: Message[] = [];
|
|
267
|
+
for (const result of response.toolResults) {
|
|
268
|
+
addChatMessage(result);
|
|
269
|
+
newToolResults.push(result);
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
// Build messages for next iteration
|
|
273
|
+
const nextMessages = [
|
|
274
|
+
...(messages || chatMessages),
|
|
275
|
+
response.message!,
|
|
276
|
+
...newToolResults,
|
|
277
|
+
];
|
|
278
|
+
await continueChatWithToolResults(apiKey, nextMessages);
|
|
279
|
+
}
|
|
280
|
+
} catch (error) {
|
|
281
|
+
console.error('Continuation error:', error);
|
|
282
|
+
}
|
|
283
|
+
};
|
|
284
|
+
|
|
285
|
+
const saveApiKey = (provider: 'openai' | 'gemini') => {
|
|
286
|
+
const input = document.getElementById(`${provider}-api-key`) as HTMLInputElement;
|
|
287
|
+
const key = input?.value.trim();
|
|
288
|
+
|
|
289
|
+
if (!key || key === '••••••••') {
|
|
290
|
+
alert('Please enter a valid API key');
|
|
291
|
+
return;
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
localStorage.setItem(`${provider}_api_key`, key);
|
|
295
|
+
input.value = '••••••••';
|
|
296
|
+
alert(`${provider === 'openai' ? 'OpenAI' : 'Gemini'} API key saved`);
|
|
297
|
+
};
|
|
298
|
+
|
|
299
|
+
return (
|
|
300
|
+
<div className="h-screen flex flex-col bg-background">
|
|
301
|
+
{/* Header */}
|
|
302
|
+
<div className="border-b border-border p-6 flex items-center justify-between bg-card">
|
|
303
|
+
<div className="flex items-center gap-3">
|
|
304
|
+
<div className="w-10 h-10 rounded-lg bg-gradient-to-br from-primary to-amber-500 flex items-center justify-center">
|
|
305
|
+
<Bot className="w-6 h-6 text-black" />
|
|
306
|
+
</div>
|
|
307
|
+
<div>
|
|
308
|
+
<h1 className="text-2xl font-bold text-foreground">AI Chat</h1>
|
|
309
|
+
<p className="text-muted-foreground text-sm">Chat with tools integration</p>
|
|
310
|
+
</div>
|
|
311
|
+
</div>
|
|
312
|
+
|
|
313
|
+
<div className="flex items-center gap-2">
|
|
314
|
+
<select
|
|
315
|
+
value={currentProvider}
|
|
316
|
+
onChange={(e) => setCurrentProvider(e.target.value as 'openai' | 'gemini')}
|
|
317
|
+
className="input w-36"
|
|
318
|
+
>
|
|
319
|
+
<option value="gemini">Gemini</option>
|
|
320
|
+
<option value="openai">OpenAI</option>
|
|
321
|
+
</select>
|
|
322
|
+
<button
|
|
323
|
+
onClick={() => setShowSettings(!showSettings)}
|
|
324
|
+
className={`btn ${showSettings ? 'btn-primary' : 'btn-secondary'}`}
|
|
325
|
+
>
|
|
326
|
+
<Settings className="w-4 h-4" />
|
|
327
|
+
</button>
|
|
328
|
+
<button onClick={clearChat} className="btn btn-secondary">
|
|
329
|
+
<Trash2 className="w-4 h-4" />
|
|
330
|
+
</button>
|
|
331
|
+
</div>
|
|
332
|
+
</div>
|
|
333
|
+
|
|
334
|
+
{/* Settings Panel */}
|
|
335
|
+
{showSettings && (
|
|
336
|
+
<div className="border-b border-border p-6 bg-muted/30">
|
|
337
|
+
<h3 className="font-semibold mb-4 text-foreground flex items-center gap-2">
|
|
338
|
+
<Settings className="w-5 h-5" />
|
|
339
|
+
API Keys
|
|
340
|
+
</h3>
|
|
341
|
+
<div className="grid grid-cols-2 gap-4">
|
|
342
|
+
<div>
|
|
343
|
+
<label className="text-sm mb-2 block text-foreground font-medium">OpenAI API Key</label>
|
|
344
|
+
<div className="flex gap-2">
|
|
345
|
+
<input
|
|
346
|
+
id="openai-api-key"
|
|
347
|
+
type="password"
|
|
348
|
+
className="input flex-1"
|
|
349
|
+
placeholder="sk-..."
|
|
350
|
+
/>
|
|
351
|
+
<button onClick={() => saveApiKey('openai')} className="btn btn-primary">
|
|
352
|
+
<Save className="w-4 h-4 mr-2" />
|
|
353
|
+
Save
|
|
354
|
+
</button>
|
|
355
|
+
</div>
|
|
356
|
+
</div>
|
|
357
|
+
<div>
|
|
358
|
+
<label className="text-sm mb-2 block text-foreground font-medium">Gemini API Key</label>
|
|
359
|
+
<div className="flex gap-2">
|
|
360
|
+
<input
|
|
361
|
+
id="gemini-api-key"
|
|
362
|
+
type="password"
|
|
363
|
+
className="input flex-1"
|
|
364
|
+
placeholder="AIza..."
|
|
365
|
+
/>
|
|
366
|
+
<button onClick={() => saveApiKey('gemini')} className="btn btn-primary">
|
|
367
|
+
<Save className="w-4 h-4 mr-2" />
|
|
368
|
+
Save
|
|
369
|
+
</button>
|
|
370
|
+
</div>
|
|
371
|
+
</div>
|
|
372
|
+
</div>
|
|
373
|
+
</div>
|
|
374
|
+
)}
|
|
375
|
+
|
|
376
|
+
{/* Messages */}
|
|
377
|
+
<div className="flex-1 overflow-y-auto p-4 space-y-4">
|
|
378
|
+
{chatMessages.map((msg, idx) => (
|
|
379
|
+
<ChatMessageComponent key={idx} message={msg} tools={tools} />
|
|
380
|
+
))}
|
|
381
|
+
{loading && (
|
|
382
|
+
<div className="flex items-center gap-2 text-text-secondary">
|
|
383
|
+
<div className="animate-pulse">Thinking...</div>
|
|
384
|
+
</div>
|
|
385
|
+
)}
|
|
386
|
+
<div ref={messagesEndRef} />
|
|
387
|
+
</div>
|
|
388
|
+
|
|
389
|
+
{/* Input */}
|
|
390
|
+
<div className="border-t border-border p-6 bg-card">
|
|
391
|
+
{currentImage && (
|
|
392
|
+
<div className="mb-3 relative inline-block">
|
|
393
|
+
<img
|
|
394
|
+
src={currentImage.data}
|
|
395
|
+
alt="Upload preview"
|
|
396
|
+
className="h-24 rounded-lg border border-border shadow-sm"
|
|
397
|
+
/>
|
|
398
|
+
<button
|
|
399
|
+
onClick={() => setCurrentImage(null)}
|
|
400
|
+
className="absolute -top-2 -right-2 bg-destructive text-white rounded-full w-7 h-7 flex items-center justify-center hover:bg-destructive/90 transition-colors shadow-md"
|
|
401
|
+
>
|
|
402
|
+
<X className="w-4 h-4" />
|
|
403
|
+
</button>
|
|
404
|
+
</div>
|
|
405
|
+
)}
|
|
406
|
+
<div className="flex gap-3">
|
|
407
|
+
<input
|
|
408
|
+
type="file"
|
|
409
|
+
ref={fileInputRef}
|
|
410
|
+
onChange={handleImageUpload}
|
|
411
|
+
accept="image/*"
|
|
412
|
+
className="hidden"
|
|
413
|
+
/>
|
|
414
|
+
<button
|
|
415
|
+
onClick={() => fileInputRef.current?.click()}
|
|
416
|
+
className="btn btn-secondary"
|
|
417
|
+
title="Upload image"
|
|
418
|
+
>
|
|
419
|
+
<ImageIcon className="w-5 h-5" />
|
|
420
|
+
</button>
|
|
421
|
+
<textarea
|
|
422
|
+
value={inputValue}
|
|
423
|
+
onChange={(e) => setInputValue(e.target.value)}
|
|
424
|
+
onKeyDown={(e) => {
|
|
425
|
+
if ((e.metaKey || e.ctrlKey) && e.key === 'Enter') {
|
|
426
|
+
handleSend();
|
|
427
|
+
}
|
|
428
|
+
}}
|
|
429
|
+
placeholder="Type a message... (Cmd/Ctrl + Enter to send)"
|
|
430
|
+
className="input flex-1 resize-none"
|
|
431
|
+
rows={2}
|
|
432
|
+
/>
|
|
433
|
+
<button onClick={handleSend} className="btn btn-primary px-6 gap-2" disabled={loading}>
|
|
434
|
+
<Send className="w-4 h-4" />
|
|
435
|
+
Send
|
|
436
|
+
</button>
|
|
437
|
+
</div>
|
|
438
|
+
</div>
|
|
439
|
+
</div>
|
|
440
|
+
);
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
function ChatMessageComponent({ message, tools }: { message: ChatMessage; tools: Tool[] }) {
|
|
444
|
+
if (message.role === 'tool') return null; // Don't render tool messages directly
|
|
445
|
+
|
|
446
|
+
const isUser = message.role === 'user';
|
|
447
|
+
|
|
448
|
+
return (
|
|
449
|
+
<div
|
|
450
|
+
className={`flex ${isUser ? 'justify-end' : 'justify-start'} animate-fade-in`}
|
|
451
|
+
>
|
|
452
|
+
<div className={`max-w-[70%] ${isUser ? 'bg-primary/10 border-primary/30' : 'bg-card'} rounded-2xl p-5 border border-border shadow-sm`}>
|
|
453
|
+
{message.image && (
|
|
454
|
+
<img
|
|
455
|
+
src={message.image.data}
|
|
456
|
+
alt={message.image.name}
|
|
457
|
+
className="rounded-lg mb-3 max-w-full border border-border"
|
|
458
|
+
/>
|
|
459
|
+
)}
|
|
460
|
+
<p className="text-sm whitespace-pre-wrap text-foreground">{message.content}</p>
|
|
461
|
+
|
|
462
|
+
{/* Tool Calls */}
|
|
463
|
+
{message.toolCalls && message.toolCalls.length > 0 && (
|
|
464
|
+
<div className="mt-4 space-y-3">
|
|
465
|
+
{message.toolCalls.map((toolCall) => (
|
|
466
|
+
<ToolCallComponent key={toolCall.id} toolCall={toolCall} tools={tools} />
|
|
467
|
+
))}
|
|
468
|
+
</div>
|
|
469
|
+
)}
|
|
470
|
+
</div>
|
|
471
|
+
</div>
|
|
472
|
+
);
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
function ToolCallComponent({ toolCall, tools }: { toolCall: ToolCall; tools: Tool[] }) {
|
|
476
|
+
const tool = tools.find((t) => t.name === toolCall.name);
|
|
477
|
+
|
|
478
|
+
// Get widget URI from multiple possible sources
|
|
479
|
+
const componentUri =
|
|
480
|
+
tool?.widget?.route ||
|
|
481
|
+
tool?.outputTemplate ||
|
|
482
|
+
tool?._meta?.['openai/outputTemplate'] ||
|
|
483
|
+
tool?._meta?.['ui/template'];
|
|
484
|
+
|
|
485
|
+
// Get result data from toolCall and unwrap if needed
|
|
486
|
+
let widgetData = toolCall.result || toolCall.arguments;
|
|
487
|
+
|
|
488
|
+
// Unwrap if response was wrapped by TransformInterceptor
|
|
489
|
+
// Check if it has the interceptor's structure: { success, data, metadata }
|
|
490
|
+
if (widgetData && typeof widgetData === 'object' &&
|
|
491
|
+
widgetData.success !== undefined && widgetData.data !== undefined) {
|
|
492
|
+
widgetData = widgetData.data; // Return the unwrapped data
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
console.log('ToolCallComponent:', {
|
|
496
|
+
toolName: toolCall.name,
|
|
497
|
+
componentUri,
|
|
498
|
+
hasData: !!widgetData,
|
|
499
|
+
tool
|
|
500
|
+
});
|
|
501
|
+
|
|
502
|
+
return (
|
|
503
|
+
<div className="rounded-lg p-4 border border-border bg-muted/30">
|
|
504
|
+
<div className="flex items-center gap-2 mb-3">
|
|
505
|
+
<Wrench className="w-4 h-4 text-primary" />
|
|
506
|
+
<span className="font-semibold text-sm text-foreground">{toolCall.name}</span>
|
|
507
|
+
</div>
|
|
508
|
+
|
|
509
|
+
{/* Arguments */}
|
|
510
|
+
<details className="text-xs text-muted-foreground">
|
|
511
|
+
<summary className="cursor-pointer hover:text-foreground transition-colors font-medium">
|
|
512
|
+
Arguments
|
|
513
|
+
</summary>
|
|
514
|
+
<pre className="mt-2 p-3 rounded-lg overflow-auto bg-background border border-border font-mono text-foreground">
|
|
515
|
+
{JSON.stringify(toolCall.arguments, null, 2)}
|
|
516
|
+
</pre>
|
|
517
|
+
</details>
|
|
518
|
+
|
|
519
|
+
{/* Widget if available */}
|
|
520
|
+
{componentUri && widgetData && (
|
|
521
|
+
<div className="mt-3 rounded-lg overflow-hidden border border-border bg-background shadow-inner" style={{
|
|
522
|
+
height: '320px'
|
|
523
|
+
}}>
|
|
524
|
+
<WidgetRenderer uri={componentUri} data={widgetData} />
|
|
525
|
+
</div>
|
|
526
|
+
)}
|
|
527
|
+
</div>
|
|
528
|
+
);
|
|
529
|
+
}
|
|
530
|
+
|