nitrostack 1.0.15 → 1.0.17
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/README.md +1 -1
- package/dist/cli/commands/dev.d.ts.map +1 -1
- package/dist/cli/commands/dev.js +2 -1
- package/dist/cli/commands/dev.js.map +1 -1
- package/dist/cli/mcp-dev-wrapper.js +30 -17
- package/dist/cli/mcp-dev-wrapper.js.map +1 -1
- package/dist/core/app-decorator.js +2 -2
- package/dist/core/app-decorator.js.map +1 -1
- package/dist/core/builders.js +2 -2
- package/dist/core/builders.js.map +1 -1
- package/dist/core/resource.js +1 -1
- package/dist/core/resource.js.map +1 -1
- package/dist/core/server.js +2 -2
- package/dist/core/server.js.map +1 -1
- package/dist/core/transports/http-server.d.ts.map +1 -1
- package/dist/core/transports/http-server.js +21 -1
- package/dist/core/transports/http-server.js.map +1 -1
- package/dist/core/types.d.ts +1 -1
- package/dist/core/types.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/studio/app/api/chat/route.ts +155 -28
- package/src/studio/app/api/init/route.ts +28 -4
- package/src/studio/app/auth/page.tsx +13 -9
- package/src/studio/app/chat/page.tsx +599 -133
- package/src/studio/app/health/page.tsx +101 -99
- package/src/studio/app/layout.tsx +24 -4
- package/src/studio/app/page.tsx +61 -56
- package/src/studio/app/ping/page.tsx +13 -8
- package/src/studio/app/prompts/page.tsx +72 -70
- package/src/studio/app/resources/page.tsx +88 -86
- package/src/studio/app/settings/page.tsx +270 -0
- package/src/studio/components/EnlargeModal.tsx +21 -15
- package/src/studio/components/LogMessage.tsx +153 -0
- package/src/studio/components/MarkdownRenderer.tsx +410 -0
- package/src/studio/components/Sidebar.tsx +197 -35
- package/src/studio/components/ToolCard.tsx +27 -9
- package/src/studio/components/WidgetRenderer.tsx +4 -2
- package/src/studio/lib/http-client-transport.ts +222 -0
- package/src/studio/lib/llm-service.ts +119 -0
- package/src/studio/lib/log-manager.ts +76 -0
- package/src/studio/lib/mcp-client.ts +103 -13
- package/src/studio/package-lock.json +3129 -0
- package/src/studio/package.json +1 -0
- package/templates/typescript-auth/README.md +3 -1
- package/templates/typescript-auth/src/db/database.ts +5 -8
- package/templates/typescript-auth/src/index.ts +13 -2
- package/templates/typescript-auth/src/modules/addresses/addresses.tools.ts +49 -6
- package/templates/typescript-auth/src/modules/cart/cart.tools.ts +13 -17
- package/templates/typescript-auth/src/modules/orders/orders.tools.ts +38 -16
- package/templates/typescript-auth/src/modules/products/products.tools.ts +4 -4
- package/templates/typescript-auth/src/widgets/app/order-confirmation/page.tsx +25 -0
- package/templates/typescript-auth/src/widgets/app/products-grid/page.tsx +26 -1
- package/templates/typescript-auth-api-key/README.md +3 -1
- package/templates/typescript-auth-api-key/src/index.ts +11 -3
- package/templates/typescript-starter/README.md +3 -1
|
@@ -4,7 +4,8 @@ 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 type { ChatMessage, Tool, ToolCall, Prompt } from '@/lib/types';
|
|
8
9
|
import {
|
|
9
10
|
Bot,
|
|
10
11
|
Settings,
|
|
@@ -13,7 +14,12 @@ import {
|
|
|
13
14
|
Send,
|
|
14
15
|
Wrench,
|
|
15
16
|
Save,
|
|
16
|
-
X
|
|
17
|
+
X,
|
|
18
|
+
Sparkles,
|
|
19
|
+
FileText,
|
|
20
|
+
Play,
|
|
21
|
+
ExternalLink,
|
|
22
|
+
Info
|
|
17
23
|
} from 'lucide-react';
|
|
18
24
|
|
|
19
25
|
export default function ChatPage() {
|
|
@@ -25,26 +31,66 @@ export default function ChatPage() {
|
|
|
25
31
|
setCurrentProvider,
|
|
26
32
|
currentImage,
|
|
27
33
|
setCurrentImage,
|
|
28
|
-
jwtToken,
|
|
29
|
-
apiKey: mcpApiKey, // Rename to avoid conflict with LLM apiKey
|
|
30
34
|
tools,
|
|
31
35
|
setTools,
|
|
32
36
|
} = useStudioStore();
|
|
37
|
+
|
|
38
|
+
// Get jwtToken and apiKey dynamically to ensure we always have the latest value
|
|
39
|
+
const getAuthTokens = () => {
|
|
40
|
+
const state = useStudioStore.getState();
|
|
41
|
+
// Check both jwtToken and OAuth token (from OAuth tab)
|
|
42
|
+
const jwtToken = state.jwtToken || state.oauthState?.currentToken;
|
|
43
|
+
return {
|
|
44
|
+
jwtToken,
|
|
45
|
+
mcpApiKey: state.apiKey,
|
|
46
|
+
};
|
|
47
|
+
};
|
|
33
48
|
|
|
34
49
|
const [inputValue, setInputValue] = useState('');
|
|
35
50
|
const [loading, setLoading] = useState(false);
|
|
36
51
|
const [showSettings, setShowSettings] = useState(false);
|
|
52
|
+
const [prompts, setPrompts] = useState<Prompt[]>([]);
|
|
53
|
+
const [selectedPrompt, setSelectedPrompt] = useState<Prompt | null>(null);
|
|
54
|
+
const [promptArgs, setPromptArgs] = useState<Record<string, string>>({});
|
|
37
55
|
const messagesEndRef = useRef<HTMLDivElement>(null);
|
|
38
56
|
const fileInputRef = useRef<HTMLInputElement>(null);
|
|
57
|
+
const textareaRef = useRef<HTMLTextAreaElement>(null);
|
|
39
58
|
|
|
40
59
|
useEffect(() => {
|
|
41
60
|
loadTools();
|
|
61
|
+
loadPrompts();
|
|
62
|
+
|
|
63
|
+
// Check if there's a suggested message from localStorage
|
|
64
|
+
if (typeof window !== 'undefined') {
|
|
65
|
+
const chatInput = window.localStorage.getItem('chatInput');
|
|
66
|
+
if (chatInput) {
|
|
67
|
+
setInputValue(chatInput);
|
|
68
|
+
window.localStorage.removeItem('chatInput');
|
|
69
|
+
// Focus after a short delay to ensure component is mounted
|
|
70
|
+
setTimeout(() => textareaRef.current?.focus(), 100);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
42
73
|
}, []);
|
|
43
74
|
|
|
44
75
|
useEffect(() => {
|
|
45
76
|
messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' });
|
|
46
77
|
}, [chatMessages]);
|
|
47
78
|
|
|
79
|
+
// Auto-focus textarea on mount and after sending
|
|
80
|
+
useEffect(() => {
|
|
81
|
+
textareaRef.current?.focus();
|
|
82
|
+
}, [chatMessages, loading]);
|
|
83
|
+
|
|
84
|
+
// Auto-resize textarea based on content
|
|
85
|
+
useEffect(() => {
|
|
86
|
+
const textarea = textareaRef.current;
|
|
87
|
+
if (textarea) {
|
|
88
|
+
textarea.style.height = '44px'; // Reset to min height
|
|
89
|
+
const scrollHeight = textarea.scrollHeight;
|
|
90
|
+
textarea.style.height = Math.min(scrollHeight, 200) + 'px'; // Max 200px
|
|
91
|
+
}
|
|
92
|
+
}, [inputValue]);
|
|
93
|
+
|
|
48
94
|
const loadTools = async () => {
|
|
49
95
|
try {
|
|
50
96
|
const data = await api.getTools();
|
|
@@ -54,6 +100,76 @@ export default function ChatPage() {
|
|
|
54
100
|
}
|
|
55
101
|
};
|
|
56
102
|
|
|
103
|
+
const loadPrompts = async () => {
|
|
104
|
+
try {
|
|
105
|
+
const data = await api.getPrompts();
|
|
106
|
+
setPrompts(data.prompts || []);
|
|
107
|
+
} catch (error) {
|
|
108
|
+
console.error('Failed to load prompts:', error);
|
|
109
|
+
}
|
|
110
|
+
};
|
|
111
|
+
|
|
112
|
+
const handleExecutePrompt = async () => {
|
|
113
|
+
if (!selectedPrompt) return;
|
|
114
|
+
|
|
115
|
+
// Close modal
|
|
116
|
+
const prompt = selectedPrompt;
|
|
117
|
+
const args = { ...promptArgs };
|
|
118
|
+
setSelectedPrompt(null);
|
|
119
|
+
setPromptArgs({});
|
|
120
|
+
|
|
121
|
+
// Build user message showing what prompt was executed
|
|
122
|
+
let userMessageContent = `Execute prompt: ${prompt.name}`;
|
|
123
|
+
if (Object.keys(args).length > 0) {
|
|
124
|
+
userMessageContent += `\nArguments: ${JSON.stringify(args, null, 2)}`;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// Add user message to chat
|
|
128
|
+
addChatMessage({
|
|
129
|
+
role: 'user',
|
|
130
|
+
content: userMessageContent,
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
setLoading(true);
|
|
134
|
+
|
|
135
|
+
try {
|
|
136
|
+
// Execute the prompt directly via API
|
|
137
|
+
const result = await api.executePrompt(prompt.name, args);
|
|
138
|
+
|
|
139
|
+
// Add the prompt result as an assistant message
|
|
140
|
+
if (result.messages && result.messages.length > 0) {
|
|
141
|
+
// Combine all prompt messages into one assistant message
|
|
142
|
+
const combinedContent = result.messages
|
|
143
|
+
.map((msg: any) => {
|
|
144
|
+
const content = typeof msg.content === 'string'
|
|
145
|
+
? msg.content
|
|
146
|
+
: msg.content?.text || JSON.stringify(msg.content);
|
|
147
|
+
return `[${msg.role.toUpperCase()}]\n${content}`;
|
|
148
|
+
})
|
|
149
|
+
.join('\n\n');
|
|
150
|
+
|
|
151
|
+
addChatMessage({
|
|
152
|
+
role: 'assistant',
|
|
153
|
+
content: combinedContent,
|
|
154
|
+
});
|
|
155
|
+
} else {
|
|
156
|
+
addChatMessage({
|
|
157
|
+
role: 'assistant',
|
|
158
|
+
content: 'Prompt executed successfully, but returned no messages.',
|
|
159
|
+
});
|
|
160
|
+
}
|
|
161
|
+
} catch (error) {
|
|
162
|
+
console.error('Failed to execute prompt:', error);
|
|
163
|
+
addChatMessage({
|
|
164
|
+
role: 'assistant',
|
|
165
|
+
content: `Error executing prompt: ${error instanceof Error ? error.message : 'Unknown error'}`,
|
|
166
|
+
});
|
|
167
|
+
} finally {
|
|
168
|
+
setLoading(false);
|
|
169
|
+
setTimeout(() => textareaRef.current?.focus(), 100);
|
|
170
|
+
}
|
|
171
|
+
};
|
|
172
|
+
|
|
57
173
|
const handleImageUpload = (e: React.ChangeEvent<HTMLInputElement>) => {
|
|
58
174
|
const file = e.target.files?.[0];
|
|
59
175
|
if (!file) return;
|
|
@@ -124,7 +240,11 @@ export default function ChatPage() {
|
|
|
124
240
|
return cleaned;
|
|
125
241
|
});
|
|
126
242
|
|
|
243
|
+
// Get fresh auth tokens from store
|
|
244
|
+
const { jwtToken, mcpApiKey } = getAuthTokens();
|
|
245
|
+
|
|
127
246
|
console.log('Sending messages to API:', cleanedMessages);
|
|
247
|
+
console.log('Auth tokens:', { hasJwtToken: !!jwtToken, hasMcpApiKey: !!mcpApiKey });
|
|
128
248
|
console.log('Original messages:', messagesToSend);
|
|
129
249
|
console.log('Cleaned messages JSON:', JSON.stringify(cleanedMessages));
|
|
130
250
|
|
|
@@ -229,6 +349,9 @@ export default function ChatPage() {
|
|
|
229
349
|
// Use provided messages or fall back to store (for recursive calls)
|
|
230
350
|
const messagesToUse = messages || chatMessages;
|
|
231
351
|
|
|
352
|
+
// Get fresh auth tokens from store (token may have been updated by login)
|
|
353
|
+
const { jwtToken, mcpApiKey } = getAuthTokens();
|
|
354
|
+
|
|
232
355
|
// Clean messages before sending
|
|
233
356
|
const cleanedMessages = messagesToUse.map(msg => {
|
|
234
357
|
const cleaned: any = {
|
|
@@ -248,6 +371,7 @@ export default function ChatPage() {
|
|
|
248
371
|
});
|
|
249
372
|
|
|
250
373
|
console.log('Continue with cleaned messages:', JSON.stringify(cleanedMessages));
|
|
374
|
+
console.log('Continue auth tokens:', { hasJwtToken: !!jwtToken, hasMcpApiKey: !!mcpApiKey });
|
|
251
375
|
|
|
252
376
|
const response = await api.chat({
|
|
253
377
|
provider: currentProvider,
|
|
@@ -297,145 +421,442 @@ export default function ChatPage() {
|
|
|
297
421
|
};
|
|
298
422
|
|
|
299
423
|
return (
|
|
300
|
-
<div className="
|
|
301
|
-
{/* Header */}
|
|
302
|
-
<div className="border-b border-border
|
|
424
|
+
<div className="fixed inset-0 flex flex-col bg-background" style={{ left: 'var(--sidebar-width, 15rem)' }}>
|
|
425
|
+
{/* Sticky Header */}
|
|
426
|
+
<div className="sticky top-0 z-10 border-b border-border/50 px-3 sm:px-6 py-3 flex flex-col sm:flex-row items-start sm:items-center justify-between bg-card/80 backdrop-blur-md shadow-sm gap-3 sm:gap-0">
|
|
303
427
|
<div className="flex items-center gap-3">
|
|
304
|
-
<div className="w-
|
|
305
|
-
<Bot className="w-
|
|
428
|
+
<div className="w-8 h-8 rounded-lg bg-gradient-to-br from-primary to-amber-500 flex items-center justify-center shadow-md">
|
|
429
|
+
<Bot className="w-5 h-5 text-white" strokeWidth={2.5} />
|
|
306
430
|
</div>
|
|
307
431
|
<div>
|
|
308
|
-
<h1 className="text-
|
|
309
|
-
<p className="text-muted-foreground text-sm">Chat with tools integration</p>
|
|
432
|
+
<h1 className="text-lg font-bold text-foreground">AI Chat</h1>
|
|
310
433
|
</div>
|
|
311
434
|
</div>
|
|
312
435
|
|
|
313
|
-
<div className="flex items-center gap-2">
|
|
436
|
+
<div className="flex items-center gap-2 w-full sm:w-auto">
|
|
314
437
|
<select
|
|
315
438
|
value={currentProvider}
|
|
316
439
|
onChange={(e) => setCurrentProvider(e.target.value as 'openai' | 'gemini')}
|
|
317
|
-
className="input w-
|
|
440
|
+
className="input text-sm px-3 py-1.5 w-full sm:w-28 flex-1 sm:flex-none"
|
|
318
441
|
>
|
|
319
442
|
<option value="gemini">Gemini</option>
|
|
320
443
|
<option value="openai">OpenAI</option>
|
|
321
444
|
</select>
|
|
322
445
|
<button
|
|
323
446
|
onClick={() => setShowSettings(!showSettings)}
|
|
324
|
-
className={`
|
|
447
|
+
className={`w-8 h-8 rounded-lg flex items-center justify-center transition-all flex-shrink-0 ${
|
|
448
|
+
showSettings
|
|
449
|
+
? 'bg-primary/10 text-primary ring-1 ring-primary/30'
|
|
450
|
+
: 'bg-muted/50 text-muted-foreground hover:bg-muted hover:text-foreground'
|
|
451
|
+
}`}
|
|
452
|
+
title="Settings"
|
|
325
453
|
>
|
|
326
454
|
<Settings className="w-4 h-4" />
|
|
327
455
|
</button>
|
|
328
|
-
<button
|
|
456
|
+
<button
|
|
457
|
+
onClick={clearChat}
|
|
458
|
+
className="w-8 h-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"
|
|
459
|
+
title="Clear chat"
|
|
460
|
+
>
|
|
329
461
|
<Trash2 className="w-4 h-4" />
|
|
330
462
|
</button>
|
|
331
463
|
</div>
|
|
332
464
|
</div>
|
|
333
465
|
|
|
334
|
-
{/* Settings Panel */}
|
|
466
|
+
{/* Enhanced Settings Panel */}
|
|
335
467
|
{showSettings && (
|
|
336
|
-
<div className="border-b border-border
|
|
337
|
-
<
|
|
338
|
-
<
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
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>
|
|
468
|
+
<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">
|
|
469
|
+
<div className="max-w-4xl mx-auto">
|
|
470
|
+
<div className="flex items-start justify-between mb-4">
|
|
471
|
+
<div>
|
|
472
|
+
<h3 className="text-sm font-semibold text-foreground flex items-center gap-2">
|
|
473
|
+
<Settings className="w-4 h-4" />
|
|
474
|
+
API Configuration
|
|
475
|
+
</h3>
|
|
476
|
+
<p className="text-xs text-muted-foreground mt-1">Configure your AI provider API keys to enable chat functionality</p>
|
|
355
477
|
</div>
|
|
356
478
|
</div>
|
|
357
|
-
|
|
358
|
-
<
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
479
|
+
|
|
480
|
+
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
|
481
|
+
{/* OpenAI Section */}
|
|
482
|
+
<div className="card p-4">
|
|
483
|
+
<div className="flex items-center justify-between mb-3">
|
|
484
|
+
<label className="text-xs font-semibold text-foreground flex items-center gap-2">
|
|
485
|
+
<div className="w-6 h-6 rounded bg-green-500/10 flex items-center justify-center">
|
|
486
|
+
<span className="text-xs font-bold text-green-600">AI</span>
|
|
487
|
+
</div>
|
|
488
|
+
OpenAI API Key
|
|
489
|
+
</label>
|
|
490
|
+
<a
|
|
491
|
+
href="https://platform.openai.com/api-keys"
|
|
492
|
+
target="_blank"
|
|
493
|
+
rel="noopener noreferrer"
|
|
494
|
+
className="text-xs text-primary hover:text-primary/80 flex items-center gap-1 transition-colors"
|
|
495
|
+
>
|
|
496
|
+
Get Key <ExternalLink className="w-3 h-3" />
|
|
497
|
+
</a>
|
|
498
|
+
</div>
|
|
499
|
+
<div className="flex gap-2 mb-3">
|
|
500
|
+
<input
|
|
501
|
+
id="openai-api-key"
|
|
502
|
+
type="password"
|
|
503
|
+
className="input flex-1 text-sm py-2"
|
|
504
|
+
placeholder="sk-proj-..."
|
|
505
|
+
/>
|
|
506
|
+
<button onClick={() => saveApiKey('openai')} className="btn btn-primary text-xs px-4 py-2">
|
|
507
|
+
<Save className="w-3 h-3 mr-1" />
|
|
508
|
+
Save
|
|
509
|
+
</button>
|
|
510
|
+
</div>
|
|
511
|
+
<div className="flex items-start gap-2 p-2 bg-blue-500/5 rounded-lg border border-blue-500/10">
|
|
512
|
+
<Info className="w-3 h-3 text-blue-500 mt-0.5 flex-shrink-0" />
|
|
513
|
+
<div className="text-xs text-muted-foreground">
|
|
514
|
+
<p className="mb-1">
|
|
515
|
+
<strong>How to get:</strong> Sign up at{' '}
|
|
516
|
+
<a href="https://platform.openai.com/signup" target="_blank" rel="noopener noreferrer" className="text-primary hover:underline">
|
|
517
|
+
OpenAI Platform
|
|
518
|
+
</a>
|
|
519
|
+
, navigate to API Keys, and create a new secret key.
|
|
520
|
+
</p>
|
|
521
|
+
<a
|
|
522
|
+
href="https://help.openai.com/en/articles/4936850-where-do-i-find-my-openai-api-key"
|
|
523
|
+
target="_blank"
|
|
524
|
+
rel="noopener noreferrer"
|
|
525
|
+
className="text-primary hover:underline inline-flex items-center gap-1"
|
|
526
|
+
>
|
|
527
|
+
View Guide <ExternalLink className="w-2.5 h-2.5" />
|
|
528
|
+
</a>
|
|
529
|
+
</div>
|
|
530
|
+
</div>
|
|
531
|
+
</div>
|
|
532
|
+
|
|
533
|
+
{/* Gemini Section */}
|
|
534
|
+
<div className="card p-4">
|
|
535
|
+
<div className="flex items-center justify-between mb-3">
|
|
536
|
+
<label className="text-xs font-semibold text-foreground flex items-center gap-2">
|
|
537
|
+
<div className="w-6 h-6 rounded bg-blue-500/10 flex items-center justify-center">
|
|
538
|
+
<span className="text-xs font-bold text-blue-600">G</span>
|
|
539
|
+
</div>
|
|
540
|
+
Gemini API Key
|
|
541
|
+
</label>
|
|
542
|
+
<a
|
|
543
|
+
href="https://aistudio.google.com/app/apikey"
|
|
544
|
+
target="_blank"
|
|
545
|
+
rel="noopener noreferrer"
|
|
546
|
+
className="text-xs text-primary hover:text-primary/80 flex items-center gap-1 transition-colors"
|
|
547
|
+
>
|
|
548
|
+
Get Key <ExternalLink className="w-3 h-3" />
|
|
549
|
+
</a>
|
|
550
|
+
</div>
|
|
551
|
+
<div className="flex gap-2 mb-3">
|
|
552
|
+
<input
|
|
553
|
+
id="gemini-api-key"
|
|
554
|
+
type="password"
|
|
555
|
+
className="input flex-1 text-sm py-2"
|
|
556
|
+
placeholder="AIza..."
|
|
557
|
+
/>
|
|
558
|
+
<button onClick={() => saveApiKey('gemini')} className="btn btn-primary text-xs px-4 py-2">
|
|
559
|
+
<Save className="w-3 h-3 mr-1" />
|
|
560
|
+
Save
|
|
561
|
+
</button>
|
|
562
|
+
</div>
|
|
563
|
+
<div className="flex items-start gap-2 p-2 bg-blue-500/5 rounded-lg border border-blue-500/10">
|
|
564
|
+
<Info className="w-3 h-3 text-blue-500 mt-0.5 flex-shrink-0" />
|
|
565
|
+
<div className="text-xs text-muted-foreground">
|
|
566
|
+
<p className="mb-1">
|
|
567
|
+
<strong>How to get:</strong> Visit{' '}
|
|
568
|
+
<a href="https://aistudio.google.com" target="_blank" rel="noopener noreferrer" className="text-primary hover:underline">
|
|
569
|
+
Google AI Studio
|
|
570
|
+
</a>
|
|
571
|
+
, sign in with your Google account, and click "Get API key".
|
|
572
|
+
</p>
|
|
573
|
+
<a
|
|
574
|
+
href="https://ai.google.dev/gemini-api/docs/api-key"
|
|
575
|
+
target="_blank"
|
|
576
|
+
rel="noopener noreferrer"
|
|
577
|
+
className="text-primary hover:underline inline-flex items-center gap-1"
|
|
578
|
+
>
|
|
579
|
+
View Guide <ExternalLink className="w-2.5 h-2.5" />
|
|
580
|
+
</a>
|
|
581
|
+
</div>
|
|
582
|
+
</div>
|
|
583
|
+
</div>
|
|
584
|
+
</div>
|
|
585
|
+
|
|
586
|
+
{/* Security Notice */}
|
|
587
|
+
<div className="mt-4 p-3 bg-amber-500/5 rounded-lg border border-amber-500/10">
|
|
588
|
+
<div className="flex items-start gap-2">
|
|
589
|
+
<Info className="w-4 h-4 text-amber-500 mt-0.5 flex-shrink-0" />
|
|
590
|
+
<div className="text-xs text-muted-foreground">
|
|
591
|
+
<strong className="text-foreground">Security Note:</strong> Your API keys are stored locally in your browser and never sent to our servers.
|
|
592
|
+
Keep them confidential and avoid sharing them publicly.
|
|
593
|
+
</div>
|
|
370
594
|
</div>
|
|
371
595
|
</div>
|
|
372
596
|
</div>
|
|
373
597
|
</div>
|
|
374
598
|
)}
|
|
375
599
|
|
|
376
|
-
{/* Messages */}
|
|
377
|
-
<div className="flex-1 overflow-y-auto
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
600
|
+
{/* ChatGPT-style Messages Container - ONLY this scrolls */}
|
|
601
|
+
<div className="flex-1 overflow-y-auto overflow-x-hidden">
|
|
602
|
+
<div className="max-w-3xl mx-auto px-4 py-6 space-y-6 min-h-full">
|
|
603
|
+
{chatMessages.length === 0 && !loading ? (
|
|
604
|
+
/* Welcome Screen */
|
|
605
|
+
<div className="flex flex-col items-center justify-center min-h-[calc(100vh-300px)] animate-fade-in">
|
|
606
|
+
<div className="w-16 h-16 rounded-2xl bg-gradient-to-br from-primary to-amber-500 flex items-center justify-center shadow-xl mb-6">
|
|
607
|
+
<Bot className="w-10 h-10 text-white" strokeWidth={2.5} />
|
|
608
|
+
</div>
|
|
609
|
+
|
|
610
|
+
<h2 className="text-3xl font-bold text-foreground mb-3">Welcome to NitroStudio</h2>
|
|
611
|
+
<p className="text-muted-foreground text-center max-w-md mb-8">
|
|
612
|
+
Your AI-powered development environment for Model Context Protocol (MCP) servers.
|
|
613
|
+
Start a conversation or try a prompt below.
|
|
614
|
+
</p>
|
|
615
|
+
|
|
616
|
+
{/* Prompts Overview */}
|
|
617
|
+
{prompts.length > 0 && (
|
|
618
|
+
<div className="w-full max-w-2xl">
|
|
619
|
+
<div className="flex items-center gap-2 mb-4">
|
|
620
|
+
<Sparkles className="w-5 h-5 text-primary" />
|
|
621
|
+
<h3 className="text-lg font-semibold text-foreground">Available Prompts</h3>
|
|
622
|
+
<span className="text-sm text-muted-foreground">({prompts.length})</span>
|
|
623
|
+
</div>
|
|
624
|
+
|
|
625
|
+
<div className="grid grid-cols-1 md:grid-cols-2 gap-3">
|
|
626
|
+
{prompts.slice(0, 6).map((prompt) => (
|
|
627
|
+
<button
|
|
628
|
+
key={prompt.name}
|
|
629
|
+
onClick={() => {
|
|
630
|
+
setSelectedPrompt(prompt);
|
|
631
|
+
setPromptArgs({});
|
|
632
|
+
}}
|
|
633
|
+
className="card card-hover p-4 text-left group transition-all hover:scale-[1.02]"
|
|
634
|
+
>
|
|
635
|
+
<div className="flex items-start gap-3">
|
|
636
|
+
<div className="w-8 h-8 rounded-lg bg-primary/10 flex items-center justify-center group-hover:bg-primary/20 transition-colors flex-shrink-0">
|
|
637
|
+
<FileText className="w-4 h-4 text-primary" />
|
|
638
|
+
</div>
|
|
639
|
+
<div className="flex-1 min-w-0">
|
|
640
|
+
<h4 className="font-semibold text-foreground text-sm mb-1 truncate">
|
|
641
|
+
{prompt.name}
|
|
642
|
+
</h4>
|
|
643
|
+
<p className="text-xs text-muted-foreground line-clamp-2">
|
|
644
|
+
{prompt.description || 'No description'}
|
|
645
|
+
</p>
|
|
646
|
+
{prompt.arguments && prompt.arguments.length > 0 && (
|
|
647
|
+
<span className="badge badge-secondary text-xs mt-2 inline-block">
|
|
648
|
+
{prompt.arguments.length} arg{prompt.arguments.length !== 1 ? 's' : ''}
|
|
649
|
+
</span>
|
|
650
|
+
)}
|
|
651
|
+
</div>
|
|
652
|
+
</div>
|
|
653
|
+
</button>
|
|
654
|
+
))}
|
|
655
|
+
</div>
|
|
656
|
+
|
|
657
|
+
{prompts.length > 6 && (
|
|
658
|
+
<p className="text-xs text-muted-foreground text-center mt-4">
|
|
659
|
+
...and {prompts.length - 6} more. Visit the Prompts tab to see all.
|
|
660
|
+
</p>
|
|
661
|
+
)}
|
|
662
|
+
</div>
|
|
663
|
+
)}
|
|
664
|
+
|
|
665
|
+
{/* Suggestion Cards */}
|
|
666
|
+
<div className="w-full max-w-2xl mt-8">
|
|
667
|
+
<p className="text-sm text-muted-foreground mb-3">Or try asking:</p>
|
|
668
|
+
<div className="grid grid-cols-1 md:grid-cols-2 gap-3">
|
|
669
|
+
{[
|
|
670
|
+
'What tools are available?',
|
|
671
|
+
'Show me the health status',
|
|
672
|
+
'List all resources',
|
|
673
|
+
'Help me get started'
|
|
674
|
+
].map((suggestion) => (
|
|
675
|
+
<button
|
|
676
|
+
key={suggestion}
|
|
677
|
+
onClick={() => {
|
|
678
|
+
setInputValue(suggestion);
|
|
679
|
+
setTimeout(() => textareaRef.current?.focus(), 100);
|
|
680
|
+
}}
|
|
681
|
+
className="card card-hover p-3 text-left text-sm text-muted-foreground hover:text-foreground transition-colors"
|
|
682
|
+
>
|
|
683
|
+
"{suggestion}"
|
|
684
|
+
</button>
|
|
685
|
+
))}
|
|
686
|
+
</div>
|
|
687
|
+
</div>
|
|
688
|
+
</div>
|
|
689
|
+
) : (
|
|
690
|
+
<>
|
|
691
|
+
{chatMessages.map((msg, idx) => (
|
|
692
|
+
<ChatMessageComponent key={idx} message={msg} tools={tools} />
|
|
693
|
+
))}
|
|
694
|
+
{loading && (
|
|
695
|
+
<div className="flex gap-4 items-start animate-fade-in">
|
|
696
|
+
<div className="w-8 h-8 rounded-full bg-gradient-to-br from-primary to-amber-500 flex items-center justify-center flex-shrink-0 shadow-md">
|
|
697
|
+
<Bot className="w-5 h-5 text-white" strokeWidth={2.5} />
|
|
698
|
+
</div>
|
|
699
|
+
<div className="flex-1 bg-card/50 backdrop-blur-sm rounded-2xl px-5 py-4 border border-border/50">
|
|
700
|
+
<div className="flex items-center gap-2">
|
|
701
|
+
<div className="flex gap-1">
|
|
702
|
+
<span className="w-2 h-2 bg-primary rounded-full animate-bounce" style={{ animationDelay: '0s' }}></span>
|
|
703
|
+
<span className="w-2 h-2 bg-primary rounded-full animate-bounce" style={{ animationDelay: '0.15s' }}></span>
|
|
704
|
+
<span className="w-2 h-2 bg-primary rounded-full animate-bounce" style={{ animationDelay: '0.3s' }}></span>
|
|
705
|
+
</div>
|
|
706
|
+
<span className="text-sm text-muted-foreground font-medium">Thinking...</span>
|
|
707
|
+
</div>
|
|
708
|
+
</div>
|
|
709
|
+
</div>
|
|
710
|
+
)}
|
|
711
|
+
</>
|
|
712
|
+
)}
|
|
713
|
+
<div ref={messagesEndRef} />
|
|
714
|
+
</div>
|
|
387
715
|
</div>
|
|
388
716
|
|
|
389
|
-
{/* Input */}
|
|
390
|
-
<div className="border-t border-border
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
<
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
717
|
+
{/* ChatGPT-style Input Area - Fixed at bottom */}
|
|
718
|
+
<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)]">
|
|
719
|
+
<div className="max-w-3xl mx-auto px-3 sm:px-4 py-3 sm:py-4">
|
|
720
|
+
{currentImage && (
|
|
721
|
+
<div className="mb-3 p-3 bg-card rounded-xl flex items-start gap-3 border border-border/50 animate-fade-in">
|
|
722
|
+
<img
|
|
723
|
+
src={currentImage.data}
|
|
724
|
+
alt={currentImage.name}
|
|
725
|
+
className="w-20 h-20 object-cover rounded-lg border border-border"
|
|
726
|
+
/>
|
|
727
|
+
<div className="flex-1 min-w-0">
|
|
728
|
+
<p className="text-sm font-medium text-foreground truncate">{currentImage.name}</p>
|
|
729
|
+
<p className="text-xs text-muted-foreground">{currentImage.type}</p>
|
|
730
|
+
</div>
|
|
731
|
+
<button
|
|
732
|
+
onClick={() => setCurrentImage(null)}
|
|
733
|
+
className="w-7 h-7 rounded-lg flex items-center justify-center bg-muted/50 hover:bg-muted text-muted-foreground hover:text-foreground transition-all flex-shrink-0"
|
|
734
|
+
>
|
|
735
|
+
<X className="w-4 h-4" />
|
|
736
|
+
</button>
|
|
737
|
+
</div>
|
|
738
|
+
)}
|
|
739
|
+
<div className="flex items-center gap-2">
|
|
740
|
+
<input
|
|
741
|
+
type="file"
|
|
742
|
+
ref={fileInputRef}
|
|
743
|
+
onChange={handleImageUpload}
|
|
744
|
+
accept="image/*"
|
|
745
|
+
className="hidden"
|
|
397
746
|
/>
|
|
398
747
|
<button
|
|
399
|
-
onClick={() =>
|
|
400
|
-
className="
|
|
748
|
+
onClick={() => fileInputRef.current?.click()}
|
|
749
|
+
className="h-11 w-11 rounded-xl flex items-center justify-center bg-muted/50 hover:bg-muted text-muted-foreground hover:text-foreground transition-all flex-shrink-0"
|
|
750
|
+
title="Upload image"
|
|
751
|
+
>
|
|
752
|
+
<ImageIcon className="w-5 h-5" />
|
|
753
|
+
</button>
|
|
754
|
+
<div className="flex-1 relative flex items-center">
|
|
755
|
+
<textarea
|
|
756
|
+
ref={textareaRef}
|
|
757
|
+
value={inputValue}
|
|
758
|
+
onChange={(e) => setInputValue(e.target.value)}
|
|
759
|
+
onKeyDown={(e) => {
|
|
760
|
+
// Send on Enter, new line on Shift+Enter
|
|
761
|
+
if (e.key === 'Enter' && !e.shiftKey) {
|
|
762
|
+
e.preventDefault();
|
|
763
|
+
handleSend();
|
|
764
|
+
}
|
|
765
|
+
}}
|
|
766
|
+
placeholder="Message NitroStudio... (Shift + Enter for new line)"
|
|
767
|
+
className="w-full px-4 py-3 rounded-xl bg-card border border-border/50 focus:border-primary/50 focus:ring-2 focus:ring-primary/20 resize-none text-sm text-foreground placeholder:text-muted-foreground transition-all outline-none"
|
|
768
|
+
rows={1}
|
|
769
|
+
style={{
|
|
770
|
+
minHeight: '44px',
|
|
771
|
+
maxHeight: '200px',
|
|
772
|
+
overflow: 'hidden',
|
|
773
|
+
}}
|
|
774
|
+
/>
|
|
775
|
+
</div>
|
|
776
|
+
<button
|
|
777
|
+
onClick={handleSend}
|
|
778
|
+
disabled={loading || (!inputValue.trim() && !currentImage)}
|
|
779
|
+
className="h-11 w-11 rounded-xl flex items-center justify-center bg-gradient-to-br from-primary to-amber-500 text-white shadow-lg hover:shadow-xl disabled:opacity-50 disabled:cursor-not-allowed transition-all flex-shrink-0 hover:scale-105 active:scale-95"
|
|
780
|
+
title="Send message (Enter)"
|
|
401
781
|
>
|
|
402
|
-
<
|
|
782
|
+
<Send className="w-5 h-5" strokeWidth={2.5} />
|
|
403
783
|
</button>
|
|
404
784
|
</div>
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
785
|
+
|
|
786
|
+
</div>
|
|
787
|
+
</div>
|
|
788
|
+
|
|
789
|
+
{/* Prompt Executor Modal */}
|
|
790
|
+
{selectedPrompt && (
|
|
791
|
+
<div
|
|
792
|
+
className="fixed inset-0 z-50 flex items-center justify-center animate-fade-in"
|
|
793
|
+
style={{ backgroundColor: 'rgba(0, 0, 0, 0.85)' }}
|
|
794
|
+
onClick={() => setSelectedPrompt(null)}
|
|
795
|
+
>
|
|
796
|
+
<div
|
|
797
|
+
className="bg-card rounded-2xl p-6 w-[600px] max-h-[80vh] overflow-auto border border-border shadow-2xl animate-scale-in"
|
|
798
|
+
onClick={(e) => e.stopPropagation()}
|
|
418
799
|
>
|
|
419
|
-
<
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
<
|
|
435
|
-
|
|
436
|
-
|
|
800
|
+
<div className="flex items-center justify-between mb-4">
|
|
801
|
+
<div className="flex items-center gap-3">
|
|
802
|
+
<div className="w-10 h-10 rounded-lg bg-primary/10 flex items-center justify-center">
|
|
803
|
+
<FileText className="w-5 h-5 text-primary" />
|
|
804
|
+
</div>
|
|
805
|
+
<h2 className="text-xl font-bold text-foreground">{selectedPrompt.name}</h2>
|
|
806
|
+
</div>
|
|
807
|
+
<button
|
|
808
|
+
onClick={() => setSelectedPrompt(null)}
|
|
809
|
+
className="btn btn-ghost w-10 h-10 p-0"
|
|
810
|
+
>
|
|
811
|
+
<X className="w-5 h-5" />
|
|
812
|
+
</button>
|
|
813
|
+
</div>
|
|
814
|
+
|
|
815
|
+
<p className="text-sm text-muted-foreground mb-6">
|
|
816
|
+
{selectedPrompt.description || 'No description'}
|
|
817
|
+
</p>
|
|
818
|
+
|
|
819
|
+
<div>
|
|
820
|
+
{selectedPrompt.arguments && selectedPrompt.arguments.length > 0 ? (
|
|
821
|
+
selectedPrompt.arguments.map((arg) => (
|
|
822
|
+
<div key={arg.name} className="mb-4">
|
|
823
|
+
<label className="block text-sm font-medium text-foreground mb-2">
|
|
824
|
+
{arg.name}
|
|
825
|
+
{arg.required && <span className="text-destructive ml-1">*</span>}
|
|
826
|
+
</label>
|
|
827
|
+
<input
|
|
828
|
+
type="text"
|
|
829
|
+
className="input"
|
|
830
|
+
value={promptArgs[arg.name] || ''}
|
|
831
|
+
onChange={(e) =>
|
|
832
|
+
setPromptArgs({ ...promptArgs, [arg.name]: e.target.value })
|
|
833
|
+
}
|
|
834
|
+
required={arg.required}
|
|
835
|
+
placeholder={arg.description || `Enter ${arg.name}`}
|
|
836
|
+
/>
|
|
837
|
+
{arg.description && (
|
|
838
|
+
<p className="text-xs text-muted-foreground mt-1">{arg.description}</p>
|
|
839
|
+
)}
|
|
840
|
+
</div>
|
|
841
|
+
))
|
|
842
|
+
) : (
|
|
843
|
+
<div className="bg-muted/30 rounded-lg p-4 mb-4">
|
|
844
|
+
<p className="text-sm text-muted-foreground">No arguments required</p>
|
|
845
|
+
</div>
|
|
846
|
+
)}
|
|
847
|
+
|
|
848
|
+
<button
|
|
849
|
+
onClick={handleExecutePrompt}
|
|
850
|
+
className="btn btn-primary w-full gap-2"
|
|
851
|
+
>
|
|
852
|
+
<Play className="w-4 h-4" />
|
|
853
|
+
Execute Prompt
|
|
854
|
+
</button>
|
|
855
|
+
</div>
|
|
856
|
+
|
|
437
857
|
</div>
|
|
438
858
|
</div>
|
|
859
|
+
)}
|
|
439
860
|
</div>
|
|
440
861
|
);
|
|
441
862
|
}
|
|
@@ -446,22 +867,46 @@ function ChatMessageComponent({ message, tools }: { message: ChatMessage; tools:
|
|
|
446
867
|
const isUser = message.role === 'user';
|
|
447
868
|
|
|
448
869
|
return (
|
|
449
|
-
<div
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
870
|
+
<div className="flex gap-4 items-start animate-fade-in group">
|
|
871
|
+
{/* Avatar */}
|
|
872
|
+
{!isUser && (
|
|
873
|
+
<div className="w-8 h-8 rounded-full bg-gradient-to-br from-primary to-amber-500 flex items-center justify-center flex-shrink-0 shadow-md group-hover:shadow-lg transition-shadow">
|
|
874
|
+
<Bot className="w-5 h-5 text-white" strokeWidth={2.5} />
|
|
875
|
+
</div>
|
|
876
|
+
)}
|
|
877
|
+
{isUser && (
|
|
878
|
+
<div className="w-8 h-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">
|
|
879
|
+
<span className="text-white text-sm font-bold">You</span>
|
|
880
|
+
</div>
|
|
881
|
+
)}
|
|
882
|
+
|
|
883
|
+
{/* Message Content */}
|
|
884
|
+
<div className="flex-1 min-w-0">
|
|
885
|
+
{/* Image if present */}
|
|
453
886
|
{message.image && (
|
|
454
|
-
<
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
887
|
+
<div className="mb-3 rounded-xl overflow-hidden border border-border/50 shadow-sm">
|
|
888
|
+
<img
|
|
889
|
+
src={message.image.data}
|
|
890
|
+
alt={message.image.name}
|
|
891
|
+
className="max-w-full"
|
|
892
|
+
/>
|
|
893
|
+
</div>
|
|
894
|
+
)}
|
|
895
|
+
|
|
896
|
+
{/* Text content with markdown rendering */}
|
|
897
|
+
{message.content && (
|
|
898
|
+
<div className="text-sm leading-relaxed mb-4">
|
|
899
|
+
{isUser ? (
|
|
900
|
+
<div className="whitespace-pre-wrap text-foreground/90">{message.content}</div>
|
|
901
|
+
) : (
|
|
902
|
+
<MarkdownRenderer content={message.content} />
|
|
903
|
+
)}
|
|
904
|
+
</div>
|
|
459
905
|
)}
|
|
460
|
-
<p className="text-sm whitespace-pre-wrap text-foreground">{message.content}</p>
|
|
461
906
|
|
|
462
|
-
{/* Tool Calls */}
|
|
907
|
+
{/* Tool Calls - ChatGPT-style cards */}
|
|
463
908
|
{message.toolCalls && message.toolCalls.length > 0 && (
|
|
464
|
-
<div className="
|
|
909
|
+
<div className="space-y-3">
|
|
465
910
|
{message.toolCalls.map((toolCall) => (
|
|
466
911
|
<ToolCallComponent key={toolCall.id} toolCall={toolCall} tools={tools} />
|
|
467
912
|
))}
|
|
@@ -473,6 +918,7 @@ function ChatMessageComponent({ message, tools }: { message: ChatMessage; tools:
|
|
|
473
918
|
}
|
|
474
919
|
|
|
475
920
|
function ToolCallComponent({ toolCall, tools }: { toolCall: ToolCall; tools: Tool[] }) {
|
|
921
|
+
const [showArgs, setShowArgs] = useState(false);
|
|
476
922
|
const tool = tools.find((t) => t.name === toolCall.name);
|
|
477
923
|
|
|
478
924
|
// Get widget URI from multiple possible sources
|
|
@@ -500,28 +946,48 @@ function ToolCallComponent({ toolCall, tools }: { toolCall: ToolCall; tools: Too
|
|
|
500
946
|
});
|
|
501
947
|
|
|
502
948
|
return (
|
|
503
|
-
<div className="rounded-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
949
|
+
<div className="rounded-2xl overflow-hidden border border-border/50 bg-card/50 backdrop-blur-sm shadow-sm hover:shadow-md transition-shadow">
|
|
950
|
+
{/* Tool Header - Collapsible */}
|
|
951
|
+
<button
|
|
952
|
+
onClick={() => setShowArgs(!showArgs)}
|
|
953
|
+
className="w-full flex items-center justify-between px-4 py-3 hover:bg-muted/30 transition-colors group"
|
|
954
|
+
>
|
|
955
|
+
<div className="flex items-center gap-2">
|
|
956
|
+
<div className="w-6 h-6 rounded-md bg-primary/10 flex items-center justify-center group-hover:bg-primary/20 transition-colors">
|
|
957
|
+
<Wrench className="w-3.5 h-3.5 text-primary" />
|
|
958
|
+
</div>
|
|
959
|
+
<span className="font-semibold text-sm text-foreground">{toolCall.name}</span>
|
|
960
|
+
</div>
|
|
961
|
+
<svg
|
|
962
|
+
className={`w-4 h-4 text-muted-foreground transition-transform ${showArgs ? 'rotate-180' : ''}`}
|
|
963
|
+
fill="none"
|
|
964
|
+
viewBox="0 0 24 24"
|
|
965
|
+
stroke="currentColor"
|
|
966
|
+
>
|
|
967
|
+
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 9l-7 7-7-7" />
|
|
968
|
+
</svg>
|
|
969
|
+
</button>
|
|
970
|
+
|
|
971
|
+
{/* Arguments - Expandable */}
|
|
972
|
+
{showArgs && (
|
|
973
|
+
<div className="px-4 pb-3 border-t border-border/30 pt-3 animate-fade-in">
|
|
974
|
+
<p className="text-xs font-medium text-muted-foreground mb-2">Arguments:</p>
|
|
975
|
+
<pre className="p-3 rounded-lg overflow-auto bg-background/50 border border-border/30 font-mono text-xs text-foreground max-h-40">
|
|
976
|
+
{JSON.stringify(toolCall.arguments, null, 2)}
|
|
977
|
+
</pre>
|
|
978
|
+
</div>
|
|
979
|
+
)}
|
|
508
980
|
|
|
509
|
-
{/*
|
|
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 */}
|
|
981
|
+
{/* Widget - ChatGPT-style embedded card */}
|
|
520
982
|
{componentUri && widgetData && (
|
|
521
|
-
<div className="
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
983
|
+
<div className="border-t border-border/30 pt-3 flex justify-center">
|
|
984
|
+
<div className="rounded-lg overflow-hidden bg-background" style={{
|
|
985
|
+
width: 'fit-content',
|
|
986
|
+
maxWidth: '100%',
|
|
987
|
+
height: '450px',
|
|
988
|
+
}}>
|
|
989
|
+
<WidgetRenderer uri={componentUri} data={widgetData} className="widget-in-chat" />
|
|
990
|
+
</div>
|
|
525
991
|
</div>
|
|
526
992
|
)}
|
|
527
993
|
</div>
|