nitrostack 1.0.70 → 1.0.72
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/package.json +1 -1
- package/src/studio/app/api/chat/route.ts +33 -15
- package/src/studio/app/auth/callback/page.tsx +6 -6
- package/src/studio/app/chat/page.tsx +1124 -415
- package/src/studio/app/chat/page.tsx.backup +1046 -187
- package/src/studio/app/globals.css +361 -191
- package/src/studio/app/health/page.tsx +72 -76
- package/src/studio/app/layout.tsx +9 -11
- package/src/studio/app/logs/page.tsx +29 -30
- package/src/studio/app/page.tsx +134 -230
- package/src/studio/app/prompts/page.tsx +115 -97
- package/src/studio/app/resources/page.tsx +115 -124
- package/src/studio/app/settings/page.tsx +1080 -125
- package/src/studio/app/tools/page.tsx +343 -0
- package/src/studio/components/EnlargeModal.tsx +76 -65
- package/src/studio/components/LogMessage.tsx +5 -5
- package/src/studio/components/MarkdownRenderer.tsx +4 -4
- package/src/studio/components/Sidebar.tsx +150 -210
- package/src/studio/components/SplashScreen.tsx +109 -0
- package/src/studio/components/ToolCard.tsx +50 -41
- package/src/studio/components/VoiceOrbOverlay.tsx +469 -0
- package/src/studio/components/WidgetRenderer.tsx +8 -3
- package/src/studio/components/tools/ToolsCanvas.tsx +327 -0
- package/src/studio/lib/llm-service.ts +104 -1
- package/src/studio/lib/store.ts +36 -21
- package/src/studio/lib/types.ts +1 -1
- package/src/studio/package-lock.json +3303 -0
- package/src/studio/package.json +3 -1
- package/src/studio/public/NitroStudio Isotype Color.png +0 -0
- package/src/studio/tailwind.config.ts +63 -17
- package/templates/typescript-starter/package-lock.json +4112 -0
- package/templates/typescript-starter/package.json +2 -3
- package/templates/typescript-starter/src/modules/calculator/calculator.tools.ts +100 -5
- package/src/studio/app/auth/page.tsx +0 -560
- package/src/studio/app/ping/page.tsx +0 -209
|
@@ -4,7 +4,34 @@ import { useEffect, useRef, useState } from 'react';
|
|
|
4
4
|
import { useStudioStore } from '@/lib/store';
|
|
5
5
|
import { api } from '@/lib/api';
|
|
6
6
|
import { WidgetRenderer } from '@/components/WidgetRenderer';
|
|
7
|
-
import
|
|
7
|
+
import { MarkdownRenderer } from '@/components/MarkdownRenderer';
|
|
8
|
+
import { VoiceOrbOverlay } from '@/components/VoiceOrbOverlay';
|
|
9
|
+
import type { ChatMessage, Tool, ToolCall, Prompt } from '@/lib/types';
|
|
10
|
+
import {
|
|
11
|
+
SparklesIcon,
|
|
12
|
+
Cog6ToothIcon,
|
|
13
|
+
TrashIcon,
|
|
14
|
+
PhotoIcon,
|
|
15
|
+
PaperAirplaneIcon,
|
|
16
|
+
WrenchScrewdriverIcon,
|
|
17
|
+
BookmarkIcon,
|
|
18
|
+
XMarkIcon,
|
|
19
|
+
DocumentTextIcon,
|
|
20
|
+
PlayIcon,
|
|
21
|
+
ArrowTopRightOnSquareIcon,
|
|
22
|
+
InformationCircleIcon,
|
|
23
|
+
EllipsisVerticalIcon,
|
|
24
|
+
MicrophoneIcon,
|
|
25
|
+
SpeakerWaveIcon,
|
|
26
|
+
StopIcon
|
|
27
|
+
} from '@heroicons/react/24/outline';
|
|
28
|
+
|
|
29
|
+
// Add type for webkitSpeechRecognition
|
|
30
|
+
declare global {
|
|
31
|
+
interface Window {
|
|
32
|
+
webkitSpeechRecognition: any;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
8
35
|
|
|
9
36
|
export default function ChatPage() {
|
|
10
37
|
const {
|
|
@@ -13,26 +40,154 @@ export default function ChatPage() {
|
|
|
13
40
|
clearChat,
|
|
14
41
|
currentProvider,
|
|
15
42
|
setCurrentProvider,
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
jwtToken,
|
|
43
|
+
currentFile,
|
|
44
|
+
setCurrentFile,
|
|
19
45
|
tools,
|
|
20
46
|
setTools,
|
|
47
|
+
elevenLabsApiKey,
|
|
48
|
+
setElevenLabsApiKey
|
|
21
49
|
} = useStudioStore();
|
|
22
50
|
|
|
51
|
+
// ... (existing helper methods)
|
|
52
|
+
const getAuthTokens = () => {
|
|
53
|
+
const state = useStudioStore.getState();
|
|
54
|
+
const jwtToken = state.jwtToken || state.oauthState?.currentToken;
|
|
55
|
+
return {
|
|
56
|
+
jwtToken,
|
|
57
|
+
mcpApiKey: state.apiKey,
|
|
58
|
+
};
|
|
59
|
+
};
|
|
60
|
+
|
|
23
61
|
const [inputValue, setInputValue] = useState('');
|
|
24
62
|
const [loading, setLoading] = useState(false);
|
|
25
63
|
const [showSettings, setShowSettings] = useState(false);
|
|
64
|
+
const [prompts, setPrompts] = useState<Prompt[]>([]);
|
|
65
|
+
const [selectedPrompt, setSelectedPrompt] = useState<Prompt | null>(null);
|
|
66
|
+
const [promptArgs, setPromptArgs] = useState<Record<string, string>>({});
|
|
67
|
+
const [fullscreenWidget, setFullscreenWidget] = useState<{ uri: string, data: any } | null>(null);
|
|
68
|
+
|
|
69
|
+
// Voice Mode State
|
|
70
|
+
const [isRecording, setIsRecording] = useState(false);
|
|
71
|
+
const [isSpeaking, setIsSpeaking] = useState(false);
|
|
72
|
+
const [voiceModeEnabled, setVoiceModeEnabled] = useState(false);
|
|
73
|
+
const [voiceOverlayOpen, setVoiceOverlayOpen] = useState(false);
|
|
74
|
+
const [spokenText, setSpokenText] = useState('');
|
|
75
|
+
const recognitionRef = useRef<any>(null);
|
|
76
|
+
const audioRef = useRef<HTMLAudioElement | null>(null);
|
|
77
|
+
|
|
26
78
|
const messagesEndRef = useRef<HTMLDivElement>(null);
|
|
27
79
|
const fileInputRef = useRef<HTMLInputElement>(null);
|
|
80
|
+
const textareaRef = useRef<HTMLTextAreaElement>(null);
|
|
81
|
+
const initialToolExecuted = useRef(false);
|
|
82
|
+
|
|
83
|
+
// ... (existing useEffects)
|
|
28
84
|
|
|
85
|
+
// Initialize Speech Recognition
|
|
29
86
|
useEffect(() => {
|
|
30
|
-
|
|
87
|
+
if (typeof window !== 'undefined' && window.webkitSpeechRecognition) {
|
|
88
|
+
const recognition = new window.webkitSpeechRecognition();
|
|
89
|
+
recognition.continuous = false;
|
|
90
|
+
recognition.interimResults = false;
|
|
91
|
+
recognition.lang = 'en-US';
|
|
92
|
+
|
|
93
|
+
recognition.onresult = (event: any) => {
|
|
94
|
+
const transcript = event.results[0][0].transcript;
|
|
95
|
+
setInputValue((prev) => prev + (prev ? ' ' : '') + transcript);
|
|
96
|
+
setIsRecording(false);
|
|
97
|
+
// Optional: Auto-send if desired, but letting user review is safer for code
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
recognition.onerror = (event: any) => {
|
|
101
|
+
console.error('Speech recognition error', event.error);
|
|
102
|
+
setIsRecording(false);
|
|
103
|
+
};
|
|
104
|
+
|
|
105
|
+
recognition.onend = () => {
|
|
106
|
+
setIsRecording(false);
|
|
107
|
+
};
|
|
108
|
+
|
|
109
|
+
recognitionRef.current = recognition;
|
|
110
|
+
}
|
|
31
111
|
}, []);
|
|
32
112
|
|
|
113
|
+
// Text-to-Speech logic for new messages
|
|
33
114
|
useEffect(() => {
|
|
34
|
-
|
|
35
|
-
|
|
115
|
+
if (!voiceModeEnabled || !elevenLabsApiKey || chatMessages.length === 0) return;
|
|
116
|
+
|
|
117
|
+
const lastMessage = chatMessages[chatMessages.length - 1];
|
|
118
|
+
if (lastMessage.role === 'assistant' && lastMessage.content) {
|
|
119
|
+
// Stop any current audio
|
|
120
|
+
if (audioRef.current) {
|
|
121
|
+
audioRef.current.pause();
|
|
122
|
+
audioRef.current = null;
|
|
123
|
+
}
|
|
124
|
+
playTextToSpeech(lastMessage.content);
|
|
125
|
+
}
|
|
126
|
+
}, [chatMessages, voiceModeEnabled, elevenLabsApiKey]);
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
const toggleRecording = () => {
|
|
130
|
+
if (isRecording) {
|
|
131
|
+
recognitionRef.current?.stop();
|
|
132
|
+
} else {
|
|
133
|
+
if (!recognitionRef.current) {
|
|
134
|
+
alert('Speech recognition not supported in this browser.');
|
|
135
|
+
return;
|
|
136
|
+
}
|
|
137
|
+
setVoiceModeEnabled(true); // Enable voice mode if they use mic
|
|
138
|
+
recognitionRef.current.start();
|
|
139
|
+
setIsRecording(true);
|
|
140
|
+
}
|
|
141
|
+
};
|
|
142
|
+
|
|
143
|
+
const playTextToSpeech = async (text: string) => {
|
|
144
|
+
if (!elevenLabsApiKey) return;
|
|
145
|
+
|
|
146
|
+
try {
|
|
147
|
+
setIsSpeaking(true);
|
|
148
|
+
const voiceId = '21m00Tcm4TlvDq8ikWAM'; // Rachel - popular default
|
|
149
|
+
const response = await fetch(`https://api.elevenlabs.io/v1/text-to-speech/${voiceId}/stream`, {
|
|
150
|
+
method: 'POST',
|
|
151
|
+
headers: {
|
|
152
|
+
'Content-Type': 'application/json',
|
|
153
|
+
'xi-api-key': elevenLabsApiKey,
|
|
154
|
+
},
|
|
155
|
+
body: JSON.stringify({
|
|
156
|
+
text,
|
|
157
|
+
model_id: 'eleven_monolingual_v1',
|
|
158
|
+
voice_settings: {
|
|
159
|
+
stability: 0.5,
|
|
160
|
+
similarity_boost: 0.75,
|
|
161
|
+
},
|
|
162
|
+
}),
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
if (!response.ok) throw new Error('TTS failed');
|
|
166
|
+
|
|
167
|
+
const blob = await response.blob();
|
|
168
|
+
const url = URL.createObjectURL(blob);
|
|
169
|
+
const audio = new Audio(url);
|
|
170
|
+
|
|
171
|
+
audio.onended = () => {
|
|
172
|
+
setIsSpeaking(false);
|
|
173
|
+
URL.revokeObjectURL(url);
|
|
174
|
+
};
|
|
175
|
+
|
|
176
|
+
audioRef.current = audio;
|
|
177
|
+
audio.play();
|
|
178
|
+
} catch (error) {
|
|
179
|
+
console.error('TTS Error:', error);
|
|
180
|
+
setIsSpeaking(false);
|
|
181
|
+
}
|
|
182
|
+
};
|
|
183
|
+
|
|
184
|
+
const stopSpeaking = () => {
|
|
185
|
+
if (audioRef.current) {
|
|
186
|
+
audioRef.current.pause();
|
|
187
|
+
audioRef.current = null;
|
|
188
|
+
}
|
|
189
|
+
setIsSpeaking(false);
|
|
190
|
+
};
|
|
36
191
|
|
|
37
192
|
const loadTools = async () => {
|
|
38
193
|
try {
|
|
@@ -43,7 +198,146 @@ export default function ChatPage() {
|
|
|
43
198
|
}
|
|
44
199
|
};
|
|
45
200
|
|
|
46
|
-
const
|
|
201
|
+
const loadPrompts = async () => {
|
|
202
|
+
try {
|
|
203
|
+
const data = await api.getPrompts();
|
|
204
|
+
setPrompts(data.prompts || []);
|
|
205
|
+
} catch (error) {
|
|
206
|
+
console.error('Failed to load prompts:', error);
|
|
207
|
+
}
|
|
208
|
+
};
|
|
209
|
+
|
|
210
|
+
const checkAndRunInitialTool = async () => {
|
|
211
|
+
// Find initial tool using specific metadata key
|
|
212
|
+
const initialTool = tools.find(t => t._meta?.['tool/initial'] === true);
|
|
213
|
+
if (!initialTool) return;
|
|
214
|
+
|
|
215
|
+
// Check for API keys (Gemini or OpenAI)
|
|
216
|
+
const geminiKey = localStorage.getItem('gemini_api_key');
|
|
217
|
+
const openaiKey = localStorage.getItem('openai_api_key');
|
|
218
|
+
const hasKey = (geminiKey && geminiKey !== '••••••••') || (openaiKey && openaiKey !== '••••••••');
|
|
219
|
+
|
|
220
|
+
if (!hasKey) return;
|
|
221
|
+
|
|
222
|
+
// Mark as executed immediately to prevent double run
|
|
223
|
+
initialToolExecuted.current = true;
|
|
224
|
+
console.log('🚀 Auto-executing initial tool:', initialTool.name);
|
|
225
|
+
|
|
226
|
+
// Initial message
|
|
227
|
+
const autoMsg: ChatMessage = {
|
|
228
|
+
role: 'user',
|
|
229
|
+
content: `(Auto) Executing initial tool: ${initialTool.name}`,
|
|
230
|
+
};
|
|
231
|
+
addChatMessage(autoMsg);
|
|
232
|
+
setLoading(true);
|
|
233
|
+
|
|
234
|
+
try {
|
|
235
|
+
const { jwtToken, mcpApiKey } = getAuthTokens();
|
|
236
|
+
const effectiveToken = jwtToken || useStudioStore.getState().oauthState?.currentToken;
|
|
237
|
+
|
|
238
|
+
// Call the tool
|
|
239
|
+
const result = await api.callTool(
|
|
240
|
+
initialTool.name,
|
|
241
|
+
{},
|
|
242
|
+
effectiveToken,
|
|
243
|
+
mcpApiKey || undefined
|
|
244
|
+
);
|
|
245
|
+
|
|
246
|
+
// Add assistant message with tool call info
|
|
247
|
+
const toolCallId = `call_${Date.now()}`;
|
|
248
|
+
const assistantMsg: ChatMessage = {
|
|
249
|
+
role: 'assistant',
|
|
250
|
+
content: `Invoking ${initialTool.name}...`,
|
|
251
|
+
toolCalls: [{
|
|
252
|
+
id: toolCallId,
|
|
253
|
+
name: initialTool.name,
|
|
254
|
+
arguments: {},
|
|
255
|
+
result // Attach result here for widget rendering
|
|
256
|
+
}]
|
|
257
|
+
};
|
|
258
|
+
addChatMessage(assistantMsg);
|
|
259
|
+
|
|
260
|
+
// Add tool result message
|
|
261
|
+
const toolResultMsg: ChatMessage = {
|
|
262
|
+
role: 'tool',
|
|
263
|
+
content: JSON.stringify(result),
|
|
264
|
+
toolCallId: toolCallId
|
|
265
|
+
};
|
|
266
|
+
addChatMessage(toolResultMsg);
|
|
267
|
+
|
|
268
|
+
} catch (error) {
|
|
269
|
+
console.error('Initial tool execution failed:', error);
|
|
270
|
+
addChatMessage({
|
|
271
|
+
role: 'assistant',
|
|
272
|
+
content: `Failed to execute initial tool ${initialTool.name}: ${error instanceof Error ? error.message : String(error)}`
|
|
273
|
+
});
|
|
274
|
+
} finally {
|
|
275
|
+
setLoading(false);
|
|
276
|
+
}
|
|
277
|
+
};
|
|
278
|
+
|
|
279
|
+
const handleExecutePrompt = async () => {
|
|
280
|
+
if (!selectedPrompt) return;
|
|
281
|
+
|
|
282
|
+
// Close modal
|
|
283
|
+
const prompt = selectedPrompt;
|
|
284
|
+
const args = { ...promptArgs };
|
|
285
|
+
setSelectedPrompt(null);
|
|
286
|
+
setPromptArgs({});
|
|
287
|
+
|
|
288
|
+
// Build user message showing what prompt was executed
|
|
289
|
+
let userMessageContent = `Execute prompt: ${prompt.name}`;
|
|
290
|
+
if (Object.keys(args).length > 0) {
|
|
291
|
+
userMessageContent += `\nArguments: ${JSON.stringify(args, null, 2)}`;
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
// Add user message to chat
|
|
295
|
+
addChatMessage({
|
|
296
|
+
role: 'user',
|
|
297
|
+
content: userMessageContent,
|
|
298
|
+
});
|
|
299
|
+
|
|
300
|
+
setLoading(true);
|
|
301
|
+
|
|
302
|
+
try {
|
|
303
|
+
// Execute the prompt directly via API
|
|
304
|
+
const result = await api.executePrompt(prompt.name, args);
|
|
305
|
+
|
|
306
|
+
// Add the prompt result as an assistant message
|
|
307
|
+
if (result.messages && result.messages.length > 0) {
|
|
308
|
+
// Combine all prompt messages into one assistant message
|
|
309
|
+
const combinedContent = result.messages
|
|
310
|
+
.map((msg: any) => {
|
|
311
|
+
const content = typeof msg.content === 'string'
|
|
312
|
+
? msg.content
|
|
313
|
+
: msg.content?.text || JSON.stringify(msg.content);
|
|
314
|
+
return `[${msg.role.toUpperCase()}]\n${content}`;
|
|
315
|
+
})
|
|
316
|
+
.join('\n\n');
|
|
317
|
+
|
|
318
|
+
addChatMessage({
|
|
319
|
+
role: 'assistant',
|
|
320
|
+
content: combinedContent,
|
|
321
|
+
});
|
|
322
|
+
} else {
|
|
323
|
+
addChatMessage({
|
|
324
|
+
role: 'assistant',
|
|
325
|
+
content: 'Prompt executed successfully, but returned no messages.',
|
|
326
|
+
});
|
|
327
|
+
}
|
|
328
|
+
} catch (error) {
|
|
329
|
+
console.error('Failed to execute prompt:', error);
|
|
330
|
+
addChatMessage({
|
|
331
|
+
role: 'assistant',
|
|
332
|
+
content: `Error executing prompt: ${error instanceof Error ? error.message : 'Unknown error'}`,
|
|
333
|
+
});
|
|
334
|
+
} finally {
|
|
335
|
+
setLoading(false);
|
|
336
|
+
setTimeout(() => textareaRef.current?.focus(), 100);
|
|
337
|
+
}
|
|
338
|
+
};
|
|
339
|
+
|
|
340
|
+
const handleFileUpload = (e: React.ChangeEvent<HTMLInputElement>) => {
|
|
47
341
|
const file = e.target.files?.[0];
|
|
48
342
|
if (!file) return;
|
|
49
343
|
|
|
@@ -54,7 +348,7 @@ export default function ChatPage() {
|
|
|
54
348
|
|
|
55
349
|
const reader = new FileReader();
|
|
56
350
|
reader.onload = (event) => {
|
|
57
|
-
|
|
351
|
+
setCurrentFile({
|
|
58
352
|
data: event.target?.result as string,
|
|
59
353
|
type: file.type,
|
|
60
354
|
name: file.name,
|
|
@@ -64,7 +358,7 @@ export default function ChatPage() {
|
|
|
64
358
|
};
|
|
65
359
|
|
|
66
360
|
const handleSend = async () => {
|
|
67
|
-
if (!inputValue.trim() && !
|
|
361
|
+
if (!inputValue.trim() && !currentFile) return;
|
|
68
362
|
|
|
69
363
|
const apiKey = localStorage.getItem(`${currentProvider}_api_key`);
|
|
70
364
|
if (!apiKey) {
|
|
@@ -78,72 +372,180 @@ export default function ChatPage() {
|
|
|
78
372
|
content: inputValue,
|
|
79
373
|
};
|
|
80
374
|
|
|
81
|
-
if (
|
|
82
|
-
userMessage.
|
|
375
|
+
if (currentFile) {
|
|
376
|
+
userMessage.file = currentFile;
|
|
83
377
|
}
|
|
84
378
|
|
|
85
379
|
addChatMessage(userMessage);
|
|
86
380
|
setInputValue('');
|
|
87
|
-
|
|
381
|
+
setCurrentFile(null);
|
|
88
382
|
setLoading(true);
|
|
89
383
|
|
|
90
384
|
try {
|
|
385
|
+
const messagesToSend = [...chatMessages, userMessage];
|
|
386
|
+
|
|
387
|
+
// Clean messages to ensure they're serializable
|
|
388
|
+
const cleanedMessages = messagesToSend.map(msg => {
|
|
389
|
+
const cleaned: any = {
|
|
390
|
+
role: msg.role,
|
|
391
|
+
content: msg.content || '',
|
|
392
|
+
};
|
|
393
|
+
|
|
394
|
+
if (msg.toolCalls && msg.toolCalls.length > 0) {
|
|
395
|
+
cleaned.toolCalls = msg.toolCalls;
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
if (msg.toolCallId) {
|
|
399
|
+
cleaned.toolCallId = msg.toolCallId;
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
// Skip image property for now (not supported by OpenAI chat completions)
|
|
403
|
+
if (msg.file) {
|
|
404
|
+
cleaned.file = msg.file;
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
return cleaned;
|
|
408
|
+
});
|
|
409
|
+
|
|
410
|
+
// Get fresh auth tokens from store
|
|
411
|
+
const { jwtToken, mcpApiKey } = getAuthTokens();
|
|
412
|
+
|
|
413
|
+
console.log('Sending messages to API:', cleanedMessages);
|
|
414
|
+
console.log('Auth tokens:', { hasJwtToken: !!jwtToken, hasMcpApiKey: !!mcpApiKey });
|
|
415
|
+
console.log('Original messages:', messagesToSend);
|
|
416
|
+
console.log('Cleaned messages JSON:', JSON.stringify(cleanedMessages));
|
|
417
|
+
|
|
91
418
|
const response = await api.chat({
|
|
92
419
|
provider: currentProvider,
|
|
93
|
-
messages:
|
|
94
|
-
apiKey,
|
|
420
|
+
messages: cleanedMessages,
|
|
421
|
+
apiKey, // LLM API key (OpenAI/Gemini)
|
|
95
422
|
jwtToken: jwtToken || undefined,
|
|
423
|
+
mcpApiKey: mcpApiKey || undefined, // MCP server API key
|
|
96
424
|
});
|
|
97
425
|
|
|
98
|
-
|
|
99
|
-
addChatMessage(response.message);
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
// Handle tool calls
|
|
426
|
+
// Handle tool calls FIRST (before adding the message)
|
|
103
427
|
if (response.toolCalls && response.toolResults) {
|
|
104
|
-
//
|
|
428
|
+
// Attach results to tool calls for widget rendering
|
|
429
|
+
const toolCallsWithResults = response.toolCalls.map((tc: ToolCall, i: number) => {
|
|
430
|
+
const toolResult = response.toolResults[i];
|
|
431
|
+
|
|
432
|
+
// Parse the result content
|
|
433
|
+
let parsedResult;
|
|
434
|
+
if (toolResult.content) {
|
|
435
|
+
try {
|
|
436
|
+
parsedResult = JSON.parse(toolResult.content);
|
|
437
|
+
|
|
438
|
+
// Unwrap if response was wrapped by TransformInterceptor
|
|
439
|
+
if (parsedResult.success !== undefined && parsedResult.data !== undefined) {
|
|
440
|
+
parsedResult = parsedResult.data;
|
|
441
|
+
}
|
|
442
|
+
} catch {
|
|
443
|
+
parsedResult = { content: toolResult.content };
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
return {
|
|
448
|
+
...tc,
|
|
449
|
+
result: parsedResult,
|
|
450
|
+
};
|
|
451
|
+
});
|
|
452
|
+
|
|
453
|
+
// Add assistant message with tool calls
|
|
454
|
+
if (response.message) {
|
|
455
|
+
response.message.toolCalls = toolCallsWithResults;
|
|
456
|
+
addChatMessage(response.message);
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
// Extract JWT token from ANY tool response (not just 'login')
|
|
105
460
|
for (let i = 0; i < response.toolCalls.length; i++) {
|
|
106
461
|
const toolCall = response.toolCalls[i];
|
|
107
462
|
const toolResult = response.toolResults[i];
|
|
108
463
|
|
|
109
|
-
if (
|
|
464
|
+
if (toolResult.content) {
|
|
110
465
|
try {
|
|
111
466
|
const parsed = JSON.parse(toolResult.content);
|
|
112
|
-
|
|
113
|
-
|
|
467
|
+
// Check for token in multiple possible locations
|
|
468
|
+
const token = parsed.token || parsed.access_token || parsed.jwt || parsed.data?.token;
|
|
469
|
+
if (token) {
|
|
470
|
+
console.log('🔐 Token received from tool in chat, saving to global state');
|
|
471
|
+
useStudioStore.getState().setJwtToken(token);
|
|
472
|
+
break; // Stop after first token found
|
|
114
473
|
}
|
|
115
474
|
} catch (e) {
|
|
116
|
-
// Ignore
|
|
475
|
+
// Ignore parsing errors
|
|
117
476
|
}
|
|
118
477
|
}
|
|
119
478
|
}
|
|
120
479
|
|
|
121
|
-
// Add tool results
|
|
480
|
+
// Add tool results to messages
|
|
481
|
+
const toolResultMessages: ChatMessage[] = [];
|
|
122
482
|
for (const result of response.toolResults) {
|
|
123
483
|
addChatMessage(result);
|
|
484
|
+
toolResultMessages.push(result);
|
|
124
485
|
}
|
|
125
486
|
|
|
126
|
-
// Continue conversation
|
|
127
|
-
|
|
487
|
+
// Continue conversation and WAIT for it to complete before allowing new messages
|
|
488
|
+
// Build the full message history to pass to continuation
|
|
489
|
+
const messagesForContinuation = [
|
|
490
|
+
...chatMessages,
|
|
491
|
+
response.message!, // Assistant message with tool calls
|
|
492
|
+
...toolResultMessages, // Tool result messages
|
|
493
|
+
];
|
|
494
|
+
await continueChatWithToolResults(apiKey, messagesForContinuation);
|
|
495
|
+
} else {
|
|
496
|
+
// No tool calls, just add the message
|
|
497
|
+
if (response.message) {
|
|
498
|
+
addChatMessage(response.message);
|
|
499
|
+
}
|
|
128
500
|
}
|
|
501
|
+
|
|
502
|
+
// Set loading to false AFTER all async operations complete
|
|
503
|
+
setLoading(false);
|
|
129
504
|
} catch (error) {
|
|
130
505
|
console.error('Chat error:', error);
|
|
131
506
|
addChatMessage({
|
|
132
507
|
role: 'assistant',
|
|
133
508
|
content: 'Sorry, I encountered an error. Please try again.',
|
|
134
509
|
});
|
|
135
|
-
} finally {
|
|
136
510
|
setLoading(false);
|
|
137
511
|
}
|
|
138
512
|
};
|
|
139
513
|
|
|
140
|
-
const continueChatWithToolResults = async (apiKey: string) => {
|
|
514
|
+
const continueChatWithToolResults = async (apiKey: string, messages?: ChatMessage[]) => {
|
|
141
515
|
try {
|
|
516
|
+
// Use provided messages or fall back to store (for recursive calls)
|
|
517
|
+
const messagesToUse = messages || chatMessages;
|
|
518
|
+
|
|
519
|
+
// Get fresh auth tokens from store (token may have been updated by login)
|
|
520
|
+
const { jwtToken, mcpApiKey } = getAuthTokens();
|
|
521
|
+
|
|
522
|
+
// Clean messages before sending
|
|
523
|
+
const cleanedMessages = messagesToUse.map(msg => {
|
|
524
|
+
const cleaned: any = {
|
|
525
|
+
role: msg.role,
|
|
526
|
+
content: msg.content || '',
|
|
527
|
+
};
|
|
528
|
+
|
|
529
|
+
if (msg.toolCalls && msg.toolCalls.length > 0) {
|
|
530
|
+
cleaned.toolCalls = msg.toolCalls;
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
if (msg.toolCallId) {
|
|
534
|
+
cleaned.toolCallId = msg.toolCallId;
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
return cleaned;
|
|
538
|
+
});
|
|
539
|
+
|
|
540
|
+
console.log('Continue with cleaned messages:', JSON.stringify(cleanedMessages));
|
|
541
|
+
console.log('Continue auth tokens:', { hasJwtToken: !!jwtToken, hasMcpApiKey: !!mcpApiKey });
|
|
542
|
+
|
|
142
543
|
const response = await api.chat({
|
|
143
544
|
provider: currentProvider,
|
|
144
|
-
messages:
|
|
145
|
-
apiKey,
|
|
545
|
+
messages: cleanedMessages,
|
|
546
|
+
apiKey, // LLM API key (OpenAI/Gemini)
|
|
146
547
|
jwtToken: jwtToken || undefined,
|
|
548
|
+
mcpApiKey: mcpApiKey || undefined, // MCP server API key
|
|
147
549
|
});
|
|
148
550
|
|
|
149
551
|
if (response.message) {
|
|
@@ -152,10 +554,19 @@ export default function ChatPage() {
|
|
|
152
554
|
|
|
153
555
|
// Recursive tool calls
|
|
154
556
|
if (response.toolCalls && response.toolResults) {
|
|
557
|
+
const newToolResults: ChatMessage[] = [];
|
|
155
558
|
for (const result of response.toolResults) {
|
|
156
559
|
addChatMessage(result);
|
|
560
|
+
newToolResults.push(result);
|
|
157
561
|
}
|
|
158
|
-
|
|
562
|
+
|
|
563
|
+
// Build messages for next iteration
|
|
564
|
+
const nextMessages = [
|
|
565
|
+
...(messages || chatMessages),
|
|
566
|
+
response.message!,
|
|
567
|
+
...newToolResults,
|
|
568
|
+
];
|
|
569
|
+
await continueChatWithToolResults(apiKey, nextMessages);
|
|
159
570
|
}
|
|
160
571
|
} catch (error) {
|
|
161
572
|
console.error('Continuation error:', error);
|
|
@@ -177,148 +588,599 @@ export default function ChatPage() {
|
|
|
177
588
|
};
|
|
178
589
|
|
|
179
590
|
return (
|
|
180
|
-
<div className="
|
|
181
|
-
{/* Header */}
|
|
182
|
-
<div className="border-b border-
|
|
183
|
-
<
|
|
184
|
-
|
|
185
|
-
<
|
|
186
|
-
</
|
|
591
|
+
<div className="fixed inset-0 flex flex-col" style={{ left: 'var(--sidebar-width, 15rem)', backgroundColor: '#0a0a0a' }}>
|
|
592
|
+
{/* Minimal Professional Header */}
|
|
593
|
+
<div className="sticky top-0 z-10 border-b border-border/50 px-3 sm:px-6 py-4 flex flex-col sm:flex-row items-start sm:items-center justify-between bg-card/50 backdrop-blur-sm gap-3 sm:gap-0">
|
|
594
|
+
<h1 className="text-lg font-semibold text-foreground flex items-center gap-2">
|
|
595
|
+
AI Chat
|
|
596
|
+
{voiceModeEnabled && <span className="text-xs bg-primary/20 text-primary px-2 py-0.5 rounded-full animate-pulse">Voice Active</span>}
|
|
597
|
+
</h1>
|
|
598
|
+
|
|
599
|
+
<div className="flex items-center gap-2 w-full sm:w-auto">
|
|
600
|
+
{/* Voice Output Toggle */}
|
|
601
|
+
{elevenLabsApiKey && (
|
|
602
|
+
<button
|
|
603
|
+
onClick={() => {
|
|
604
|
+
if (isSpeaking) stopSpeaking();
|
|
605
|
+
setVoiceModeEnabled(!voiceModeEnabled);
|
|
606
|
+
}}
|
|
607
|
+
className={`h-8 w-8 rounded-lg flex items-center justify-center transition-all flex-shrink-0 ${voiceModeEnabled
|
|
608
|
+
? 'bg-primary/20 text-primary ring-1 ring-primary/50'
|
|
609
|
+
: 'bg-muted/50 text-muted-foreground hover:text-foreground'
|
|
610
|
+
}`}
|
|
611
|
+
title={voiceModeEnabled ? "Disable Voice Output" : "Enable Voice Output"}
|
|
612
|
+
>
|
|
613
|
+
{isSpeaking ? <SpeakerWaveIcon className="h-4 w-4 animate-pulse" /> : <MicrophoneIcon className="h-4 w-4" />}
|
|
614
|
+
</button>
|
|
615
|
+
)}
|
|
187
616
|
|
|
188
|
-
<div className="flex items-center gap-2">
|
|
189
617
|
<select
|
|
190
618
|
value={currentProvider}
|
|
191
619
|
onChange={(e) => setCurrentProvider(e.target.value as 'openai' | 'gemini')}
|
|
192
|
-
className="input w-
|
|
620
|
+
className="input text-sm px-3 py-1.5 w-full sm:w-28 flex-1 sm:flex-none"
|
|
193
621
|
>
|
|
194
|
-
<option value="openai">OpenAI</option>
|
|
195
622
|
<option value="gemini">Gemini</option>
|
|
623
|
+
<option value="openai">OpenAI</option>
|
|
196
624
|
</select>
|
|
197
|
-
<button
|
|
198
|
-
|
|
625
|
+
<button
|
|
626
|
+
onClick={() => setShowSettings(!showSettings)}
|
|
627
|
+
className={`h-8 w-8 rounded-lg flex items-center justify-center transition-all flex-shrink-0 ${showSettings
|
|
628
|
+
? 'bg-primary/10 text-primary ring-1 ring-primary/30'
|
|
629
|
+
: 'bg-muted/50 text-muted-foreground hover:bg-muted hover:text-foreground'
|
|
630
|
+
}`}
|
|
631
|
+
title="Settings"
|
|
632
|
+
>
|
|
633
|
+
<Cog6ToothIcon className="h-4 w-4" />
|
|
199
634
|
</button>
|
|
200
|
-
<button
|
|
201
|
-
|
|
635
|
+
<button
|
|
636
|
+
onClick={clearChat}
|
|
637
|
+
className="h-8 w-8 rounded-lg flex items-center justify-center bg-muted/50 text-muted-foreground hover:bg-muted hover:text-foreground transition-all flex-shrink-0"
|
|
638
|
+
title="Clear chat"
|
|
639
|
+
>
|
|
640
|
+
<TrashIcon className="h-4 w-4" />
|
|
202
641
|
</button>
|
|
203
642
|
</div>
|
|
204
643
|
</div>
|
|
205
644
|
|
|
206
|
-
{/* Settings Panel */}
|
|
645
|
+
{/* Enhanced Settings Panel */}
|
|
207
646
|
{showSettings && (
|
|
208
|
-
<div className="border-b border-
|
|
209
|
-
<
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
</button>
|
|
647
|
+
<div className="border-b border-border/50 px-3 sm:px-6 py-4 sm:py-5 bg-muted/20 backdrop-blur-md shadow-sm">
|
|
648
|
+
<div className="max-w-4xl mx-auto">
|
|
649
|
+
<div className="flex items-start justify-between mb-4">
|
|
650
|
+
<div>
|
|
651
|
+
<h3 className="text-sm font-semibold text-foreground flex items-center gap-2">
|
|
652
|
+
<Cog6ToothIcon className="h-4 w-4" />
|
|
653
|
+
API Configuration
|
|
654
|
+
</h3>
|
|
655
|
+
<p className="text-xs text-muted-foreground mt-1">Configure your AI provider API keys to enable chat functionality</p>
|
|
218
656
|
</div>
|
|
219
657
|
</div>
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
<
|
|
225
|
-
|
|
226
|
-
|
|
658
|
+
|
|
659
|
+
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
|
660
|
+
{/* OpenAI Section */}
|
|
661
|
+
<div className="card p-4">
|
|
662
|
+
<div className="flex items-center justify-between mb-3">
|
|
663
|
+
<label className="text-xs font-semibold text-foreground flex items-center gap-2">
|
|
664
|
+
<div className="w-6 h-6 rounded bg-green-500/10 flex items-center justify-center">
|
|
665
|
+
<span className="text-xs font-bold text-green-600">AI</span>
|
|
666
|
+
</div>
|
|
667
|
+
OpenAI API Key
|
|
668
|
+
</label>
|
|
669
|
+
<a
|
|
670
|
+
href="https://platform.openai.com/api-keys"
|
|
671
|
+
target="_blank"
|
|
672
|
+
rel="noopener noreferrer"
|
|
673
|
+
className="text-xs text-primary hover:text-primary/80 flex items-center gap-1 transition-colors"
|
|
674
|
+
>
|
|
675
|
+
Get Key <ArrowTopRightOnSquareIcon className="w-3 h-3" />
|
|
676
|
+
</a>
|
|
677
|
+
</div>
|
|
678
|
+
<div className="flex gap-2 mb-3">
|
|
679
|
+
<input
|
|
680
|
+
id="openai-api-key"
|
|
681
|
+
type="password"
|
|
682
|
+
className="input flex-1 text-sm py-2"
|
|
683
|
+
placeholder="sk-proj-..."
|
|
684
|
+
/>
|
|
685
|
+
<button onClick={() => saveApiKey('openai')} className="btn btn-primary text-xs px-4 py-2">
|
|
686
|
+
<BookmarkIcon className="w-3 h-3 mr-1" />
|
|
687
|
+
Save
|
|
688
|
+
</button>
|
|
689
|
+
</div>
|
|
690
|
+
<div className="flex items-start gap-2 p-2 bg-blue-500/5 rounded-lg border border-blue-500/10">
|
|
691
|
+
<InformationCircleIcon className="w-3 h-3 text-blue-500 mt-0.5 flex-shrink-0" />
|
|
692
|
+
<div className="text-xs text-muted-foreground">
|
|
693
|
+
<p className="mb-1">
|
|
694
|
+
<strong>How to get:</strong> Sign up at{' '}
|
|
695
|
+
<a href="https://platform.openai.com/signup" target="_blank" rel="noopener noreferrer" className="text-primary hover:underline">
|
|
696
|
+
OpenAI Platform
|
|
697
|
+
</a>
|
|
698
|
+
, navigate to API Keys, and create a new secret key.
|
|
699
|
+
</p>
|
|
700
|
+
<a
|
|
701
|
+
href="https://help.openai.com/en/articles/4936850-where-do-i-find-my-openai-api-key"
|
|
702
|
+
target="_blank"
|
|
703
|
+
rel="noopener noreferrer"
|
|
704
|
+
className="text-primary hover:underline inline-flex items-center gap-1"
|
|
705
|
+
>
|
|
706
|
+
View Guide <ArrowTopRightOnSquareIcon className="w-2.5 h-2.5" />
|
|
707
|
+
</a>
|
|
708
|
+
</div>
|
|
709
|
+
</div>
|
|
710
|
+
</div>
|
|
711
|
+
|
|
712
|
+
{/* Gemini Section */}
|
|
713
|
+
<div className="card p-4">
|
|
714
|
+
<div className="flex items-center justify-between mb-3">
|
|
715
|
+
<label className="text-xs font-semibold text-foreground flex items-center gap-2">
|
|
716
|
+
<div className="w-6 h-6 rounded bg-blue-500/10 flex items-center justify-center">
|
|
717
|
+
<span className="text-xs font-bold text-blue-600">G</span>
|
|
718
|
+
</div>
|
|
719
|
+
Gemini API Key
|
|
720
|
+
</label>
|
|
721
|
+
<a
|
|
722
|
+
href="https://aistudio.google.com/app/apikey"
|
|
723
|
+
target="_blank"
|
|
724
|
+
rel="noopener noreferrer"
|
|
725
|
+
className="text-xs text-primary hover:text-primary/80 flex items-center gap-1 transition-colors"
|
|
726
|
+
>
|
|
727
|
+
Get Key <ArrowTopRightOnSquareIcon className="w-3 h-3" />
|
|
728
|
+
</a>
|
|
729
|
+
</div>
|
|
730
|
+
<div className="flex gap-2 mb-3">
|
|
731
|
+
<input
|
|
732
|
+
id="gemini-api-key"
|
|
733
|
+
type="password"
|
|
734
|
+
className="input flex-1 text-sm py-2"
|
|
735
|
+
placeholder="AIza..."
|
|
736
|
+
/>
|
|
737
|
+
<button onClick={() => saveApiKey('gemini')} className="btn btn-primary text-xs px-4 py-2">
|
|
738
|
+
<BookmarkIcon className="w-3 h-3 mr-1" />
|
|
739
|
+
Save
|
|
740
|
+
</button>
|
|
741
|
+
</div>
|
|
742
|
+
<div className="flex items-start gap-2 p-2 bg-blue-500/5 rounded-lg border border-blue-500/10">
|
|
743
|
+
<InformationCircleIcon className="w-3 h-3 text-blue-500 mt-0.5 flex-shrink-0" />
|
|
744
|
+
<div className="text-xs text-muted-foreground">
|
|
745
|
+
<p className="mb-1">
|
|
746
|
+
<strong>How to get:</strong> Visit{' '}
|
|
747
|
+
<a href="https://aistudio.google.com" target="_blank" rel="noopener noreferrer" className="text-primary hover:underline">
|
|
748
|
+
Google AI Studio
|
|
749
|
+
</a>
|
|
750
|
+
, sign in with your Google account, and click "Get API key".
|
|
751
|
+
</p>
|
|
752
|
+
<a
|
|
753
|
+
href="https://ai.google.dev/gemini-api/docs/api-key"
|
|
754
|
+
target="_blank"
|
|
755
|
+
rel="noopener noreferrer"
|
|
756
|
+
className="text-primary hover:underline inline-flex items-center gap-1"
|
|
757
|
+
>
|
|
758
|
+
View Guide <ArrowTopRightOnSquareIcon className="w-2.5 h-2.5" />
|
|
759
|
+
</a>
|
|
760
|
+
</div>
|
|
761
|
+
</div>
|
|
762
|
+
</div>
|
|
763
|
+
</div>
|
|
764
|
+
|
|
765
|
+
{/* Security Notice */}
|
|
766
|
+
<div className="mt-4 p-3 bg-status-info/5 rounded border border-status-info/10">
|
|
767
|
+
<div className="flex items-start gap-2">
|
|
768
|
+
<InformationCircleIcon className="h-4 w-4 text-status-info mt-0.5 flex-shrink-0" />
|
|
769
|
+
<div className="text-xs text-muted-foreground">
|
|
770
|
+
<strong className="text-foreground">Security Note:</strong> Your API keys are stored locally in your browser and never sent to our servers.
|
|
771
|
+
Keep them confidential and avoid sharing them publicly.
|
|
772
|
+
</div>
|
|
773
|
+
</div>
|
|
774
|
+
</div>
|
|
775
|
+
|
|
776
|
+
{/* ElevenLabs Voice Section */}
|
|
777
|
+
<div className="mt-4">
|
|
778
|
+
<h3 className="text-xs font-semibold text-muted-foreground uppercase tracking-wider mb-3">Voice Integration</h3>
|
|
779
|
+
<div className="card p-4">
|
|
780
|
+
<div className="flex items-center justify-between mb-3">
|
|
781
|
+
<label className="text-xs font-semibold text-foreground flex items-center gap-2">
|
|
782
|
+
<div className="w-6 h-6 rounded bg-purple-500/10 flex items-center justify-center">
|
|
783
|
+
<MicrophoneIcon className="w-3.5 h-3.5 text-purple-600" />
|
|
784
|
+
</div>
|
|
785
|
+
ElevenLabs API Key
|
|
786
|
+
</label>
|
|
787
|
+
<a
|
|
788
|
+
href="https://elevenlabs.io/api"
|
|
789
|
+
target="_blank"
|
|
790
|
+
rel="noopener noreferrer"
|
|
791
|
+
className="text-xs text-primary hover:text-primary/80 flex items-center gap-1 transition-colors"
|
|
792
|
+
>
|
|
793
|
+
Get Key <ArrowTopRightOnSquareIcon className="w-3 h-3" />
|
|
794
|
+
</a>
|
|
795
|
+
</div>
|
|
796
|
+
<div className="flex gap-2 mb-3">
|
|
797
|
+
<input
|
|
798
|
+
type="password"
|
|
799
|
+
className="input flex-1 text-sm py-2"
|
|
800
|
+
placeholder="sk_..."
|
|
801
|
+
value={elevenLabsApiKey || ''}
|
|
802
|
+
onChange={(e) => setElevenLabsApiKey(e.target.value || null)}
|
|
803
|
+
/>
|
|
804
|
+
{elevenLabsApiKey && (
|
|
805
|
+
<button
|
|
806
|
+
onClick={() => setElevenLabsApiKey(null)}
|
|
807
|
+
className="btn btn-ghost text-destructive text-xs px-3"
|
|
808
|
+
>
|
|
809
|
+
Clear
|
|
810
|
+
</button>
|
|
811
|
+
)}
|
|
812
|
+
</div>
|
|
813
|
+
<div className="flex items-start gap-2 p-2 bg-purple-500/5 rounded-lg border border-purple-500/10">
|
|
814
|
+
<InformationCircleIcon className="w-3 h-3 text-purple-500 mt-0.5 flex-shrink-0" />
|
|
815
|
+
<div className="text-xs text-muted-foreground">
|
|
816
|
+
<p>
|
|
817
|
+
<strong>Enables voice mode:</strong> When configured, a microphone button appears
|
|
818
|
+
allowing you to speak your messages and hear AI responses read aloud.
|
|
819
|
+
</p>
|
|
820
|
+
</div>
|
|
821
|
+
</div>
|
|
227
822
|
</div>
|
|
228
823
|
</div>
|
|
229
824
|
</div>
|
|
230
825
|
</div>
|
|
231
826
|
)}
|
|
232
827
|
|
|
233
|
-
{/* Messages */}
|
|
234
|
-
<div className="flex-1 overflow-y-auto
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
828
|
+
{/* ChatGPT-style Messages Container - ONLY this scrolls */}
|
|
829
|
+
<div className="flex-1 overflow-y-auto overflow-x-hidden">
|
|
830
|
+
<div className="max-w-5xl mx-auto px-4 py-6 space-y-6 min-h-full">
|
|
831
|
+
{chatMessages.length === 0 && !loading ? (
|
|
832
|
+
/* Welcome Screen */
|
|
833
|
+
<div className="flex flex-col items-center justify-center min-h-[calc(100vh-300px)] animate-fade-in">
|
|
834
|
+
<div className="w-16 h-16 rounded bg-gradient-to-br from-primary to-secondary flex items-center justify-center shadow-xl mb-6">
|
|
835
|
+
<SparklesIcon className="h-10 w-10 text-white" />
|
|
836
|
+
</div>
|
|
837
|
+
|
|
838
|
+
<h2 className="text-3xl font-bold text-foreground mb-3">Welcome to NitroStudio</h2>
|
|
839
|
+
<p className="text-muted-foreground text-center max-w-md mb-8">
|
|
840
|
+
Your AI-powered development environment for Model Context Protocol (MCP) servers.
|
|
841
|
+
Start a conversation or try a prompt below.
|
|
842
|
+
</p>
|
|
843
|
+
|
|
844
|
+
{/* Prompts Overview */}
|
|
845
|
+
{prompts.length > 0 && (
|
|
846
|
+
<div className="w-full max-w-2xl">
|
|
847
|
+
<div className="flex items-center gap-2 mb-4">
|
|
848
|
+
<SparklesIcon className="h-5 w-5 text-primary" />
|
|
849
|
+
<h3 className="text-lg font-semibold text-foreground">Available Prompts</h3>
|
|
850
|
+
<span className="text-sm text-muted-foreground">({prompts.length})</span>
|
|
851
|
+
</div>
|
|
852
|
+
|
|
853
|
+
<div className="grid grid-cols-1 md:grid-cols-2 gap-3">
|
|
854
|
+
{prompts.slice(0, 6).map((prompt) => (
|
|
855
|
+
<button
|
|
856
|
+
key={prompt.name}
|
|
857
|
+
onClick={() => {
|
|
858
|
+
setSelectedPrompt(prompt);
|
|
859
|
+
setPromptArgs({});
|
|
860
|
+
}}
|
|
861
|
+
className="card card-hover p-4 text-left group transition-all hover:scale-[1.02]"
|
|
862
|
+
>
|
|
863
|
+
<div className="flex items-start gap-3">
|
|
864
|
+
<div className="h-8 w-8 rounded-lg bg-primary/10 flex items-center justify-center group-hover:bg-primary/20 transition-colors flex-shrink-0">
|
|
865
|
+
<DocumentTextIcon className="h-4 w-4 text-primary" />
|
|
866
|
+
</div>
|
|
867
|
+
<div className="flex-1 min-w-0">
|
|
868
|
+
<h4 className="font-semibold text-foreground text-sm mb-1 truncate">
|
|
869
|
+
{prompt.name}
|
|
870
|
+
</h4>
|
|
871
|
+
<p className="text-xs text-muted-foreground line-clamp-2">
|
|
872
|
+
{prompt.description || 'No description'}
|
|
873
|
+
</p>
|
|
874
|
+
{prompt.arguments && prompt.arguments.length > 0 && (
|
|
875
|
+
<span className="badge badge-secondary text-xs mt-2 inline-block">
|
|
876
|
+
{prompt.arguments.length} arg{prompt.arguments.length !== 1 ? 's' : ''}
|
|
877
|
+
</span>
|
|
878
|
+
)}
|
|
879
|
+
</div>
|
|
880
|
+
</div>
|
|
881
|
+
</button>
|
|
882
|
+
))}
|
|
883
|
+
</div>
|
|
884
|
+
|
|
885
|
+
{prompts.length > 6 && (
|
|
886
|
+
<p className="text-xs text-muted-foreground text-center mt-4">
|
|
887
|
+
...and {prompts.length - 6} more. Visit the Prompts tab to see all.
|
|
888
|
+
</p>
|
|
889
|
+
)}
|
|
890
|
+
</div>
|
|
891
|
+
)}
|
|
892
|
+
|
|
893
|
+
{/* Suggestion Cards */}
|
|
894
|
+
<div className="w-full max-w-2xl mt-8">
|
|
895
|
+
<p className="text-sm text-muted-foreground mb-3">Or try asking:</p>
|
|
896
|
+
<div className="grid grid-cols-1 md:grid-cols-2 gap-3">
|
|
897
|
+
{[
|
|
898
|
+
'What tools are available?',
|
|
899
|
+
'Show me the health status',
|
|
900
|
+
'List all resources',
|
|
901
|
+
'Help me get started'
|
|
902
|
+
].map((suggestion) => (
|
|
903
|
+
<button
|
|
904
|
+
key={suggestion}
|
|
905
|
+
onClick={() => {
|
|
906
|
+
setInputValue(suggestion);
|
|
907
|
+
setTimeout(() => textareaRef.current?.focus(), 100);
|
|
908
|
+
}}
|
|
909
|
+
className="card card-hover p-3 text-left text-sm text-muted-foreground hover:text-foreground transition-colors"
|
|
910
|
+
>
|
|
911
|
+
"{suggestion}"
|
|
912
|
+
</button>
|
|
913
|
+
))}
|
|
914
|
+
</div>
|
|
915
|
+
</div>
|
|
916
|
+
</div>
|
|
917
|
+
) : (
|
|
918
|
+
<>
|
|
919
|
+
{chatMessages.map((msg, idx) => (
|
|
920
|
+
<ChatMessageComponent key={idx} message={msg} tools={tools} />
|
|
921
|
+
))}
|
|
922
|
+
{loading && (
|
|
923
|
+
<div className="flex gap-4 items-start animate-fade-in">
|
|
924
|
+
<div className="h-8 w-8 rounded-full bg-gradient-to-br from-primary to-secondary flex items-center justify-center flex-shrink-0 shadow-md">
|
|
925
|
+
<SparklesIcon className="h-5 w-5 text-white" />
|
|
926
|
+
</div>
|
|
927
|
+
<div className="flex-1 bg-card/50 backdrop-blur-sm rounded px-5 py-4 border border-border/50">
|
|
928
|
+
<div className="flex items-center gap-2">
|
|
929
|
+
<div className="flex gap-1">
|
|
930
|
+
<span className="w-2 h-2 bg-primary rounded-full animate-bounce" style={{ animationDelay: '0s' }}></span>
|
|
931
|
+
<span className="w-2 h-2 bg-primary rounded-full animate-bounce" style={{ animationDelay: '0.15s' }}></span>
|
|
932
|
+
<span className="w-2 h-2 bg-primary rounded-full animate-bounce" style={{ animationDelay: '0.3s' }}></span>
|
|
933
|
+
</div>
|
|
934
|
+
<span className="text-sm text-muted-foreground font-medium">Thinking...</span>
|
|
935
|
+
</div>
|
|
936
|
+
</div>
|
|
937
|
+
</div>
|
|
938
|
+
)}
|
|
939
|
+
</>
|
|
940
|
+
)}
|
|
941
|
+
<div ref={messagesEndRef} />
|
|
942
|
+
</div>
|
|
244
943
|
</div>
|
|
245
944
|
|
|
246
|
-
{/* Input */}
|
|
247
|
-
<div className="border-t border-
|
|
248
|
-
|
|
249
|
-
<div className=
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
className="absolute -top-2 -
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
945
|
+
{/* ChatGPT-style Input Area - Fixed at bottom */}
|
|
946
|
+
<div className="sticky bottom-0 border-t border-border/50 bg-background/95 backdrop-blur-md shadow-[0_-2px_10px_rgba(0,0,0,0.1)]">
|
|
947
|
+
<div className="max-w-4xl mx-auto px-4 pb-6">
|
|
948
|
+
<div className={`
|
|
949
|
+
rounded-xl border shadow-sm p-4 relative transition-all duration-300
|
|
950
|
+
${isRecording
|
|
951
|
+
? 'bg-primary/5 border-primary ring-1 ring-primary/50'
|
|
952
|
+
: 'bg-card border-border focus-within:ring-2 focus-within:ring-primary/20'
|
|
953
|
+
}
|
|
954
|
+
`}>
|
|
955
|
+
{isRecording && (
|
|
956
|
+
<div className="absolute -top-10 left-1/2 -translate-x-1/2 flex items-center gap-2 px-4 py-2 bg-primary text-primary-foreground rounded-full text-sm font-medium animate-bounce shadow-lg">
|
|
957
|
+
<MicrophoneIcon className="w-4 h-4" />
|
|
958
|
+
Listening...
|
|
959
|
+
</div>
|
|
960
|
+
)}
|
|
961
|
+
|
|
962
|
+
<div className="flex items-end gap-2">
|
|
963
|
+
<input
|
|
964
|
+
type="file"
|
|
965
|
+
ref={fileInputRef}
|
|
966
|
+
onChange={handleFileUpload}
|
|
967
|
+
accept="image/*,.pdf,.txt,.md,.json,.csv,.docx"
|
|
968
|
+
className="hidden"
|
|
969
|
+
/>
|
|
970
|
+
<button
|
|
971
|
+
onClick={() => fileInputRef.current?.click()}
|
|
972
|
+
className="p-2 text-muted-foreground hover:text-foreground hover:bg-muted rounded-lg transition-colors"
|
|
973
|
+
title="Attach file (max 20MB)"
|
|
974
|
+
>
|
|
975
|
+
<PhotoIcon className="h-5 w-5" />
|
|
976
|
+
</button>
|
|
977
|
+
|
|
978
|
+
<textarea
|
|
979
|
+
ref={textareaRef}
|
|
980
|
+
value={inputValue}
|
|
981
|
+
onChange={(e) => setInputValue(e.target.value)}
|
|
982
|
+
onKeyDown={(e) => {
|
|
983
|
+
if (e.key === 'Enter' && !e.shiftKey) {
|
|
984
|
+
e.preventDefault();
|
|
985
|
+
handleSend();
|
|
986
|
+
}
|
|
987
|
+
}}
|
|
988
|
+
placeholder={isRecording ? "Listening..." : "Message Nitro..."}
|
|
989
|
+
className="flex-1 bg-transparent border-0 focus:ring-0 resize-none p-2 min-h-[44px] max-h-[200px]"
|
|
990
|
+
rows={1}
|
|
991
|
+
disabled={loading || isRecording}
|
|
992
|
+
/>
|
|
993
|
+
|
|
994
|
+
{/* Microphone button - only show if ElevenLabs API key is configured */}
|
|
995
|
+
{elevenLabsApiKey && (
|
|
996
|
+
<button
|
|
997
|
+
onClick={() => setVoiceOverlayOpen(true)}
|
|
998
|
+
className="p-2 text-muted-foreground hover:text-primary hover:bg-primary/10 rounded-lg transition-colors"
|
|
999
|
+
title="Voice mode"
|
|
1000
|
+
>
|
|
1001
|
+
<MicrophoneIcon className="h-5 w-5" />
|
|
1002
|
+
</button>
|
|
1003
|
+
)}
|
|
1004
|
+
|
|
1005
|
+
{/* Send button */}
|
|
1006
|
+
<button
|
|
1007
|
+
onClick={handleSend}
|
|
1008
|
+
disabled={loading || (!inputValue.trim() && !currentFile)}
|
|
1009
|
+
className="p-2 text-primary hover:bg-primary/10 rounded-lg transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
|
|
1010
|
+
title="Send message"
|
|
1011
|
+
>
|
|
1012
|
+
<PaperAirplaneIcon className="h-5 w-5" />
|
|
1013
|
+
</button>
|
|
1014
|
+
</div>
|
|
261
1015
|
</div>
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
1016
|
+
|
|
1017
|
+
{/* Prompt Executor Modal */}
|
|
1018
|
+
{selectedPrompt && (
|
|
1019
|
+
<div
|
|
1020
|
+
className="fixed inset-0 z-50 flex items-center justify-center animate-fade-in"
|
|
1021
|
+
style={{ backgroundColor: 'rgba(0, 0, 0, 0.85)' }}
|
|
1022
|
+
onClick={() => setSelectedPrompt(null)}
|
|
1023
|
+
>
|
|
1024
|
+
<div
|
|
1025
|
+
className="bg-card rounded p-6 w-[600px] max-h-[80vh] overflow-auto border border-border shadow-2xl animate-scale-in"
|
|
1026
|
+
onClick={(e) => e.stopPropagation()}
|
|
1027
|
+
>
|
|
1028
|
+
<div className="flex items-center justify-between mb-4">
|
|
1029
|
+
<div className="flex items-center gap-3">
|
|
1030
|
+
<div className="h-10 w-10 rounded-lg bg-primary/10 flex items-center justify-center">
|
|
1031
|
+
<DocumentTextIcon className="h-5 w-5 text-primary" />
|
|
1032
|
+
</div>
|
|
1033
|
+
<h2 className="text-xl font-bold text-foreground">{selectedPrompt.name}</h2>
|
|
1034
|
+
</div>
|
|
1035
|
+
<button
|
|
1036
|
+
onClick={() => setSelectedPrompt(null)}
|
|
1037
|
+
className="btn btn-ghost w-10 h-10 p-0"
|
|
1038
|
+
>
|
|
1039
|
+
<XMarkIcon className="h-5 w-5" />
|
|
1040
|
+
</button>
|
|
1041
|
+
</div>
|
|
1042
|
+
|
|
1043
|
+
<p className="text-sm text-muted-foreground mb-6">
|
|
1044
|
+
{selectedPrompt.description || 'No description'}
|
|
1045
|
+
</p>
|
|
1046
|
+
|
|
1047
|
+
<div>
|
|
1048
|
+
{selectedPrompt.arguments && selectedPrompt.arguments.length > 0 ? (
|
|
1049
|
+
selectedPrompt.arguments.map((arg) => (
|
|
1050
|
+
<div key={arg.name} className="mb-4">
|
|
1051
|
+
<label className="block text-sm font-medium text-foreground mb-2">
|
|
1052
|
+
{arg.name}
|
|
1053
|
+
{arg.required && <span className="text-destructive ml-1">*</span>}
|
|
1054
|
+
</label>
|
|
1055
|
+
<input
|
|
1056
|
+
type="text"
|
|
1057
|
+
className="input"
|
|
1058
|
+
value={promptArgs[arg.name] || ''}
|
|
1059
|
+
onChange={(e) =>
|
|
1060
|
+
setPromptArgs({ ...promptArgs, [arg.name]: e.target.value })
|
|
1061
|
+
}
|
|
1062
|
+
required={arg.required}
|
|
1063
|
+
placeholder={arg.description || `Enter ${arg.name}`}
|
|
1064
|
+
/>
|
|
1065
|
+
{arg.description && (
|
|
1066
|
+
<p className="text-xs text-muted-foreground mt-1">{arg.description}</p>
|
|
1067
|
+
)}
|
|
1068
|
+
</div>
|
|
1069
|
+
))
|
|
1070
|
+
) : (
|
|
1071
|
+
<div className="bg-muted/30 rounded-lg p-4 mb-4">
|
|
1072
|
+
<p className="text-sm text-muted-foreground">No arguments required</p>
|
|
1073
|
+
</div>
|
|
1074
|
+
)}
|
|
1075
|
+
|
|
1076
|
+
<button
|
|
1077
|
+
onClick={handleExecutePrompt}
|
|
1078
|
+
className="btn btn-primary w-full gap-2"
|
|
1079
|
+
>
|
|
1080
|
+
<PlayIcon className="h-4 w-4" />
|
|
1081
|
+
Execute Prompt
|
|
1082
|
+
</button>
|
|
1083
|
+
</div>
|
|
1084
|
+
|
|
1085
|
+
</div>
|
|
1086
|
+
</div>
|
|
1087
|
+
)}
|
|
1088
|
+
|
|
1089
|
+
{/* Fullscreen Widget Modal */}
|
|
1090
|
+
{fullscreenWidget && (
|
|
1091
|
+
<div
|
|
1092
|
+
className="fixed inset-0 z-50 flex items-center justify-center"
|
|
1093
|
+
style={{ backgroundColor: 'rgba(0, 0, 0, 0.9)' }}
|
|
1094
|
+
>
|
|
1095
|
+
{/* Close Button */}
|
|
1096
|
+
<button
|
|
1097
|
+
onClick={() => setFullscreenWidget(null)}
|
|
1098
|
+
className="absolute top-4 right-4 z-60 p-3 rounded-lg bg-white/10 hover:bg-white/20 backdrop-blur-sm border border-white/20 transition-all"
|
|
1099
|
+
title="Exit fullscreen"
|
|
1100
|
+
>
|
|
1101
|
+
<XMarkIcon className="w-6 h-6 text-white" />
|
|
1102
|
+
</button>
|
|
1103
|
+
|
|
1104
|
+
{/* Widget Container */}
|
|
1105
|
+
<div className="w-full h-full p-8">
|
|
1106
|
+
<div className="w-full h-full rounded-xl overflow-hidden shadow-2xl">
|
|
1107
|
+
<WidgetRenderer uri={fullscreenWidget.uri} data={fullscreenWidget.data} className="widget-fullscreen" />
|
|
1108
|
+
</div>
|
|
1109
|
+
</div>
|
|
1110
|
+
</div>
|
|
1111
|
+
)}
|
|
292
1112
|
</div>
|
|
293
1113
|
</div>
|
|
1114
|
+
|
|
1115
|
+
{/* Voice Mode Overlay */}
|
|
1116
|
+
<VoiceOrbOverlay
|
|
1117
|
+
isOpen={voiceOverlayOpen}
|
|
1118
|
+
onClose={() => {
|
|
1119
|
+
setVoiceOverlayOpen(false);
|
|
1120
|
+
setVoiceModeEnabled(false);
|
|
1121
|
+
}}
|
|
1122
|
+
onTranscript={(text) => setInputValue(text)}
|
|
1123
|
+
onSendMessage={(text) => {
|
|
1124
|
+
setInputValue(text);
|
|
1125
|
+
setVoiceOverlayOpen(false);
|
|
1126
|
+
// Trigger send after a short delay to let state update
|
|
1127
|
+
setTimeout(() => handleSend(), 100);
|
|
1128
|
+
}}
|
|
1129
|
+
elevenLabsApiKey={elevenLabsApiKey || ''}
|
|
1130
|
+
isSpeaking={isSpeaking}
|
|
1131
|
+
spokenText={spokenText}
|
|
1132
|
+
/>
|
|
294
1133
|
</div>
|
|
295
1134
|
);
|
|
296
1135
|
}
|
|
297
1136
|
|
|
298
1137
|
function ChatMessageComponent({ message, tools }: { message: ChatMessage; tools: Tool[] }) {
|
|
299
|
-
if (message.role === 'tool') return null;
|
|
300
|
-
|
|
1138
|
+
if (message.role === 'tool') return null;
|
|
301
1139
|
const isUser = message.role === 'user';
|
|
302
1140
|
|
|
303
1141
|
return (
|
|
304
|
-
<div
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
1142
|
+
<div className="flex gap-4 items-start animate-fade-in group">
|
|
1143
|
+
{!isUser && (
|
|
1144
|
+
<div className="h-8 w-8 rounded-full bg-gradient-to-br from-primary to-secondary flex items-center justify-center flex-shrink-0 shadow-md group-hover:shadow-lg transition-shadow">
|
|
1145
|
+
<SparklesIcon className="h-5 w-5 text-white" />
|
|
1146
|
+
</div>
|
|
1147
|
+
)}
|
|
1148
|
+
{isUser && (
|
|
1149
|
+
<div className="h-8 w-8 rounded-full bg-gradient-to-br from-slate-600 to-slate-700 flex items-center justify-center flex-shrink-0 shadow-md group-hover:shadow-lg transition-shadow">
|
|
1150
|
+
<span className="text-white text-sm font-bold">You</span>
|
|
1151
|
+
</div>
|
|
1152
|
+
)}
|
|
1153
|
+
<div className="flex-1 min-w-0">
|
|
1154
|
+
{message.file && (
|
|
1155
|
+
<div className="mb-3 rounded-xl overflow-hidden border border-border/50 shadow-sm max-w-sm">
|
|
1156
|
+
{message.file.type.startsWith('image/') ? (
|
|
1157
|
+
<img src={message.file.data} alt={message.file.name} className="max-w-full" />
|
|
1158
|
+
) : (
|
|
1159
|
+
<div className="p-4 bg-muted/30 flex items-center gap-3">
|
|
1160
|
+
<div className="h-10 w-10 rounded-lg bg-primary/10 flex items-center justify-center">
|
|
1161
|
+
<DocumentTextIcon className="h-5 w-5 text-primary" />
|
|
1162
|
+
</div>
|
|
1163
|
+
<div className="flex-1 min-w-0">
|
|
1164
|
+
<p className="text-sm font-medium text-foreground truncate">{message.file.name}</p>
|
|
1165
|
+
<p className="text-xs text-muted-foreground">{message.file.type}</p>
|
|
1166
|
+
</div>
|
|
1167
|
+
</div>
|
|
1168
|
+
)}
|
|
1169
|
+
</div>
|
|
1170
|
+
)}
|
|
1171
|
+
{message.content && (
|
|
1172
|
+
<div className="text-sm leading-relaxed mb-4">
|
|
1173
|
+
{isUser ? (
|
|
1174
|
+
<div className="whitespace-pre-wrap text-foreground/90">{message.content}</div>
|
|
1175
|
+
) : (
|
|
1176
|
+
<MarkdownRenderer content={message.content} />
|
|
1177
|
+
)}
|
|
1178
|
+
</div>
|
|
314
1179
|
)}
|
|
315
|
-
<p className="text-sm whitespace-pre-wrap">{message.content}</p>
|
|
316
|
-
|
|
317
|
-
{/* Tool Calls */}
|
|
318
1180
|
{message.toolCalls && message.toolCalls.length > 0 && (
|
|
319
|
-
<div className="
|
|
320
|
-
{message.toolCalls.map((
|
|
321
|
-
<ToolCallComponent key={
|
|
1181
|
+
<div className="space-y-3">
|
|
1182
|
+
{message.toolCalls.map((tc: ToolCall) => (
|
|
1183
|
+
<ToolCallComponent key={tc.id} toolCall={tc} tools={tools} />
|
|
322
1184
|
))}
|
|
323
1185
|
</div>
|
|
324
1186
|
)}
|
|
@@ -328,63 +1190,60 @@ function ChatMessageComponent({ message, tools }: { message: ChatMessage; tools:
|
|
|
328
1190
|
}
|
|
329
1191
|
|
|
330
1192
|
function ToolCallComponent({ toolCall, tools }: { toolCall: ToolCall; tools: Tool[] }) {
|
|
1193
|
+
const [showArgs, setShowArgs] = useState(false);
|
|
331
1194
|
const tool = tools.find((t) => t.name === toolCall.name);
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
tool?.
|
|
336
|
-
tool?.outputTemplate ||
|
|
337
|
-
tool?._meta?.['openai/outputTemplate'] ||
|
|
1195
|
+
|
|
1196
|
+
const componentUri =
|
|
1197
|
+
tool?.widget?.route ||
|
|
1198
|
+
tool?.outputTemplate ||
|
|
1199
|
+
tool?._meta?.['openai/outputTemplate'] ||
|
|
338
1200
|
tool?._meta?.['ui/template'];
|
|
339
|
-
|
|
340
|
-
// Get result data from toolCall and unwrap if needed
|
|
1201
|
+
|
|
341
1202
|
let widgetData = toolCall.result || toolCall.arguments;
|
|
342
|
-
|
|
343
|
-
// Unwrap if response was wrapped by TransformInterceptor
|
|
344
|
-
// Check if it has the interceptor's structure: { success, data, metadata }
|
|
345
|
-
if (widgetData && typeof widgetData === 'object' &&
|
|
346
|
-
widgetData.success !== undefined && widgetData.data !== undefined) {
|
|
347
|
-
widgetData = widgetData.data; // Return the unwrapped data
|
|
348
|
-
}
|
|
349
1203
|
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
tool
|
|
355
|
-
});
|
|
1204
|
+
if (widgetData && typeof widgetData === 'object' &&
|
|
1205
|
+
widgetData.success !== undefined && widgetData.data !== undefined) {
|
|
1206
|
+
widgetData = widgetData.data;
|
|
1207
|
+
}
|
|
356
1208
|
|
|
357
1209
|
return (
|
|
358
|
-
<div className="
|
|
359
|
-
backgroundColor: 'hsl(var(--muted))',
|
|
360
|
-
borderColor: 'hsl(var(--border))'
|
|
361
|
-
}}>
|
|
362
|
-
<div className="flex items-center gap-2 mb-2">
|
|
363
|
-
<span>🔧</span>
|
|
364
|
-
<span className="font-medium text-sm">{toolCall.name}</span>
|
|
365
|
-
</div>
|
|
366
|
-
|
|
367
|
-
{/* Arguments */}
|
|
368
|
-
<details className="text-xs" style={{ color: 'hsl(var(--muted-foreground))' }}>
|
|
369
|
-
<summary className="cursor-pointer">Arguments</summary>
|
|
370
|
-
<pre className="mt-2 p-2 rounded overflow-auto" style={{
|
|
371
|
-
backgroundColor: 'hsl(var(--background))'
|
|
372
|
-
}}>
|
|
373
|
-
{JSON.stringify(toolCall.arguments, null, 2)}
|
|
374
|
-
</pre>
|
|
375
|
-
</details>
|
|
376
|
-
|
|
377
|
-
{/* Widget if available */}
|
|
1210
|
+
<div className="relative group/widget">
|
|
378
1211
|
{componentUri && widgetData && (
|
|
379
|
-
<div className="
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
1212
|
+
<div className="rounded-lg overflow-hidden max-w-5xl">
|
|
1213
|
+
<WidgetRenderer uri={componentUri} data={widgetData} className="widget-in-chat" />
|
|
1214
|
+
</div>
|
|
1215
|
+
)}
|
|
1216
|
+
<button
|
|
1217
|
+
onClick={() => setShowArgs(!showArgs)}
|
|
1218
|
+
className="absolute top-2 right-2 w-8 h-8 rounded-lg flex items-center justify-center bg-background/80 backdrop-blur-sm border border-border/50 hover:bg-background hover:border-border transition-all opacity-0 group-hover/widget:opacity-100 shadow-sm z-10"
|
|
1219
|
+
title="View tool details"
|
|
1220
|
+
>
|
|
1221
|
+
<EllipsisVerticalIcon className="h-4 w-4 text-muted-foreground" />
|
|
1222
|
+
</button>
|
|
1223
|
+
{showArgs && (
|
|
1224
|
+
<div className="absolute top-12 right-2 w-96 max-w-[calc(100%-1rem)] bg-card rounded-xl border border-border shadow-2xl p-4 animate-fade-in z-20">
|
|
1225
|
+
<div className="flex items-center justify-between mb-3">
|
|
1226
|
+
<div className="flex items-center gap-2">
|
|
1227
|
+
<div className="w-6 h-6 rounded-md bg-primary/10 flex items-center justify-center">
|
|
1228
|
+
<WrenchScrewdriverIcon className="w-3.5 h-3.5 text-primary" />
|
|
1229
|
+
</div>
|
|
1230
|
+
<span className="font-semibold text-sm text-foreground">{toolCall.name}</span>
|
|
1231
|
+
</div>
|
|
1232
|
+
<button
|
|
1233
|
+
onClick={() => setShowArgs(false)}
|
|
1234
|
+
className="w-6 h-6 rounded-md flex items-center justify-center hover:bg-muted transition-colors"
|
|
1235
|
+
>
|
|
1236
|
+
<XMarkIcon className="h-4 w-4 text-muted-foreground" />
|
|
1237
|
+
</button>
|
|
1238
|
+
</div>
|
|
1239
|
+
<div>
|
|
1240
|
+
<p className="text-xs font-medium text-muted-foreground mb-2">Arguments:</p>
|
|
1241
|
+
<pre className="p-3 rounded-lg overflow-auto bg-background border border-border/30 font-mono text-xs text-foreground max-h-60">
|
|
1242
|
+
{JSON.stringify(toolCall.arguments, null, 2)}
|
|
1243
|
+
</pre>
|
|
1244
|
+
</div>
|
|
385
1245
|
</div>
|
|
386
1246
|
)}
|
|
387
1247
|
</div>
|
|
388
1248
|
);
|
|
389
1249
|
}
|
|
390
|
-
|