nitrostack 1.0.16 → 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.
@@ -4,6 +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 { MarkdownRenderer } from '@/components/MarkdownRenderer';
7
8
  import type { ChatMessage, Tool, ToolCall, Prompt } from '@/lib/types';
8
9
  import {
9
10
  Bot,
@@ -58,6 +59,17 @@ export default function ChatPage() {
58
59
  useEffect(() => {
59
60
  loadTools();
60
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
+ }
61
73
  }, []);
62
74
 
63
75
  useEffect(() => {
@@ -101,23 +113,61 @@ export default function ChatPage() {
101
113
  if (!selectedPrompt) return;
102
114
 
103
115
  // Close modal
116
+ const prompt = selectedPrompt;
117
+ const args = { ...promptArgs };
104
118
  setSelectedPrompt(null);
119
+ setPromptArgs({});
105
120
 
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)}`;
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)}`;
110
125
  }
111
126
 
112
- // Set input value so user can see what's being sent
113
- setInputValue(messageContent);
114
- setPromptArgs({});
127
+ // Add user message to chat
128
+ addChatMessage({
129
+ role: 'user',
130
+ content: userMessageContent,
131
+ });
132
+
133
+ setLoading(true);
115
134
 
116
- // Focus textarea and trigger send
117
- setTimeout(() => {
118
- textareaRef.current?.focus();
119
- handleSend();
120
- }, 100);
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
+ }
121
171
  };
122
172
 
123
173
  const handleImageUpload = (e: React.ChangeEvent<HTMLInputElement>) => {
@@ -373,7 +423,7 @@ export default function ChatPage() {
373
423
  return (
374
424
  <div className="fixed inset-0 flex flex-col bg-background" style={{ left: 'var(--sidebar-width, 15rem)' }}>
375
425
  {/* 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">
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">
377
427
  <div className="flex items-center gap-3">
378
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">
379
429
  <Bot className="w-5 h-5 text-white" strokeWidth={2.5} />
@@ -383,18 +433,18 @@ export default function ChatPage() {
383
433
  </div>
384
434
  </div>
385
435
 
386
- <div className="flex items-center gap-2">
436
+ <div className="flex items-center gap-2 w-full sm:w-auto">
387
437
  <select
388
438
  value={currentProvider}
389
439
  onChange={(e) => setCurrentProvider(e.target.value as 'openai' | 'gemini')}
390
- className="input text-sm px-3 py-1.5 w-28"
440
+ className="input text-sm px-3 py-1.5 w-full sm:w-28 flex-1 sm:flex-none"
391
441
  >
392
442
  <option value="gemini">Gemini</option>
393
443
  <option value="openai">OpenAI</option>
394
444
  </select>
395
445
  <button
396
446
  onClick={() => setShowSettings(!showSettings)}
397
- className={`w-8 h-8 rounded-lg flex items-center justify-center transition-all ${
447
+ className={`w-8 h-8 rounded-lg flex items-center justify-center transition-all flex-shrink-0 ${
398
448
  showSettings
399
449
  ? 'bg-primary/10 text-primary ring-1 ring-primary/30'
400
450
  : 'bg-muted/50 text-muted-foreground hover:bg-muted hover:text-foreground'
@@ -405,7 +455,7 @@ export default function ChatPage() {
405
455
  </button>
406
456
  <button
407
457
  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"
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"
409
459
  title="Clear chat"
410
460
  >
411
461
  <Trash2 className="w-4 h-4" />
@@ -415,7 +465,7 @@ export default function ChatPage() {
415
465
 
416
466
  {/* Enhanced Settings Panel */}
417
467
  {showSettings && (
418
- <div className="border-b border-border/50 px-6 py-5 bg-muted/20 backdrop-blur-md shadow-sm">
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">
419
469
  <div className="max-w-4xl mx-auto">
420
470
  <div className="flex items-start justify-between mb-4">
421
471
  <div>
@@ -427,7 +477,7 @@ export default function ChatPage() {
427
477
  </div>
428
478
  </div>
429
479
 
430
- <div className="grid grid-cols-1 lg:grid-cols-2 gap-4">
480
+ <div className="grid grid-cols-1 md:grid-cols-2 gap-4">
431
481
  {/* OpenAI Section */}
432
482
  <div className="card p-4">
433
483
  <div className="flex items-center justify-between mb-3">
@@ -666,7 +716,7 @@ export default function ChatPage() {
666
716
 
667
717
  {/* ChatGPT-style Input Area - Fixed at bottom */}
668
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)]">
669
- <div className="max-w-3xl mx-auto px-4 py-4">
719
+ <div className="max-w-3xl mx-auto px-3 sm:px-4 py-3 sm:py-4">
670
720
  {currentImage && (
671
721
  <div className="mb-3 p-3 bg-card rounded-xl flex items-start gap-3 border border-border/50 animate-fade-in">
672
722
  <img
@@ -732,9 +782,7 @@ export default function ChatPage() {
732
782
  <Send className="w-5 h-5" strokeWidth={2.5} />
733
783
  </button>
734
784
  </div>
735
- <p className="text-xs text-muted-foreground/60 text-center mt-2">
736
- NitroStudio can make mistakes. Check important info.
737
- </p>
785
+
738
786
  </div>
739
787
  </div>
740
788
 
@@ -803,10 +851,11 @@ export default function ChatPage() {
803
851
  >
804
852
  <Play className="w-4 h-4" />
805
853
  Execute Prompt
806
- </button>
807
- </div>
854
+ </button>
808
855
  </div>
856
+
809
857
  </div>
858
+ </div>
810
859
  )}
811
860
  </div>
812
861
  );
@@ -844,10 +893,14 @@ function ChatMessageComponent({ message, tools }: { message: ChatMessage; tools:
844
893
  </div>
845
894
  )}
846
895
 
847
- {/* Text content */}
896
+ {/* Text content with markdown rendering */}
848
897
  {message.content && (
849
- <div className="text-sm text-foreground/90 leading-relaxed whitespace-pre-wrap mb-4">
850
- {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
+ )}
851
904
  </div>
852
905
  )}
853
906
 
@@ -927,11 +980,13 @@ function ToolCallComponent({ toolCall, tools }: { toolCall: ToolCall; tools: Too
927
980
 
928
981
  {/* Widget - ChatGPT-style embedded card */}
929
982
  {componentUri && widgetData && (
930
- <div className="border-t border-border/30">
983
+ <div className="border-t border-border/30 pt-3 flex justify-center">
931
984
  <div className="rounded-lg overflow-hidden bg-background" style={{
932
- height: '400px'
985
+ width: 'fit-content',
986
+ maxWidth: '100%',
987
+ height: '450px',
933
988
  }}>
934
- <WidgetRenderer uri={componentUri} data={widgetData} />
989
+ <WidgetRenderer uri={componentUri} data={widgetData} className="widget-in-chat" />
935
990
  </div>
936
991
  </div>
937
992
  )}
@@ -40,7 +40,7 @@ export default function RootLayout({
40
40
  <div className="flex min-h-screen bg-gradient-to-br from-background via-background to-muted/20">
41
41
  <Sidebar />
42
42
  <main className="flex-1 transition-all duration-300 ease-in-out" style={{ marginLeft: 'var(--sidebar-width, 15rem)' }}>
43
- <div className="min-h-screen p-8">
43
+ <div className="min-h-screen p-3 sm:p-6 md:p-8">
44
44
  {children}
45
45
  </div>
46
46
  </main>
@@ -26,13 +26,18 @@ export function EnlargeModal() {
26
26
  if (type !== 'tool') return;
27
27
 
28
28
  closeEnlargeModal();
29
- setCurrentTab('chat');
30
- router.push('/chat');
31
29
 
32
- // Store the tool name so chat can suggest using it
30
+ // Build the tool execution message
31
+ const toolMessage = `Use the ${item.name} tool`;
32
+
33
+ // Store both the tool name and the message
33
34
  if (typeof window !== 'undefined') {
34
35
  window.localStorage.setItem('suggestedTool', item.name);
36
+ window.localStorage.setItem('chatInput', toolMessage);
35
37
  }
38
+
39
+ setCurrentTab('chat');
40
+ router.push('/chat');
36
41
  };
37
42
 
38
43
  return (
@@ -42,34 +47,35 @@ export function EnlargeModal() {
42
47
  onClick={closeEnlargeModal}
43
48
  >
44
49
  <div
45
- className="relative w-[90vw] h-[90vh] bg-card border border-border rounded-2xl shadow-2xl overflow-hidden animate-scale-in"
50
+ className="relative w-[95vw] max-w-7xl h-[90vh] bg-card border border-border rounded-xl sm:rounded-2xl shadow-2xl overflow-hidden animate-scale-in"
46
51
  onClick={(e) => e.stopPropagation()}
47
52
  >
48
53
  {/* Header */}
49
- <div className="flex items-center justify-between p-6 border-b border-border bg-muted/30">
50
- <div className="flex items-center gap-4">
51
- <span className="text-3xl">{type === 'tool' ? '⚡' : '🎨'}</span>
52
- <div>
53
- <h2 className="text-xl font-bold text-foreground">{item.name}</h2>
54
- <p className="text-sm text-muted-foreground mt-1">
54
+ <div className="flex flex-col sm:flex-row items-start sm:items-center justify-between p-4 sm:p-6 border-b border-border bg-muted/30 gap-3 sm:gap-0">
55
+ <div className="flex items-start sm:items-center gap-3 sm:gap-4 flex-1 min-w-0">
56
+ <span className="text-2xl sm:text-3xl flex-shrink-0">{type === 'tool' ? '⚡' : '🎨'}</span>
57
+ <div className="min-w-0 flex-1">
58
+ <h2 className="text-lg sm:text-xl font-bold text-foreground truncate">{item.name}</h2>
59
+ <p className="text-xs sm:text-sm text-muted-foreground mt-1 line-clamp-2">
55
60
  {item.description || 'No description available'}
56
61
  </p>
57
62
  </div>
58
63
  </div>
59
64
 
60
- <div className="flex items-center gap-2">
65
+ <div className="flex items-center gap-2 w-full sm:w-auto">
61
66
  {type === 'tool' && (
62
67
  <button
63
68
  onClick={handleUseInChat}
64
- className="btn btn-primary flex items-center gap-2"
69
+ className="btn btn-primary flex items-center gap-2 flex-1 sm:flex-none text-sm"
65
70
  >
66
71
  <MessageSquare className="w-4 h-4" />
67
- Use in Chat
72
+ <span className="hidden sm:inline">Use in Chat</span>
73
+ <span className="sm:hidden">Chat</span>
68
74
  </button>
69
75
  )}
70
76
  <button
71
77
  onClick={closeEnlargeModal}
72
- className="btn btn-ghost w-10 h-10 p-0 flex items-center justify-center"
78
+ className="btn btn-ghost w-10 h-10 p-0 flex items-center justify-center flex-shrink-0"
73
79
  aria-label="Close"
74
80
  >
75
81
  <X className="w-5 h-5" />
@@ -78,7 +84,7 @@ export function EnlargeModal() {
78
84
  </div>
79
85
 
80
86
  {/* Widget Content */}
81
- <div className="h-[calc(100%-88px)] p-6 overflow-auto bg-background">
87
+ <div className="h-[calc(100%-120px)] sm:h-[calc(100%-100px)] p-3 sm:p-6 overflow-auto bg-background">
82
88
  {componentUri && widgetData ? (
83
89
  <div className="w-full h-full rounded-xl overflow-hidden border border-border shadow-inner">
84
90
  <WidgetRenderer uri={componentUri} data={widgetData} className="w-full h-full" />
@@ -0,0 +1,153 @@
1
+ 'use client';
2
+
3
+ import { useState } from 'react';
4
+ import { Copy, Check, ChevronDown, ChevronRight } from 'lucide-react';
5
+
6
+ interface LogMessageProps {
7
+ message: string;
8
+ }
9
+
10
+ export function LogMessage({ message }: LogMessageProps) {
11
+ const [copied, setCopied] = useState(false);
12
+ const [isExpanded, setIsExpanded] = useState(true);
13
+
14
+ const handleCopy = async (text: string) => {
15
+ try {
16
+ await navigator.clipboard.writeText(text);
17
+ setCopied(true);
18
+ setTimeout(() => setCopied(false), 2000);
19
+ } catch (error) {
20
+ console.error('Failed to copy:', error);
21
+ }
22
+ };
23
+
24
+ // Try to parse and format JSON
25
+ const tryParseJSON = (text: string): { isJSON: boolean; formatted?: string; parsed?: any } => {
26
+ try {
27
+ const trimmed = text.trim();
28
+ if (!trimmed.startsWith('{') && !trimmed.startsWith('[')) {
29
+ return { isJSON: false };
30
+ }
31
+
32
+ const parsed = JSON.parse(trimmed);
33
+
34
+ // Pretty print with proper indentation
35
+ const formatted = JSON.stringify(parsed, (key, value) => {
36
+ // If value is a string that looks like JSON, try to parse it
37
+ if (typeof value === 'string' && (value.trim().startsWith('{') || value.trim().startsWith('['))) {
38
+ try {
39
+ return JSON.parse(value);
40
+ } catch {
41
+ return value;
42
+ }
43
+ }
44
+ return value;
45
+ }, 2);
46
+
47
+ return { isJSON: true, formatted, parsed };
48
+ } catch {
49
+ return { isJSON: false };
50
+ }
51
+ };
52
+
53
+ // Syntax highlight JSON
54
+ const highlightJSON = (jsonString: string) => {
55
+ return jsonString
56
+ .replace(/("(\\u[a-zA-Z0-9]{4}|\\[^u]|[^\\"])*"(\s*:)?)/g, (match) => {
57
+ let cls = 'json-string';
58
+ if (/:$/.test(match)) {
59
+ cls = 'json-key';
60
+ }
61
+ return `<span class="${cls}">${match}</span>`;
62
+ })
63
+ .replace(/\b(true|false|null)\b/g, '<span class="json-boolean">$1</span>')
64
+ .replace(/\b(-?\d+\.?\d*)\b/g, '<span class="json-number">$1</span>')
65
+ .replace(/([{}[\],])/g, '<span class="json-punctuation">$1</span>');
66
+ };
67
+
68
+ const result = tryParseJSON(message);
69
+
70
+ if (!result.isJSON) {
71
+ // Plain text message
72
+ return <span className="text-slate-300 break-all">{message}</span>;
73
+ }
74
+
75
+ // JSON message - collapsible with syntax highlighting
76
+ return (
77
+ <div className="flex-1 min-w-0">
78
+ <div className="flex items-center gap-2 mb-1">
79
+ <button
80
+ onClick={() => setIsExpanded(!isExpanded)}
81
+ className="text-slate-400 hover:text-slate-200 transition-colors"
82
+ >
83
+ {isExpanded ? (
84
+ <ChevronDown className="w-4 h-4" />
85
+ ) : (
86
+ <ChevronRight className="w-4 h-4" />
87
+ )}
88
+ </button>
89
+ <span className="text-slate-500 text-xs font-semibold">JSON Response</span>
90
+ <button
91
+ onClick={() => handleCopy(result.formatted || message)}
92
+ className="ml-auto text-slate-500 hover:text-slate-300 transition-colors p-1 rounded hover:bg-slate-800"
93
+ title={copied ? 'Copied!' : 'Copy JSON'}
94
+ >
95
+ {copied ? (
96
+ <Check className="w-3.5 h-3.5 text-green-400" />
97
+ ) : (
98
+ <Copy className="w-3.5 h-3.5" />
99
+ )}
100
+ </button>
101
+ </div>
102
+
103
+ {isExpanded && (
104
+ <div className="json-container">
105
+ <pre
106
+ className="json-content"
107
+ dangerouslySetInnerHTML={{ __html: highlightJSON(result.formatted || message) }}
108
+ />
109
+ </div>
110
+ )}
111
+
112
+ <style jsx>{`
113
+ .json-container {
114
+ background: rgb(15, 23, 42);
115
+ border: 1px solid rgb(51, 65, 85);
116
+ border-radius: 0.5rem;
117
+ padding: 0.75rem;
118
+ overflow-x: auto;
119
+ margin-top: 0.5rem;
120
+ }
121
+
122
+ .json-content {
123
+ margin: 0;
124
+ font-family: 'JetBrains Mono', 'Courier New', monospace;
125
+ font-size: 0.75rem;
126
+ line-height: 1.5;
127
+ color: rgb(226, 232, 240);
128
+ }
129
+
130
+ :global(.json-key) {
131
+ color: rgb(96, 165, 250); /* Blue */
132
+ }
133
+
134
+ :global(.json-string) {
135
+ color: rgb(134, 239, 172); /* Green */
136
+ }
137
+
138
+ :global(.json-number) {
139
+ color: rgb(251, 146, 60); /* Orange */
140
+ }
141
+
142
+ :global(.json-boolean) {
143
+ color: rgb(167, 139, 250); /* Purple */
144
+ }
145
+
146
+ :global(.json-punctuation) {
147
+ color: rgb(148, 163, 184); /* Gray */
148
+ }
149
+ `}</style>
150
+ </div>
151
+ );
152
+ }
153
+