nitrostack 1.0.65 → 1.0.66
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/package.json +2 -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 +250 -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 +109 -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 +175 -0
- package/src/studio/app/auth/page.tsx +560 -0
- package/src/studio/app/chat/page.tsx +1133 -0
- package/src/studio/app/chat/page.tsx.backup +390 -0
- package/src/studio/app/globals.css +486 -0
- package/src/studio/app/health/page.tsx +179 -0
- package/src/studio/app/layout.tsx +68 -0
- package/src/studio/app/logs/page.tsx +279 -0
- package/src/studio/app/page.tsx +351 -0
- package/src/studio/app/page.tsx.backup +346 -0
- package/src/studio/app/ping/page.tsx +209 -0
- package/src/studio/app/prompts/page.tsx +230 -0
- package/src/studio/app/resources/page.tsx +315 -0
- package/src/studio/app/settings/page.tsx +199 -0
- package/src/studio/branding.md +807 -0
- package/src/studio/components/EnlargeModal.tsx +138 -0
- package/src/studio/components/LogMessage.tsx +153 -0
- package/src/studio/components/MarkdownRenderer.tsx +410 -0
- package/src/studio/components/Sidebar.tsx +295 -0
- package/src/studio/components/ToolCard.tsx +139 -0
- package/src/studio/components/WidgetRenderer.tsx +346 -0
- package/src/studio/lib/api.ts +207 -0
- package/src/studio/lib/http-client-transport.ts +222 -0
- package/src/studio/lib/llm-service.ts +480 -0
- package/src/studio/lib/log-manager.ts +76 -0
- package/src/studio/lib/mcp-client.ts +258 -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 +38 -0
- package/src/studio/package.json +35 -0
- package/src/studio/postcss.config.mjs +10 -0
- package/src/studio/public/nitrocloud.png +0 -0
- package/src/studio/tailwind.config.ts +67 -0
- package/src/studio/tsconfig.json +42 -0
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { useEffect, useState } from 'react';
|
|
4
|
+
import { useStudioStore } from '@/lib/store';
|
|
5
|
+
import { api } from '@/lib/api';
|
|
6
|
+
import type { Prompt } from '@/lib/types';
|
|
7
|
+
import { FileText, RefreshCw, X, Play, AlertCircle } from 'lucide-react';
|
|
8
|
+
|
|
9
|
+
export default function PromptsPage() {
|
|
10
|
+
const { prompts, setPrompts, loading, setLoading } = useStudioStore();
|
|
11
|
+
const [searchQuery, setSearchQuery] = useState('');
|
|
12
|
+
const [selectedPrompt, setSelectedPrompt] = useState<Prompt | null>(null);
|
|
13
|
+
const [promptArgs, setPromptArgs] = useState<Record<string, string>>({});
|
|
14
|
+
const [promptResult, setPromptResult] = useState<any>(null);
|
|
15
|
+
const [executing, setExecuting] = useState(false);
|
|
16
|
+
|
|
17
|
+
useEffect(() => {
|
|
18
|
+
loadPrompts();
|
|
19
|
+
}, []);
|
|
20
|
+
|
|
21
|
+
const loadPrompts = async () => {
|
|
22
|
+
setLoading('prompts', true);
|
|
23
|
+
try {
|
|
24
|
+
const data = await api.getPrompts();
|
|
25
|
+
setPrompts(data.prompts || []);
|
|
26
|
+
} catch (error) {
|
|
27
|
+
console.error('Failed to load prompts:', error);
|
|
28
|
+
} finally {
|
|
29
|
+
setLoading('prompts', false);
|
|
30
|
+
}
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
const handleExecutePrompt = async (e: React.FormEvent) => {
|
|
34
|
+
e.preventDefault();
|
|
35
|
+
if (!selectedPrompt) return;
|
|
36
|
+
|
|
37
|
+
setExecuting(true);
|
|
38
|
+
setPromptResult(null);
|
|
39
|
+
|
|
40
|
+
try {
|
|
41
|
+
const result = await api.executePrompt(selectedPrompt.name, promptArgs);
|
|
42
|
+
setPromptResult(result);
|
|
43
|
+
} catch (error) {
|
|
44
|
+
console.error('Prompt execution failed:', error);
|
|
45
|
+
setPromptResult({ error: 'Execution failed' });
|
|
46
|
+
} finally {
|
|
47
|
+
setExecuting(false);
|
|
48
|
+
}
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
const filteredPrompts = prompts.filter((prompt) =>
|
|
52
|
+
prompt.name.toLowerCase().includes(searchQuery.toLowerCase())
|
|
53
|
+
);
|
|
54
|
+
|
|
55
|
+
return (
|
|
56
|
+
<div className="fixed inset-0 flex flex-col bg-background" style={{ left: 'var(--sidebar-width, 15rem)' }}>
|
|
57
|
+
{/* Sticky Header */}
|
|
58
|
+
<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">
|
|
59
|
+
<div className="flex items-center gap-3">
|
|
60
|
+
<div className="w-8 h-8 rounded-lg bg-gradient-to-br from-blue-500 to-cyan-500 flex items-center justify-center shadow-md">
|
|
61
|
+
<FileText className="w-5 h-5 text-white" strokeWidth={2.5} />
|
|
62
|
+
</div>
|
|
63
|
+
<div>
|
|
64
|
+
<h1 className="text-lg font-bold text-foreground">Prompts</h1>
|
|
65
|
+
</div>
|
|
66
|
+
</div>
|
|
67
|
+
<button onClick={loadPrompts} className="btn btn-primary text-sm px-4 py-2 gap-2">
|
|
68
|
+
<RefreshCw className="w-4 h-4" />
|
|
69
|
+
Refresh
|
|
70
|
+
</button>
|
|
71
|
+
</div>
|
|
72
|
+
|
|
73
|
+
{/* Content - ONLY this scrolls */}
|
|
74
|
+
<div className="flex-1 overflow-y-auto overflow-x-hidden">
|
|
75
|
+
<div className="max-w-7xl mx-auto px-6 py-6">
|
|
76
|
+
<input
|
|
77
|
+
type="text"
|
|
78
|
+
placeholder="Search prompts..."
|
|
79
|
+
value={searchQuery}
|
|
80
|
+
onChange={(e) => setSearchQuery(e.target.value)}
|
|
81
|
+
className="input mb-6"
|
|
82
|
+
/>
|
|
83
|
+
|
|
84
|
+
{/* Prompts Grid */}
|
|
85
|
+
{loading.prompts ? (
|
|
86
|
+
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
|
|
87
|
+
{[1, 2, 3].map((i) => (
|
|
88
|
+
<div key={i} className="card skeleton h-40"></div>
|
|
89
|
+
))}
|
|
90
|
+
</div>
|
|
91
|
+
) : filteredPrompts.length === 0 ? (
|
|
92
|
+
<div className="empty-state">
|
|
93
|
+
<AlertCircle className="empty-state-icon" />
|
|
94
|
+
<p className="empty-state-title">
|
|
95
|
+
{searchQuery ? 'No prompts found' : 'No prompts available'}
|
|
96
|
+
</p>
|
|
97
|
+
<p className="empty-state-description">
|
|
98
|
+
{searchQuery ? 'Try a different search term' : 'No prompts have been registered'}
|
|
99
|
+
</p>
|
|
100
|
+
</div>
|
|
101
|
+
) : (
|
|
102
|
+
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
|
|
103
|
+
{filteredPrompts.map((prompt) => (
|
|
104
|
+
<div
|
|
105
|
+
key={prompt.name}
|
|
106
|
+
className="card card-hover p-6 cursor-pointer animate-fade-in"
|
|
107
|
+
onClick={() => {
|
|
108
|
+
setSelectedPrompt(prompt);
|
|
109
|
+
setPromptArgs({});
|
|
110
|
+
setPromptResult(null);
|
|
111
|
+
}}
|
|
112
|
+
>
|
|
113
|
+
<div className="flex items-center gap-3 mb-3">
|
|
114
|
+
<div className="w-10 h-10 rounded-lg bg-blue-500/10 flex items-center justify-center">
|
|
115
|
+
<FileText className="w-5 h-5 text-blue-500" />
|
|
116
|
+
</div>
|
|
117
|
+
<h3 className="font-semibold text-foreground">{prompt.name}</h3>
|
|
118
|
+
</div>
|
|
119
|
+
|
|
120
|
+
<p className="text-sm text-muted-foreground line-clamp-2 mb-3">
|
|
121
|
+
{prompt.description || 'No description'}
|
|
122
|
+
</p>
|
|
123
|
+
|
|
124
|
+
{prompt.arguments && prompt.arguments.length > 0 && (
|
|
125
|
+
<div className="flex items-center gap-1 text-xs text-muted-foreground">
|
|
126
|
+
<span className="badge badge-secondary">
|
|
127
|
+
{prompt.arguments.length} argument{prompt.arguments.length !== 1 ? 's' : ''}
|
|
128
|
+
</span>
|
|
129
|
+
</div>
|
|
130
|
+
)}
|
|
131
|
+
</div>
|
|
132
|
+
))}
|
|
133
|
+
</div>
|
|
134
|
+
)}
|
|
135
|
+
</div>
|
|
136
|
+
</div>
|
|
137
|
+
|
|
138
|
+
{/* Prompt Executor Modal */}
|
|
139
|
+
{selectedPrompt && (
|
|
140
|
+
<div
|
|
141
|
+
className="fixed inset-0 z-50 flex items-center justify-center animate-fade-in"
|
|
142
|
+
style={{ backgroundColor: 'rgba(0, 0, 0, 0.85)' }}
|
|
143
|
+
onClick={() => setSelectedPrompt(null)}
|
|
144
|
+
>
|
|
145
|
+
<div
|
|
146
|
+
className="bg-card rounded-2xl p-6 w-[600px] max-h-[80vh] overflow-auto border border-border shadow-2xl animate-scale-in"
|
|
147
|
+
onClick={(e) => e.stopPropagation()}
|
|
148
|
+
>
|
|
149
|
+
<div className="flex items-center justify-between mb-4">
|
|
150
|
+
<div className="flex items-center gap-3">
|
|
151
|
+
<div className="w-10 h-10 rounded-lg bg-blue-500/10 flex items-center justify-center">
|
|
152
|
+
<FileText className="w-5 h-5 text-blue-500" />
|
|
153
|
+
</div>
|
|
154
|
+
<h2 className="text-xl font-bold text-foreground">{selectedPrompt.name}</h2>
|
|
155
|
+
</div>
|
|
156
|
+
<button
|
|
157
|
+
onClick={() => setSelectedPrompt(null)}
|
|
158
|
+
className="btn btn-ghost w-10 h-10 p-0"
|
|
159
|
+
>
|
|
160
|
+
<X className="w-5 h-5" />
|
|
161
|
+
</button>
|
|
162
|
+
</div>
|
|
163
|
+
|
|
164
|
+
<p className="text-sm text-muted-foreground mb-6">
|
|
165
|
+
{selectedPrompt.description || 'No description'}
|
|
166
|
+
</p>
|
|
167
|
+
|
|
168
|
+
<form onSubmit={handleExecutePrompt}>
|
|
169
|
+
{selectedPrompt.arguments && selectedPrompt.arguments.length > 0 ? (
|
|
170
|
+
selectedPrompt.arguments.map((arg) => (
|
|
171
|
+
<div key={arg.name} className="mb-4">
|
|
172
|
+
<label className="block text-sm font-medium text-foreground mb-2">
|
|
173
|
+
{arg.name}
|
|
174
|
+
{arg.required && <span className="text-destructive ml-1">*</span>}
|
|
175
|
+
</label>
|
|
176
|
+
<input
|
|
177
|
+
type="text"
|
|
178
|
+
className="input"
|
|
179
|
+
value={promptArgs[arg.name] || ''}
|
|
180
|
+
onChange={(e) =>
|
|
181
|
+
setPromptArgs({ ...promptArgs, [arg.name]: e.target.value })
|
|
182
|
+
}
|
|
183
|
+
required={arg.required}
|
|
184
|
+
placeholder={arg.description || `Enter ${arg.name}`}
|
|
185
|
+
/>
|
|
186
|
+
{arg.description && (
|
|
187
|
+
<p className="text-xs text-muted-foreground mt-1">{arg.description}</p>
|
|
188
|
+
)}
|
|
189
|
+
</div>
|
|
190
|
+
))
|
|
191
|
+
) : (
|
|
192
|
+
<div className="bg-muted/30 rounded-lg p-4 mb-4">
|
|
193
|
+
<p className="text-sm text-muted-foreground">No arguments required</p>
|
|
194
|
+
</div>
|
|
195
|
+
)}
|
|
196
|
+
|
|
197
|
+
<button type="submit" className="btn btn-primary w-full gap-2" disabled={executing}>
|
|
198
|
+
<Play className="w-4 h-4" />
|
|
199
|
+
{executing ? 'Executing...' : 'Execute Prompt'}
|
|
200
|
+
</button>
|
|
201
|
+
</form>
|
|
202
|
+
|
|
203
|
+
{promptResult && (
|
|
204
|
+
<div className="mt-6">
|
|
205
|
+
<h3 className="font-semibold text-foreground mb-3">Messages:</h3>
|
|
206
|
+
<div className="space-y-3">
|
|
207
|
+
{promptResult.messages?.map((msg: any, idx: number) => {
|
|
208
|
+
// Extract text from content (can be string or object with type/text)
|
|
209
|
+
const contentText = typeof msg.content === 'string'
|
|
210
|
+
? msg.content
|
|
211
|
+
: msg.content?.text || JSON.stringify(msg.content);
|
|
212
|
+
|
|
213
|
+
return (
|
|
214
|
+
<div key={idx} className="bg-muted/30 border border-border p-4 rounded-lg">
|
|
215
|
+
<div className="text-xs text-muted-foreground mb-2 uppercase font-semibold">
|
|
216
|
+
{msg.role}
|
|
217
|
+
</div>
|
|
218
|
+
<div className="text-sm whitespace-pre-wrap text-foreground">{contentText}</div>
|
|
219
|
+
</div>
|
|
220
|
+
);
|
|
221
|
+
})}
|
|
222
|
+
</div>
|
|
223
|
+
</div>
|
|
224
|
+
)}
|
|
225
|
+
</div>
|
|
226
|
+
</div>
|
|
227
|
+
)}
|
|
228
|
+
</div>
|
|
229
|
+
);
|
|
230
|
+
}
|
|
@@ -0,0 +1,315 @@
|
|
|
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="fixed inset-0 flex flex-col bg-background" style={{ left: 'var(--sidebar-width, 15rem)' }}>
|
|
115
|
+
{/* Sticky Header */}
|
|
116
|
+
<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">
|
|
117
|
+
<div className="flex items-center gap-3">
|
|
118
|
+
<div className="w-8 h-8 rounded-lg bg-gradient-to-br from-purple-500 to-pink-500 flex items-center justify-center shadow-md">
|
|
119
|
+
<Package className="w-5 h-5 text-white" strokeWidth={2.5} />
|
|
120
|
+
</div>
|
|
121
|
+
<div>
|
|
122
|
+
<h1 className="text-lg font-bold text-foreground">Resources</h1>
|
|
123
|
+
</div>
|
|
124
|
+
</div>
|
|
125
|
+
<button onClick={loadResources} className="btn btn-primary text-sm px-4 py-2 gap-2">
|
|
126
|
+
<RefreshCw className="w-4 h-4" />
|
|
127
|
+
Refresh
|
|
128
|
+
</button>
|
|
129
|
+
</div>
|
|
130
|
+
|
|
131
|
+
{/* Content - ONLY this scrolls */}
|
|
132
|
+
<div className="flex-1 overflow-y-auto overflow-x-hidden">
|
|
133
|
+
<div className="max-w-7xl mx-auto px-6 py-6">
|
|
134
|
+
<input
|
|
135
|
+
type="text"
|
|
136
|
+
placeholder="Search resources..."
|
|
137
|
+
value={searchQuery}
|
|
138
|
+
onChange={(e) => setSearchQuery(e.target.value)}
|
|
139
|
+
className="input mb-6"
|
|
140
|
+
/>
|
|
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
|
+
</div>
|
|
313
|
+
</div>
|
|
314
|
+
);
|
|
315
|
+
}
|