nitrostack 1.0.15 → 1.0.16

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