nitrostack 1.0.1 → 1.0.3

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 (51) hide show
  1. package/CHANGELOG.md +23 -0
  2. package/dist/cli/index.js +4 -1
  3. package/dist/cli/index.js.map +1 -1
  4. package/package.json +1 -1
  5. package/src/studio/README.md +140 -0
  6. package/src/studio/app/api/auth/fetch-metadata/route.ts +71 -0
  7. package/src/studio/app/api/auth/register-client/route.ts +67 -0
  8. package/src/studio/app/api/chat/route.ts +123 -0
  9. package/src/studio/app/api/health/checks/route.ts +42 -0
  10. package/src/studio/app/api/health/route.ts +13 -0
  11. package/src/studio/app/api/init/route.ts +85 -0
  12. package/src/studio/app/api/ping/route.ts +13 -0
  13. package/src/studio/app/api/prompts/[name]/route.ts +21 -0
  14. package/src/studio/app/api/prompts/route.ts +13 -0
  15. package/src/studio/app/api/resources/[...uri]/route.ts +18 -0
  16. package/src/studio/app/api/resources/route.ts +13 -0
  17. package/src/studio/app/api/roots/route.ts +13 -0
  18. package/src/studio/app/api/sampling/route.ts +14 -0
  19. package/src/studio/app/api/tools/[name]/call/route.ts +41 -0
  20. package/src/studio/app/api/tools/route.ts +23 -0
  21. package/src/studio/app/api/widget-examples/route.ts +44 -0
  22. package/src/studio/app/auth/callback/page.tsx +160 -0
  23. package/src/studio/app/auth/page.tsx +543 -0
  24. package/src/studio/app/chat/page.tsx +530 -0
  25. package/src/studio/app/chat/page.tsx.backup +390 -0
  26. package/src/studio/app/globals.css +410 -0
  27. package/src/studio/app/health/page.tsx +177 -0
  28. package/src/studio/app/layout.tsx +48 -0
  29. package/src/studio/app/page.tsx +337 -0
  30. package/src/studio/app/page.tsx.backup +346 -0
  31. package/src/studio/app/ping/page.tsx +204 -0
  32. package/src/studio/app/prompts/page.tsx +228 -0
  33. package/src/studio/app/resources/page.tsx +313 -0
  34. package/src/studio/components/EnlargeModal.tsx +116 -0
  35. package/src/studio/components/Sidebar.tsx +133 -0
  36. package/src/studio/components/ToolCard.tsx +108 -0
  37. package/src/studio/components/WidgetRenderer.tsx +99 -0
  38. package/src/studio/lib/api.ts +207 -0
  39. package/src/studio/lib/llm-service.ts +361 -0
  40. package/src/studio/lib/mcp-client.ts +168 -0
  41. package/src/studio/lib/store.ts +192 -0
  42. package/src/studio/lib/theme-provider.tsx +50 -0
  43. package/src/studio/lib/types.ts +107 -0
  44. package/src/studio/lib/widget-loader.ts +90 -0
  45. package/src/studio/middleware.ts +27 -0
  46. package/src/studio/next.config.js +16 -0
  47. package/src/studio/package-lock.json +2696 -0
  48. package/src/studio/package.json +34 -0
  49. package/src/studio/postcss.config.mjs +10 -0
  50. package/src/studio/tailwind.config.ts +67 -0
  51. package/src/studio/tsconfig.json +42 -0
@@ -0,0 +1,313 @@
1
+ 'use client';
2
+
3
+ import { useEffect, useState, useCallback } from 'react';
4
+ import { useStudioStore } from '@/lib/store';
5
+ import { api } from '@/lib/api';
6
+ import { WidgetRenderer } from '@/components/WidgetRenderer';
7
+ import type { Resource } from '@/lib/types';
8
+ import { Package, RefreshCw, Palette, Maximize2, Sparkles, AlertCircle } from 'lucide-react';
9
+
10
+ interface WidgetExample {
11
+ name: string;
12
+ description: string;
13
+ data: Record<string, any>;
14
+ }
15
+
16
+ interface WidgetMetadata {
17
+ uri: string;
18
+ name: string;
19
+ description: string;
20
+ examples: WidgetExample[];
21
+ tags?: string[];
22
+ }
23
+
24
+ export default function ResourcesPage() {
25
+ const { resources, setResources, loading, setLoading, openEnlargeModal } = useStudioStore();
26
+ const [searchQuery, setSearchQuery] = useState('');
27
+ const [widgets, setWidgets] = useState<WidgetMetadata[]>([]);
28
+ const [loadingWidgets, setLoadingWidgets] = useState(false);
29
+ const [selectedExamples, setSelectedExamples] = useState<Record<string, number>>({});
30
+
31
+ useEffect(() => {
32
+ loadResources();
33
+ loadWidgets();
34
+ }, []);
35
+
36
+ const loadResources = async () => {
37
+ setLoading('resources', true);
38
+ try {
39
+ const data = await api.getResources();
40
+ setResources(data.resources || []);
41
+ } catch (error) {
42
+ console.error('Failed to load resources:', error);
43
+ } finally {
44
+ setLoading('resources', false);
45
+ }
46
+ };
47
+
48
+ const loadWidgets = async () => {
49
+ setLoadingWidgets(true);
50
+ try {
51
+ console.log('📦 Loading widget examples from API...');
52
+ const data = await api.getWidgetExamples();
53
+ console.log('📦 Widget examples response:', data);
54
+ console.log('📦 Number of widgets loaded:', data.widgets?.length || 0);
55
+
56
+ if (data.widgets && data.widgets.length > 0) {
57
+ console.log('📦 First widget:', data.widgets[0]);
58
+ } else {
59
+ console.warn('⚠️ No widgets returned from API. Check MCP server logs.');
60
+ }
61
+
62
+ setWidgets(data.widgets || []);
63
+ } catch (error) {
64
+ console.error('❌ Failed to load widget examples:', error);
65
+ } finally {
66
+ setLoadingWidgets(false);
67
+ }
68
+ };
69
+
70
+ const isUIWidget = (resource: Resource) => {
71
+ return resource.mimeType === 'text/html' ||
72
+ resource.uri.startsWith('widget://') ||
73
+ resource._meta?.['ui/widget'] === true;
74
+ };
75
+
76
+ const filteredResources = resources.filter(
77
+ (resource) =>
78
+ !isUIWidget(resource) &&
79
+ (resource.name.toLowerCase().includes(searchQuery.toLowerCase()) ||
80
+ resource.uri.toLowerCase().includes(searchQuery.toLowerCase()))
81
+ );
82
+
83
+ const filteredWidgets = widgets.filter((widget) =>
84
+ widget.name.toLowerCase().includes(searchQuery.toLowerCase()) ||
85
+ widget.uri.toLowerCase().includes(searchQuery.toLowerCase()) ||
86
+ widget.description.toLowerCase().includes(searchQuery.toLowerCase())
87
+ );
88
+
89
+ const handleExampleChange = useCallback((widgetUri: string, exampleIndex: number) => {
90
+ setSelectedExamples(prev => ({ ...prev, [widgetUri]: exampleIndex }));
91
+ }, []);
92
+
93
+ const getSelectedExample = useCallback((widgetUri: string) => {
94
+ return selectedExamples[widgetUri] || 0;
95
+ }, [selectedExamples]);
96
+
97
+ const handleWidgetEnlarge = useCallback((e: React.MouseEvent, widget: WidgetMetadata) => {
98
+ e.preventDefault();
99
+ e.stopPropagation();
100
+
101
+ const exampleIndex = getSelectedExample(widget.uri);
102
+
103
+ setTimeout(() => {
104
+ openEnlargeModal('resource', {
105
+ uri: widget.uri,
106
+ name: widget.name,
107
+ description: widget.description,
108
+ responseData: widget.examples[exampleIndex]?.data
109
+ });
110
+ }, 0);
111
+ }, [selectedExamples, openEnlargeModal, getSelectedExample]);
112
+
113
+ return (
114
+ <div className="min-h-screen bg-background p-8">
115
+ {/* Header */}
116
+ <div className="mb-8">
117
+ <div className="flex items-center justify-between mb-6">
118
+ <div className="flex items-center gap-3">
119
+ <div className="w-12 h-12 rounded-lg bg-gradient-to-br from-purple-500 to-pink-500 flex items-center justify-center">
120
+ <Package className="w-6 h-6 text-white" />
121
+ </div>
122
+ <div>
123
+ <h1 className="text-3xl font-bold text-foreground">Resources</h1>
124
+ <p className="text-muted-foreground mt-1">Browse MCP resources and UI widgets</p>
125
+ </div>
126
+ </div>
127
+ <button onClick={loadResources} className="btn btn-primary gap-2">
128
+ <RefreshCw className="w-4 h-4" />
129
+ Refresh
130
+ </button>
131
+ </div>
132
+
133
+ <input
134
+ type="text"
135
+ placeholder="Search resources..."
136
+ value={searchQuery}
137
+ onChange={(e) => setSearchQuery(e.target.value)}
138
+ className="input"
139
+ />
140
+ </div>
141
+
142
+ {/* UI Widgets Section */}
143
+ {(loadingWidgets || widgets.length > 0) && (
144
+ <div className="mb-12">
145
+ <h2 className="text-2xl font-bold text-foreground mb-6 flex items-center gap-2">
146
+ <Palette className="w-6 h-6 text-primary" />
147
+ UI Widgets {widgets.length > 0 && `(${filteredWidgets.length})`}
148
+ </h2>
149
+
150
+ {loadingWidgets ? (
151
+ <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
152
+ {[1, 2].map((i) => (
153
+ <div key={i} className="card skeleton h-96"></div>
154
+ ))}
155
+ </div>
156
+ ) : filteredWidgets.length === 0 ? (
157
+ <div className="card p-8 text-center">
158
+ <AlertCircle className="w-12 h-12 text-muted-foreground mx-auto mb-3" />
159
+ <p className="text-muted-foreground">
160
+ {searchQuery ? 'No widgets match your search' : 'No UI widgets configured'}
161
+ </p>
162
+ </div>
163
+ ) : (
164
+ <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
165
+ {filteredWidgets.map((widget) => (
166
+ <div key={widget.uri} className="card card-hover p-6 animate-fade-in">
167
+ <div className="flex items-center gap-3 mb-4">
168
+ <div className="w-10 h-10 rounded-lg bg-purple-500/10 flex items-center justify-center">
169
+ <Palette className="w-5 h-5 text-purple-500" />
170
+ </div>
171
+ <div className="flex-1">
172
+ <h3 className="font-semibold text-foreground">{widget.name}</h3>
173
+ <span className="badge badge-success text-xs mt-1">
174
+ {widget.examples.length} example{widget.examples.length !== 1 ? 's' : ''}
175
+ </span>
176
+ </div>
177
+ </div>
178
+
179
+ <p className="text-sm text-muted-foreground mb-3 line-clamp-2">
180
+ {widget.description}
181
+ </p>
182
+
183
+ <p className="text-xs text-muted-foreground mb-4 font-mono truncate bg-muted/30 px-2 py-1 rounded">
184
+ {widget.uri}
185
+ </p>
186
+
187
+ {widget.tags && widget.tags.length > 0 && (
188
+ <div className="flex flex-wrap gap-2 mb-4">
189
+ {widget.tags.map((tag) => (
190
+ <span key={tag} className="badge badge-secondary text-xs">
191
+ #{tag}
192
+ </span>
193
+ ))}
194
+ </div>
195
+ )}
196
+
197
+ {/* Widget Preview with selected example */}
198
+ {widget.examples.length > 0 && (() => {
199
+ const selectedIdx = getSelectedExample(widget.uri);
200
+ const selectedExample = widget.examples[selectedIdx];
201
+ return (
202
+ <div className="relative mb-4 rounded-lg overflow-hidden border border-border bg-muted/20">
203
+ <div className="absolute top-2 left-2 z-10 flex items-center gap-1 bg-primary/90 backdrop-blur-sm text-black px-2 py-1 rounded-md text-xs font-semibold shadow-lg">
204
+ <Sparkles className="w-3 h-3" />
205
+ {selectedExample.name}
206
+ </div>
207
+ <div className="h-64">
208
+ <WidgetRenderer
209
+ uri={widget.uri}
210
+ data={selectedExample.data}
211
+ />
212
+ </div>
213
+ </div>
214
+ );
215
+ })()}
216
+
217
+ {/* Example Selector (if more than 1) */}
218
+ {widget.examples.length > 1 && (
219
+ <select
220
+ className="input text-sm mb-4"
221
+ value={getSelectedExample(widget.uri)}
222
+ onChange={(e) => handleExampleChange(widget.uri, parseInt(e.target.value))}
223
+ >
224
+ {widget.examples.map((example, idx) => (
225
+ <option key={idx} value={idx}>
226
+ {example.name}
227
+ </option>
228
+ ))}
229
+ </select>
230
+ )}
231
+
232
+ <button
233
+ onClick={(e) => handleWidgetEnlarge(e, widget)}
234
+ className="btn btn-secondary w-full gap-2"
235
+ >
236
+ <Maximize2 className="w-4 h-4" />
237
+ Enlarge
238
+ </button>
239
+ </div>
240
+ ))}
241
+ </div>
242
+ )}
243
+ </div>
244
+ )}
245
+
246
+ {/* MCP Resources Section */}
247
+ <div>
248
+ <h2 className="text-2xl font-bold text-foreground mb-6 flex items-center gap-2">
249
+ <Package className="w-6 h-6 text-primary" />
250
+ MCP Resources {resources.length > 0 && `(${filteredResources.length})`}
251
+ </h2>
252
+
253
+ {/* Resources Grid */}
254
+ {loading.resources ? (
255
+ <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
256
+ {[1, 2, 3].map((i) => (
257
+ <div key={i} className="card skeleton h-64"></div>
258
+ ))}
259
+ </div>
260
+ ) : filteredResources.length === 0 ? (
261
+ <div className="empty-state">
262
+ <Package className="empty-state-icon" />
263
+ <p className="empty-state-title">
264
+ {searchQuery ? 'No resources found' : 'No resources available'}
265
+ </p>
266
+ <p className="empty-state-description">
267
+ {searchQuery ? 'Try a different search term' : 'No MCP resources have been registered'}
268
+ </p>
269
+ </div>
270
+ ) : (
271
+ <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
272
+ {filteredResources.map((resource) => (
273
+ <div key={resource.uri} className="card card-hover p-6 animate-fade-in">
274
+ <div className="flex items-start justify-between mb-4">
275
+ <div className="flex items-center gap-3">
276
+ <div className="w-10 h-10 rounded-lg bg-blue-500/10 flex items-center justify-center">
277
+ <Package className="w-5 h-5 text-blue-500" />
278
+ </div>
279
+ <div>
280
+ <h3 className="font-semibold text-foreground">{resource.name}</h3>
281
+ <span className="badge badge-primary text-xs mt-1">
282
+ Resource
283
+ </span>
284
+ </div>
285
+ </div>
286
+ </div>
287
+
288
+ <p className="text-sm text-muted-foreground mb-4 line-clamp-2">
289
+ {resource.description || 'No description'}
290
+ </p>
291
+
292
+ <div className="bg-muted/30 rounded-lg p-3 border border-border">
293
+ <p className="text-xs text-muted-foreground mb-1 font-semibold uppercase tracking-wide">URI</p>
294
+ <p className="text-xs text-foreground font-mono truncate">
295
+ {resource.uri}
296
+ </p>
297
+ </div>
298
+
299
+ {resource.mimeType && (
300
+ <div className="mt-3">
301
+ <span className="badge badge-secondary text-xs">
302
+ {resource.mimeType}
303
+ </span>
304
+ </div>
305
+ )}
306
+ </div>
307
+ ))}
308
+ </div>
309
+ )}
310
+ </div>
311
+ </div>
312
+ );
313
+ }
@@ -0,0 +1,116 @@
1
+ 'use client';
2
+
3
+ import { useStudioStore } from '@/lib/store';
4
+ import { WidgetRenderer } from './WidgetRenderer';
5
+ import { X, MessageSquare } from 'lucide-react';
6
+ import { useRouter } from 'next/navigation';
7
+
8
+ export function EnlargeModal() {
9
+ const { enlargeModal, closeEnlargeModal, setCurrentTab } = useStudioStore();
10
+ const router = useRouter();
11
+
12
+ if (!enlargeModal.open || !enlargeModal.item) return null;
13
+
14
+ const { type, item } = enlargeModal;
15
+
16
+ // Get widget URI
17
+ const componentUri =
18
+ type === 'tool'
19
+ ? (item.widget?.route || item.outputTemplate || item._meta?.['openai/outputTemplate'] || item._meta?.['ui/template'])
20
+ : item.uri;
21
+
22
+ // Get data from item's examples or responseData
23
+ const widgetData = item.examples?.response || item.responseData || {};
24
+
25
+ const handleUseInChat = () => {
26
+ if (type !== 'tool') return;
27
+
28
+ closeEnlargeModal();
29
+ setCurrentTab('chat');
30
+ router.push('/chat');
31
+
32
+ // Store the tool name so chat can suggest using it
33
+ if (typeof window !== 'undefined') {
34
+ window.localStorage.setItem('suggestedTool', item.name);
35
+ }
36
+ };
37
+
38
+ return (
39
+ <div
40
+ className="fixed inset-0 z-50 flex items-center justify-center animate-fade-in"
41
+ style={{ backgroundColor: 'rgba(0, 0, 0, 0.85)' }}
42
+ onClick={closeEnlargeModal}
43
+ >
44
+ <div
45
+ className="relative w-[90vw] h-[90vh] bg-card border border-border rounded-2xl shadow-2xl overflow-hidden animate-scale-in"
46
+ onClick={(e) => e.stopPropagation()}
47
+ >
48
+ {/* 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">
55
+ {item.description || 'No description available'}
56
+ </p>
57
+ </div>
58
+ </div>
59
+
60
+ <div className="flex items-center gap-2">
61
+ {type === 'tool' && (
62
+ <button
63
+ onClick={handleUseInChat}
64
+ className="btn btn-primary flex items-center gap-2"
65
+ >
66
+ <MessageSquare className="w-4 h-4" />
67
+ Use in Chat
68
+ </button>
69
+ )}
70
+ <button
71
+ onClick={closeEnlargeModal}
72
+ className="btn btn-ghost w-10 h-10 p-0 flex items-center justify-center"
73
+ aria-label="Close"
74
+ >
75
+ <X className="w-5 h-5" />
76
+ </button>
77
+ </div>
78
+ </div>
79
+
80
+ {/* Widget Content */}
81
+ <div className="h-[calc(100%-88px)] p-6 overflow-auto bg-background">
82
+ {componentUri && widgetData ? (
83
+ <div className="w-full h-full rounded-xl overflow-hidden border border-border shadow-inner">
84
+ <WidgetRenderer uri={componentUri} data={widgetData} className="w-full h-full" />
85
+ </div>
86
+ ) : (
87
+ <div className="flex items-center justify-center h-full">
88
+ <div className="text-center">
89
+ <p className="text-muted-foreground mb-2">
90
+ {!componentUri ? 'No widget URI available' : 'No example data available'}
91
+ </p>
92
+ <p className="text-xs text-muted-foreground">
93
+ {type === 'tool' ? 'This tool does not have a UI widget attached' : 'No preview data found'}
94
+ </p>
95
+ </div>
96
+ </div>
97
+ )}
98
+ </div>
99
+
100
+ {/* Metadata Footer */}
101
+ {type === 'tool' && item.inputSchema && (
102
+ <div className="absolute bottom-0 left-0 right-0 p-4 border-t border-border glass">
103
+ <details className="text-sm">
104
+ <summary className="cursor-pointer text-muted-foreground hover:text-foreground font-medium">
105
+ Input Schema
106
+ </summary>
107
+ <pre className="mt-2 p-3 bg-muted rounded-lg text-xs text-foreground overflow-auto max-h-40 font-mono">
108
+ {JSON.stringify(item.inputSchema, null, 2)}
109
+ </pre>
110
+ </details>
111
+ </div>
112
+ )}
113
+ </div>
114
+ </div>
115
+ );
116
+ }
@@ -0,0 +1,133 @@
1
+ 'use client';
2
+
3
+ import { useStudioStore } from '@/lib/store';
4
+ import type { TabType } from '@/lib/types';
5
+ import { useRouter, usePathname } from 'next/navigation';
6
+ import { useEffect, useState } from 'react';
7
+ import {
8
+ Wrench,
9
+ MessageSquare,
10
+ Package,
11
+ FileText,
12
+ Activity,
13
+ Shield,
14
+ Wifi,
15
+ Zap
16
+ } from 'lucide-react';
17
+
18
+ const navItems: Array<{ id: TabType; label: string; icon: any; path: string }> = [
19
+ { id: 'tools', label: 'Tools', icon: Wrench, path: '/' },
20
+ { id: 'chat', label: 'AI Chat', icon: MessageSquare, path: '/chat' },
21
+ { id: 'resources', label: 'Resources', icon: Package, path: '/resources' },
22
+ { id: 'prompts', label: 'Prompts', icon: FileText, path: '/prompts' },
23
+ { id: 'health', label: 'Health', icon: Activity, path: '/health' },
24
+ { id: 'auth', label: 'OAuth 2.1', icon: Shield, path: '/auth' },
25
+ { id: 'ping', label: 'Ping', icon: Wifi, path: '/ping' },
26
+ ];
27
+
28
+ export function Sidebar() {
29
+ const { connection } = useStudioStore();
30
+ const router = useRouter();
31
+ const pathname = usePathname();
32
+ const [mounted, setMounted] = useState(false);
33
+
34
+ useEffect(() => {
35
+ setMounted(true);
36
+ // Force dark mode
37
+ document.documentElement.className = 'dark antialiased';
38
+ }, []);
39
+
40
+ const handleNavigation = (path: string) => {
41
+ router.push(path);
42
+ };
43
+
44
+ const getConnectionStatus = () => {
45
+ if (connection.status === 'connected') return 'connected';
46
+ if (connection.status === 'connecting') return 'connecting';
47
+ return 'disconnected';
48
+ };
49
+
50
+ if (!mounted) return null;
51
+
52
+ return (
53
+ <nav className="fixed left-0 top-0 h-screen w-64 glass flex flex-col z-50">
54
+ {/* Header */}
55
+ <div className="p-6 border-b border-border">
56
+ <div className="flex items-center gap-3 mb-4">
57
+ <div className="w-10 h-10 rounded-lg bg-gradient-to-br from-primary to-amber-500 flex items-center justify-center shadow-lg">
58
+ <Zap className="w-6 h-6 text-black" />
59
+ </div>
60
+ <div>
61
+ <h1 className="text-xl font-bold bg-gradient-to-r from-primary to-amber-500 bg-clip-text text-transparent">
62
+ NitroStack
63
+ </h1>
64
+ <p className="text-xs text-muted-foreground font-medium">Studio v3.1</p>
65
+ </div>
66
+ </div>
67
+
68
+ {/* Connection Status */}
69
+ <div className="flex items-center gap-2 px-3 py-2 rounded-lg bg-muted/50 border border-border/50">
70
+ <div className="relative flex items-center justify-center">
71
+ <div
72
+ className={`w-2.5 h-2.5 rounded-full ${
73
+ getConnectionStatus() === 'connected'
74
+ ? 'bg-emerald-500'
75
+ : getConnectionStatus() === 'connecting'
76
+ ? 'bg-amber-500'
77
+ : 'bg-rose-500'
78
+ }`}
79
+ style={{
80
+ boxShadow: getConnectionStatus() === 'connected'
81
+ ? '0 0 12px rgba(34, 197, 94, 0.6)'
82
+ : getConnectionStatus() === 'connecting'
83
+ ? '0 0 12px rgba(245, 158, 11, 0.6)'
84
+ : '0 0 12px rgba(239, 68, 68, 0.6)'
85
+ }}
86
+ />
87
+ </div>
88
+ <span className="text-xs font-semibold uppercase tracking-wide text-foreground">
89
+ {connection.status}
90
+ </span>
91
+ </div>
92
+ </div>
93
+
94
+ {/* Navigation */}
95
+ <div className="flex-1 overflow-y-auto py-4 px-2">
96
+ <div className="space-y-1">
97
+ {navItems.map((item) => {
98
+ const Icon = item.icon;
99
+ const isActive = pathname === item.path;
100
+
101
+ return (
102
+ <button
103
+ key={item.id}
104
+ onClick={() => handleNavigation(item.path)}
105
+ className={`
106
+ w-full flex items-center gap-3 px-4 py-2.5 text-sm font-medium rounded-lg transition-all group
107
+ ${isActive
108
+ ? 'bg-primary/10 text-primary shadow-sm ring-1 ring-primary/20'
109
+ : 'text-foreground hover:bg-muted hover:text-primary'
110
+ }
111
+ `}
112
+ >
113
+ <Icon className={`w-5 h-5 ${isActive ? 'text-primary' : 'text-muted-foreground group-hover:text-primary'}`} />
114
+ <span>{item.label}</span>
115
+ </button>
116
+ );
117
+ })}
118
+ </div>
119
+ </div>
120
+
121
+ {/* Footer */}
122
+ <div className="p-4 border-t border-border bg-card/50 backdrop-blur">
123
+ {/* Version */}
124
+ <div className="px-4 py-2 text-xs text-muted-foreground">
125
+ <div className="flex items-center justify-between">
126
+ <span>MCP Protocol</span>
127
+ <span className="font-mono font-semibold text-primary">v1.0</span>
128
+ </div>
129
+ </div>
130
+ </div>
131
+ </nav>
132
+ );
133
+ }
@@ -0,0 +1,108 @@
1
+ 'use client';
2
+
3
+ import type { Tool } from '@/lib/types';
4
+ import { useStudioStore } from '@/lib/store';
5
+ import { WidgetRenderer } from './WidgetRenderer';
6
+ import { useRouter } from 'next/navigation';
7
+ import { Zap, Palette, Maximize2, Play, Sparkles } from 'lucide-react';
8
+
9
+ interface ToolCardProps {
10
+ tool: Tool;
11
+ onExecute: (tool: Tool) => void;
12
+ }
13
+
14
+ export function ToolCard({ tool, onExecute }: ToolCardProps) {
15
+ const { openEnlargeModal } = useStudioStore();
16
+ const router = useRouter();
17
+
18
+ // Check if tool has widget - check multiple sources
19
+ const widgetUri =
20
+ tool.widget?.route ||
21
+ tool.outputTemplate ||
22
+ tool._meta?.['ui/template'] ||
23
+ tool._meta?.['openai/outputTemplate'];
24
+ const hasWidget = !!widgetUri;
25
+
26
+ // Get example data for preview
27
+ const exampleData = tool.examples?.response;
28
+
29
+ const handleUseInChat = (e: React.MouseEvent) => {
30
+ e.stopPropagation();
31
+ router.push(`/chat?tool=${tool.name}`);
32
+ };
33
+
34
+ const handleEnlarge = (e: React.MouseEvent) => {
35
+ e.stopPropagation();
36
+ openEnlargeModal('tool', { ...tool, responseData: exampleData });
37
+ };
38
+
39
+ return (
40
+ <div
41
+ className="card card-hover p-6 animate-fade-in cursor-pointer"
42
+ onClick={() => onExecute(tool)}
43
+ >
44
+ {/* Header */}
45
+ <div className="flex items-start justify-between mb-4">
46
+ <div className="flex items-center gap-3">
47
+ <div className={`w-12 h-12 rounded-lg flex items-center justify-center ${hasWidget ? 'bg-purple-500/10' : 'bg-primary/10'}`}>
48
+ {hasWidget ? (
49
+ <Palette className="w-6 h-6 text-purple-500" />
50
+ ) : (
51
+ <Zap className="w-6 h-6 text-primary" />
52
+ )}
53
+ </div>
54
+ <div>
55
+ <h3 className="font-semibold text-lg text-foreground">
56
+ {tool.name}
57
+ </h3>
58
+ <span className={`badge ${hasWidget ? 'badge-secondary' : 'badge-primary'} text-xs mt-1`}>
59
+ {hasWidget ? 'tool + widget' : 'tool'}
60
+ </span>
61
+ </div>
62
+ </div>
63
+ </div>
64
+
65
+ {/* Description */}
66
+ <p className="text-sm text-muted-foreground mb-4 line-clamp-2">
67
+ {tool.description || 'No description'}
68
+ </p>
69
+
70
+ {/* Widget Preview */}
71
+ {hasWidget && exampleData && (
72
+ <div className="relative mb-4 rounded-lg overflow-hidden border border-border bg-muted/20">
73
+ <div className="absolute top-2 left-2 z-10 flex items-center gap-1 bg-primary/90 backdrop-blur-sm text-black px-2 py-1 rounded-md text-xs font-semibold shadow-lg">
74
+ <Sparkles className="w-3 h-3" />
75
+ Widget Preview
76
+ </div>
77
+ <div className="h-64 relative">
78
+ <WidgetRenderer
79
+ uri={widgetUri}
80
+ data={exampleData}
81
+ className="w-full h-full"
82
+ />
83
+ </div>
84
+ </div>
85
+ )}
86
+
87
+ {/* Action Buttons */}
88
+ <div className="flex items-center gap-2" onClick={(e) => e.stopPropagation()}>
89
+ {hasWidget && exampleData && (
90
+ <button
91
+ onClick={handleEnlarge}
92
+ className="btn btn-secondary flex-1 text-sm gap-2"
93
+ >
94
+ <Maximize2 className="w-4 h-4" />
95
+ <span>Enlarge</span>
96
+ </button>
97
+ )}
98
+ <button
99
+ onClick={() => onExecute(tool)}
100
+ className="btn btn-primary flex-1 text-sm gap-2"
101
+ >
102
+ <Play className="w-4 h-4" />
103
+ <span>Execute</span>
104
+ </button>
105
+ </div>
106
+ </div>
107
+ );
108
+ }