nitrostack 1.0.71 → 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.
@@ -3,53 +3,68 @@
3
3
  import { useEffect, useState } from 'react';
4
4
  import { useStudioStore } from '@/lib/store';
5
5
  import { api } from '@/lib/api';
6
- import { ToolCard } from '@/components/ToolCard';
7
- import { WidgetRenderer } from '@/components/WidgetRenderer';
8
6
  import type { Tool } from '@/lib/types';
9
- import { Wrench, RefreshCw, X, Play, AlertCircle } from 'lucide-react';
7
+ import dynamic from 'next/dynamic';
8
+ import { ArrowPathIcon, WrenchScrewdriverIcon, XMarkIcon, PlayIcon, ChevronDownIcon } from '@heroicons/react/24/outline';
9
+ import { WidgetRenderer } from '@/components/WidgetRenderer';
10
+
11
+
12
+ // Dynamic import for ToolsCanvas
13
+ const ToolsCanvas = dynamic(() => import('@/components/tools/ToolsCanvas').then(mod => mod.default), {
14
+ ssr: false,
15
+ loading: () => <div className="w-full h-[600px] bg-muted/10 animate-pulse rounded-xl" />
16
+ });
17
+
18
+ export default function AppPage() {
19
+ const {
20
+ tools, setTools,
21
+ resources, setResources,
22
+ prompts, setPrompts,
23
+ loading, setLoading,
24
+ connection, setConnection,
25
+ jwtToken, apiKey, oauthState
26
+ } = useStudioStore();
10
27
 
11
- export default function ToolsPage() {
12
- const { tools, setTools, loading, setLoading, connection, setConnection, jwtToken, apiKey, oauthState } = useStudioStore();
13
- const [searchQuery, setSearchQuery] = useState('');
14
28
  const [selectedTool, setSelectedTool] = useState<Tool | null>(null);
15
29
  const [toolArgs, setToolArgs] = useState<Record<string, any>>({});
16
30
  const [toolResult, setToolResult] = useState<any>(null);
17
31
  const [executingTool, setExecutingTool] = useState(false);
18
-
19
- // Get effective token - check both jwtToken and OAuth token
32
+
33
+
34
+
35
+ // Get effective token
20
36
  const effectiveToken = jwtToken || oauthState?.currentToken;
21
37
 
22
- // Initialize MCP, load tools and check connection on mount
23
38
  useEffect(() => {
24
39
  const init = async () => {
25
- await api.initialize();
26
- await loadTools();
27
- await checkConnection();
40
+ if (tools.length === 0 || resources.length === 0 || prompts.length === 0) {
41
+ await loadData();
42
+ }
28
43
  };
29
44
  init();
30
45
  }, []);
31
46
 
32
- const checkConnection = async () => {
33
- try {
34
- const health = await api.checkConnection();
35
- setConnection({
36
- connected: health.connected,
37
- status: health.connected ? 'connected' : 'disconnected',
38
- });
39
- } catch (error) {
40
- setConnection({ connected: false, status: 'disconnected' });
41
- }
42
- };
43
-
44
- const loadTools = async () => {
47
+ const loadData = async () => {
45
48
  setLoading('tools', true);
49
+ setLoading('resources', true);
50
+ setLoading('prompts', true);
51
+
46
52
  try {
47
- const data = await api.getTools();
48
- setTools(data.tools || []);
53
+ const [toolsData, resourcesData, promptsData] = await Promise.all([
54
+ api.getTools().catch(() => ({ tools: [] })),
55
+ api.getResources().catch(() => ({ resources: [] })),
56
+ api.getPrompts().catch(() => ({ prompts: [] }))
57
+ ]);
58
+
59
+ setTools(toolsData.tools || []);
60
+ setResources(resourcesData.resources || []);
61
+ setPrompts(promptsData.prompts || []);
49
62
  } catch (error) {
50
- console.error('Failed to load tools:', error);
63
+ console.error('Failed to load data:', error);
51
64
  } finally {
52
65
  setLoading('tools', false);
66
+ setLoading('resources', false);
67
+ setLoading('prompts', false);
53
68
  }
54
69
  };
55
70
 
@@ -59,6 +74,15 @@ export default function ToolsPage() {
59
74
  setToolResult(null);
60
75
  };
61
76
 
77
+ const handleResourceClick = (resource: any) => {
78
+ // For now just console log, or we could open a details modal
79
+ console.log('Resource clicked:', resource);
80
+ };
81
+
82
+ const handlePromptClick = (prompt: any) => {
83
+ console.log('Prompt clicked:', prompt);
84
+ };
85
+
62
86
  const handleSubmitTool = async (e: React.FormEvent) => {
63
87
  e.preventDefault();
64
88
  if (!selectedTool) return;
@@ -67,26 +91,21 @@ export default function ToolsPage() {
67
91
  setToolResult(null);
68
92
 
69
93
  try {
70
- // Pass JWT token and API key if available (check both jwtToken and OAuth token)
71
94
  const result = await api.callTool(selectedTool.name, toolArgs, effectiveToken || undefined, apiKey || undefined);
72
95
  setToolResult(result);
73
96
 
74
- // Extract JWT token from ANY tool response (not just 'login')
75
- // Check if result contains a token field at any level
76
97
  if (result.content) {
77
98
  try {
78
99
  const content = result.content[0]?.text;
79
100
  if (content) {
80
101
  const parsed = JSON.parse(content);
81
- // Check for token in multiple possible locations
82
102
  const token = parsed.token || parsed.access_token || parsed.jwt || parsed.data?.token;
83
103
  if (token) {
84
- console.log('🔐 Token received from tool, saving to global state');
85
104
  useStudioStore.getState().setJwtToken(token);
86
105
  }
87
106
  }
88
107
  } catch (e) {
89
- // Ignore parsing errors
108
+ // Ignore
90
109
  }
91
110
  }
92
111
  } catch (error) {
@@ -97,247 +116,131 @@ export default function ToolsPage() {
97
116
  }
98
117
  };
99
118
 
100
- const filteredTools = tools.filter((tool) =>
101
- tool.name.toLowerCase().includes(searchQuery.toLowerCase())
102
- );
119
+
103
120
 
104
121
  return (
105
122
  <>
106
123
  <div className="fixed inset-0 flex flex-col bg-background" style={{ left: 'var(--sidebar-width, 15rem)' }}>
107
- {/* Sticky Header */}
108
- <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">
124
+ {/* Minimal Professional Header */}
125
+ <div className="sticky top-0 z-10 border-b border-border/50 px-6 py-4 flex items-center justify-between bg-card/50 backdrop-blur-sm">
126
+ <div className="flex items-center gap-6">
127
+ <h1 className="text-lg font-semibold text-foreground">App Canvas</h1>
128
+ </div>
129
+
109
130
  <div className="flex items-center gap-3">
110
- <div className="w-8 h-8 rounded-lg bg-gradient-to-br from-primary to-amber-500 flex items-center justify-center shadow-md">
111
- <Wrench className="w-5 h-5 text-white" strokeWidth={2.5} />
112
- </div>
113
- <div>
114
- <h1 className="text-lg font-bold text-foreground">Tools</h1>
115
- </div>
131
+ {/* Run App Button Removed */}
132
+
133
+ <button onClick={loadData} className="btn btn-ghost text-muted-foreground hover:text-foreground text-sm px-3 py-2 gap-2">
134
+ <ArrowPathIcon className="h-4 w-4" />
135
+ </button>
116
136
  </div>
117
- <button onClick={loadTools} className="btn btn-primary text-sm px-4 py-2 gap-2">
118
- <RefreshCw className="w-4 h-4" />
119
- Refresh
120
- </button>
121
137
  </div>
122
138
 
123
- {/* Content - ONLY this scrolls */}
124
- <div className="flex-1 overflow-y-auto overflow-x-hidden">
125
- <div className="max-w-7xl mx-auto px-6 py-6">
126
- {/* Search */}
127
- <div className="mb-6">
128
- <input
129
- type="text"
130
- placeholder="Search tools..."
131
- value={searchQuery}
132
- onChange={(e) => setSearchQuery(e.target.value)}
133
- className="input w-full"
134
- />
139
+ {/* Content - Canvas View */}
140
+ <div className="flex-1 relative overflow-hidden bg-[#09090b]">
141
+ {loading.tools || loading.resources || loading.prompts ? (
142
+ <div className="w-full h-full flex items-center justify-center">
143
+ <div className="w-8 h-8 border-2 border-primary border-t-transparent rounded-full animate-spin"></div>
135
144
  </div>
136
-
137
- {/* Tools Grid */}
138
- {loading.tools ? (
139
- <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
140
- {[1, 2, 3].map((i) => (
141
- <div key={i} className="card skeleton h-64"></div>
142
- ))}
143
- </div>
144
- ) : filteredTools.length === 0 ? (
145
- <div className="empty-state">
146
- <AlertCircle className="empty-state-icon" />
147
- <p className="empty-state-title">
148
- {searchQuery ? 'No tools found matching your search' : 'No tools available'}
149
- </p>
150
- <p className="empty-state-description">
151
- {searchQuery ? 'Try a different search term' : 'No MCP tools have been registered'}
152
- </p>
153
- </div>
154
- ) : (
155
- <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
156
- {filteredTools.map((tool) => (
157
- <ToolCard key={tool.name} tool={tool} onExecute={handleExecuteTool} />
158
- ))}
159
- </div>
160
- )}
161
- </div>
145
+ ) : (
146
+ <ToolsCanvas
147
+ tools={tools}
148
+ resources={resources}
149
+ prompts={prompts}
150
+ onToolClick={handleExecuteTool}
151
+ onResourceClick={handleResourceClick}
152
+ onPromptClick={handlePromptClick}
153
+ />
154
+ )}
162
155
  </div>
163
156
  </div>
164
157
 
165
- {/* Tool Executor Modal */}
158
+ {/* Run App Modal - Removed */}
159
+
160
+ {/* Tool Executor Side Drawer - Reused for consistency */}
166
161
  {selectedTool && (
167
- <div
168
- className="fixed inset-0 z-50 flex items-center justify-center animate-fade-in"
169
- style={{ backgroundColor: 'rgba(0, 0, 0, 0.85)' }}
170
- onClick={() => setSelectedTool(null)}
171
- >
172
- <div
173
- className="w-[700px] max-h-[90vh] overflow-auto rounded-2xl border border-border p-6 bg-card shadow-2xl animate-scale-in"
174
- onClick={(e) => e.stopPropagation()}
175
- >
176
- <div className="flex items-center justify-between mb-4">
177
- <div className="flex items-center gap-3">
178
- <div className="w-10 h-10 rounded-lg bg-primary/10 flex items-center justify-center">
179
- <Wrench className="w-5 h-5 text-primary" />
180
- </div>
181
- <h2 className="text-xl font-bold text-foreground">{selectedTool.name}</h2>
162
+ <div className="fixed inset-0 z-50 flex justify-end bg-background/80 backdrop-blur-sm animate-fade-in" onClick={() => setSelectedTool(null)}>
163
+ <div className="relative w-full max-w-2xl h-full bg-card border-l border-border shadow-2xl overflow-hidden animate-slide-in-right flex flex-col" onClick={(e) => e.stopPropagation()}>
164
+ <div className="flex items-center justify-between px-6 py-4 border-b border-border bg-card">
165
+ <div className="flex items-center gap-3">
166
+ <div className="w-10 h-10 rounded-lg bg-primary/10 flex items-center justify-center">
167
+ <WrenchScrewdriverIcon className="w-5 h-5 text-primary" />
168
+ </div>
169
+ <div>
170
+ <h2 className="text-lg font-bold text-foreground">{selectedTool.name}</h2>
171
+ <p className="text-xs text-muted-foreground">Tool Executor</p>
182
172
  </div>
183
- <button
184
- onClick={() => setSelectedTool(null)}
185
- className="btn btn-ghost w-10 h-10 p-0"
186
- >
187
- <X className="w-5 h-5" />
188
- </button>
189
173
  </div>
174
+ <button onClick={() => setSelectedTool(null)} className="btn btn-ghost w-8 h-8 p-0 flex items-center justify-center rounded-full hover:bg-muted" aria-label="Close">
175
+ <XMarkIcon className="w-5 h-5" />
176
+ </button>
177
+ </div>
190
178
 
191
- <p className="text-sm text-muted-foreground mb-6">
192
- {selectedTool.description || 'No description'}
193
- </p>
179
+ <div className="flex-1 overflow-y-auto bg-muted/5 p-6">
180
+ <div className="bg-card p-4 rounded-xl border border-border mb-6">
181
+ <p className="text-sm text-foreground leading-relaxed">{selectedTool.description || 'No description available'}</p>
182
+ </div>
194
183
 
195
- <form onSubmit={handleSubmitTool}>
196
- {/* Generate form inputs from schema */}
184
+ <form onSubmit={handleSubmitTool} className="space-y-6">
197
185
  {selectedTool.inputSchema?.properties && typeof selectedTool.inputSchema.properties === 'object' && Object.keys(selectedTool.inputSchema.properties).length > 0 ? (
198
- <>
186
+ <div className="space-y-4">
199
187
  {Object.entries(selectedTool.inputSchema.properties).map(([key, prop]: [string, any]) => {
200
188
  const isRequired = selectedTool.inputSchema?.required?.includes(key);
201
-
202
- // Handle different input types
203
189
  if (prop.enum) {
204
- // Enum/Select field
205
190
  return (
206
- <div key={key} className="mb-4">
207
- <label className="block text-sm font-medium text-foreground mb-2">
208
- {prop.title || key}
209
- {isRequired && <span className="text-destructive ml-1">*</span>}
210
- </label>
211
- <select
212
- className="input"
213
- value={toolArgs[key] || prop.default || ''}
214
- onChange={(e) => setToolArgs({ ...toolArgs, [key]: e.target.value })}
215
- required={isRequired}
216
- >
191
+ <div key={key}>
192
+ <label className="block text-sm font-medium text-foreground mb-2">{prop.title || key}{isRequired && <span className="text-destructive ml-1">*</span>}</label>
193
+ <select className="input w-full" value={toolArgs[key] || prop.default || ''} onChange={(e) => setToolArgs({ ...toolArgs, [key]: e.target.value })} required={isRequired}>
217
194
  <option value="">Select...</option>
218
- {prop.enum.map((val: any) => (
219
- <option key={val} value={val}>
220
- {val}
221
- </option>
222
- ))}
195
+ {prop.enum.map((val: any) => <option key={val} value={val}>{val}</option>)}
223
196
  </select>
224
- {prop.description && (
225
- <p className="text-xs text-muted-foreground mt-1">{prop.description}</p>
226
- )}
227
197
  </div>
228
198
  );
229
199
  } else if (prop.type === 'boolean') {
230
- // Checkbox field
231
200
  return (
232
- <div key={key} className="mb-4">
233
- <label className="flex items-center gap-2 cursor-pointer">
234
- <input
235
- type="checkbox"
236
- className="w-4 h-4"
237
- checked={toolArgs[key] || false}
238
- onChange={(e) => setToolArgs({ ...toolArgs, [key]: e.target.checked })}
239
- />
240
- <span className="text-sm font-medium text-foreground">
241
- {prop.title || key}
242
- {isRequired && <span className="text-destructive ml-1">*</span>}
243
- </span>
201
+ <div key={key}>
202
+ <label className="flex items-center gap-2 cursor-pointer p-3 bg-muted/30 rounded-lg border border-border">
203
+ <input type="checkbox" className="checkbox checkbox-primary w-4 h-4" checked={toolArgs[key] || false} onChange={(e) => setToolArgs({ ...toolArgs, [key]: e.target.checked })} />
204
+ <span className="text-sm font-medium text-foreground">{prop.title || key}</span>
244
205
  </label>
245
- {prop.description && (
246
- <p className="text-xs text-muted-foreground mt-1 ml-6">{prop.description}</p>
247
- )}
248
206
  </div>
249
207
  );
250
208
  } else {
251
- // Text/Number field
252
209
  return (
253
- <div key={key} className="mb-4">
254
- <label className="block text-sm font-medium text-foreground mb-2">
255
- {prop.title || key}
256
- {isRequired && <span className="text-destructive ml-1">*</span>}
257
- </label>
258
- <input
259
- type={prop.type === 'number' || prop.type === 'integer' ? 'number' : 'text'}
260
- className="input"
261
- value={toolArgs[key] || prop.default || ''}
262
- onChange={(e) => {
263
- const value = prop.type === 'number' || prop.type === 'integer'
264
- ? (e.target.value ? Number(e.target.value) : '')
265
- : e.target.value;
266
- setToolArgs({ ...toolArgs, [key]: value });
267
- }}
268
- required={isRequired}
269
- placeholder={prop.description}
270
- min={prop.minimum}
271
- max={prop.maximum}
272
- />
273
- {prop.description && (
274
- <p className="text-xs text-muted-foreground mt-1">{prop.description}</p>
275
- )}
210
+ <div key={key}>
211
+ <label className="block text-sm font-medium text-foreground mb-2">{prop.title || key}{isRequired && <span className="text-destructive ml-1">*</span>}</label>
212
+ <input type={prop.type === 'number' || prop.type === 'integer' ? 'number' : 'text'} className="input w-full" value={toolArgs[key] || prop.default || ''} onChange={(e) => setToolArgs({ ...toolArgs, [key]: prop.type === 'number' || prop.type === 'integer' ? Number(e.target.value) : e.target.value })} required={isRequired} placeholder={prop.description} />
276
213
  </div>
277
214
  );
278
215
  }
279
216
  })}
280
- </>
281
- ) : (
282
- <div className="mb-4 p-4 bg-muted/30 rounded-lg border border-border">
283
- <p className="text-sm text-muted-foreground">No input required</p>
284
217
  </div>
218
+ ) : (
219
+ <div className="p-4 bg-muted/30 rounded-lg border border-border border-dashed text-center"><p className="text-sm text-muted-foreground">No arguments required</p></div>
285
220
  )}
286
221
 
287
- <button
288
- type="submit"
289
- className="btn btn-primary w-full gap-2"
290
- disabled={executingTool}
291
- >
292
- <Play className="w-4 h-4" />
293
- {executingTool ? 'Executing...' : 'Execute Tool'}
294
- </button>
222
+ <div className="sticky bottom-0 -mx-6 -mb-6 p-6 bg-card border-t border-border mt-auto">
223
+ <button type="submit" className="btn btn-primary w-full gap-2 py-3" disabled={executingTool}>
224
+ <PlayIcon className="w-4 h-4" />{executingTool ? 'Executing...' : 'Execute Tool'}
225
+ </button>
226
+ </div>
295
227
  </form>
296
228
 
297
- {/* Result */}
298
229
  {toolResult && (
299
- <div className="mt-6 space-y-4">
300
- <div>
301
- <h3 className="font-semibold text-foreground mb-3">Result:</h3>
302
- <pre className="bg-muted/30 p-4 rounded-lg text-sm overflow-auto max-h-64 text-foreground font-mono border border-border">
303
- {JSON.stringify(toolResult, null, 2)}
304
- </pre>
230
+ <div className="mt-8 pt-8 border-t border-border">
231
+ <div className="bg-muted/30 rounded-xl border border-border overflow-hidden mb-6">
232
+ <pre className="p-4 text-xs font-mono text-foreground overflow-auto max-h-96">{JSON.stringify(toolResult, null, 2)}</pre>
305
233
  </div>
306
-
307
- {/* Widget UI Rendering */}
308
234
  {(() => {
309
- // Get widget URI from multiple sources (same as ToolCard)
310
- const widgetUri =
311
- selectedTool.widget?.route ||
312
- selectedTool.outputTemplate ||
313
- selectedTool._meta?.['ui/template'] ||
314
- selectedTool._meta?.['openai/outputTemplate'];
315
-
235
+ const widgetUri = selectedTool.widget?.route || selectedTool.outputTemplate || selectedTool._meta?.['ui/template'] || selectedTool._meta?.['openai/outputTemplate'];
316
236
  return widgetUri && toolResult ? (
317
- <div>
318
- <h3 className="font-semibold text-foreground mb-3">UI Widget:</h3>
319
- <div className="border border-border rounded-lg overflow-hidden h-64 bg-background shadow-inner">
320
- <WidgetRenderer
321
- uri={widgetUri}
322
- data={(() => {
323
- // Try to parse JSON from content[0].text, otherwise use raw result
324
- if (toolResult.content?.[0]?.text) {
325
- try {
326
- const parsed = JSON.parse(toolResult.content[0].text);
327
- // Unwrap if response was wrapped by TransformInterceptor
328
- if (parsed.success !== undefined && parsed.data !== undefined) {
329
- return parsed.data;
330
- }
331
- return parsed;
332
- } catch {
333
- return { message: toolResult.content[0].text };
334
- }
335
- }
336
- return toolResult;
337
- })()}
338
- className="w-full h-full"
339
- />
340
- </div>
237
+ <div className="border border-border rounded-xl overflow-hidden bg-background shadow-sm">
238
+ <WidgetRenderer uri={widgetUri} data={(() => {
239
+ if (toolResult.content?.[0]?.text) {
240
+ try { const parsed = JSON.parse(toolResult.content[0].text); return (parsed.success !== undefined && parsed.data !== undefined) ? parsed.data : parsed; } catch { return { message: toolResult.content[0].text }; }
241
+ }
242
+ return toolResult;
243
+ })()} className="w-full widget-in-chat widget-expanded" />
341
244
  </div>
342
245
  ) : null;
343
246
  })()}
@@ -345,7 +248,8 @@ export default function ToolsPage() {
345
248
  )}
346
249
  </div>
347
250
  </div>
348
- )}
251
+ </div>
252
+ )}
349
253
  </>
350
254
  );
351
255
  }