nitrostack 1.0.1 → 1.0.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +15 -0
- package/dist/cli/index.js +4 -1
- package/dist/cli/index.js.map +1 -1
- package/package.json +1 -1
- package/src/studio/README.md +140 -0
- package/src/studio/app/api/auth/fetch-metadata/route.ts +71 -0
- package/src/studio/app/api/auth/register-client/route.ts +67 -0
- package/src/studio/app/api/chat/route.ts +123 -0
- package/src/studio/app/api/health/checks/route.ts +42 -0
- package/src/studio/app/api/health/route.ts +13 -0
- package/src/studio/app/api/init/route.ts +85 -0
- package/src/studio/app/api/ping/route.ts +13 -0
- package/src/studio/app/api/prompts/[name]/route.ts +21 -0
- package/src/studio/app/api/prompts/route.ts +13 -0
- package/src/studio/app/api/resources/[...uri]/route.ts +18 -0
- package/src/studio/app/api/resources/route.ts +13 -0
- package/src/studio/app/api/roots/route.ts +13 -0
- package/src/studio/app/api/sampling/route.ts +14 -0
- package/src/studio/app/api/tools/[name]/call/route.ts +41 -0
- package/src/studio/app/api/tools/route.ts +23 -0
- package/src/studio/app/api/widget-examples/route.ts +44 -0
- package/src/studio/app/auth/callback/page.tsx +160 -0
- package/src/studio/app/auth/page.tsx +543 -0
- package/src/studio/app/chat/page.tsx +530 -0
- package/src/studio/app/chat/page.tsx.backup +390 -0
- package/src/studio/app/globals.css +410 -0
- package/src/studio/app/health/page.tsx +177 -0
- package/src/studio/app/layout.tsx +48 -0
- package/src/studio/app/page.tsx +337 -0
- package/src/studio/app/page.tsx.backup +346 -0
- package/src/studio/app/ping/page.tsx +204 -0
- package/src/studio/app/prompts/page.tsx +228 -0
- package/src/studio/app/resources/page.tsx +313 -0
- package/src/studio/components/EnlargeModal.tsx +116 -0
- package/src/studio/components/Sidebar.tsx +133 -0
- package/src/studio/components/ToolCard.tsx +108 -0
- package/src/studio/components/WidgetRenderer.tsx +99 -0
- package/src/studio/lib/api.ts +207 -0
- package/src/studio/lib/llm-service.ts +361 -0
- package/src/studio/lib/mcp-client.ts +168 -0
- package/src/studio/lib/store.ts +192 -0
- package/src/studio/lib/theme-provider.tsx +50 -0
- package/src/studio/lib/types.ts +107 -0
- package/src/studio/lib/widget-loader.ts +90 -0
- package/src/studio/middleware.ts +27 -0
- package/src/studio/next.config.js +16 -0
- package/src/studio/package-lock.json +2696 -0
- package/src/studio/package.json +34 -0
- package/src/studio/postcss.config.mjs +10 -0
- package/src/studio/tailwind.config.ts +67 -0
- package/src/studio/tsconfig.json +41 -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
|
+
}
|