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,295 @@
|
|
|
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 Image from 'next/image';
|
|
8
|
+
import {
|
|
9
|
+
Wrench,
|
|
10
|
+
MessageSquare,
|
|
11
|
+
Package,
|
|
12
|
+
FileText,
|
|
13
|
+
Activity,
|
|
14
|
+
Shield,
|
|
15
|
+
Wifi,
|
|
16
|
+
Zap,
|
|
17
|
+
Settings,
|
|
18
|
+
Sparkles,
|
|
19
|
+
Terminal
|
|
20
|
+
} from 'lucide-react';
|
|
21
|
+
|
|
22
|
+
const navItems: Array<{ id: TabType | 'settings' | 'logs'; label: string; icon: any; path: string }> = [
|
|
23
|
+
{ id: 'tools', label: 'Tools', icon: Wrench, path: '/' },
|
|
24
|
+
{ id: 'chat', label: 'AI Chat', icon: MessageSquare, path: '/chat' },
|
|
25
|
+
{ id: 'resources', label: 'Resources', icon: Package, path: '/resources' },
|
|
26
|
+
{ id: 'prompts', label: 'Prompts', icon: FileText, path: '/prompts' },
|
|
27
|
+
{ id: 'health', label: 'Health', icon: Activity, path: '/health' },
|
|
28
|
+
{ id: 'logs', label: 'Logs', icon: Terminal, path: '/logs' },
|
|
29
|
+
{ id: 'auth', label: 'OAuth 2.1', icon: Shield, path: '/auth' },
|
|
30
|
+
{ id: 'ping', label: 'Ping', icon: Wifi, path: '/ping' },
|
|
31
|
+
{ id: 'settings', label: 'Settings', icon: Settings, path: '/settings' },
|
|
32
|
+
];
|
|
33
|
+
|
|
34
|
+
export function Sidebar() {
|
|
35
|
+
const { connection } = useStudioStore();
|
|
36
|
+
const router = useRouter();
|
|
37
|
+
const pathname = usePathname();
|
|
38
|
+
const [mounted, setMounted] = useState(false);
|
|
39
|
+
const [isCollapsed, setIsCollapsed] = useState(false);
|
|
40
|
+
|
|
41
|
+
useEffect(() => {
|
|
42
|
+
setMounted(true);
|
|
43
|
+
// Force dark mode
|
|
44
|
+
document.documentElement.className = 'dark antialiased';
|
|
45
|
+
// Load collapse state from localStorage
|
|
46
|
+
const saved = localStorage.getItem('sidebar_collapsed');
|
|
47
|
+
if (saved !== null) setIsCollapsed(saved === 'true');
|
|
48
|
+
}, []);
|
|
49
|
+
|
|
50
|
+
const handleNavigation = (path: string) => {
|
|
51
|
+
router.push(path);
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
const getConnectionStatus = () => {
|
|
55
|
+
if (connection.status === 'connected') return 'connected';
|
|
56
|
+
if (connection.status === 'connecting') return 'connecting';
|
|
57
|
+
return 'disconnected';
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
const toggleSidebar = () => {
|
|
61
|
+
const newState = !isCollapsed;
|
|
62
|
+
setIsCollapsed(newState);
|
|
63
|
+
localStorage.setItem('sidebar_collapsed', String(newState));
|
|
64
|
+
// Dispatch custom event to update layout
|
|
65
|
+
window.dispatchEvent(new Event('sidebar-toggle'));
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
if (!mounted) return null;
|
|
69
|
+
|
|
70
|
+
return (
|
|
71
|
+
<nav className={`fixed left-0 top-0 h-screen glass flex flex-col z-50 border-r border-border/50 transition-all duration-300 ease-in-out ${
|
|
72
|
+
isCollapsed ? 'w-16' : 'w-60 md:w-60'
|
|
73
|
+
} ${isCollapsed ? '' : 'max-md:w-16'}`}>
|
|
74
|
+
{/* Compact Professional Header */}
|
|
75
|
+
<div className="relative p-3 border-b border-border/50 bg-gradient-to-b from-card/80 to-transparent">
|
|
76
|
+
<div className="flex items-center justify-between mb-2">
|
|
77
|
+
{/* Minimalist Professional Logo */}
|
|
78
|
+
<div
|
|
79
|
+
className="flex items-center gap-2 group cursor-pointer flex-1"
|
|
80
|
+
onClick={() => handleNavigation('/')}
|
|
81
|
+
>
|
|
82
|
+
<div className="relative flex-shrink-0">
|
|
83
|
+
<div className="absolute inset-0 rounded-lg bg-gradient-to-br from-primary to-amber-500 blur opacity-30 group-hover:opacity-50 transition-opacity duration-300" />
|
|
84
|
+
<div className="relative w-9 h-9 rounded-lg bg-gradient-to-br from-slate-900 to-slate-800 border border-primary/30 flex items-center justify-center shadow-lg group-hover:shadow-primary/30 transition-all duration-300 overflow-hidden">
|
|
85
|
+
{/* NitroCloud Logo */}
|
|
86
|
+
<Image
|
|
87
|
+
src="/nitrocloud.png"
|
|
88
|
+
alt="NitroCloud Logo"
|
|
89
|
+
width={36}
|
|
90
|
+
height={36}
|
|
91
|
+
className="relative z-10 object-contain"
|
|
92
|
+
unoptimized
|
|
93
|
+
/>
|
|
94
|
+
{/* Subtle glow effect */}
|
|
95
|
+
<div className="absolute inset-0 bg-gradient-to-tr from-primary/20 to-amber-500/20 group-hover:opacity-100 opacity-0 transition-opacity duration-300" />
|
|
96
|
+
</div>
|
|
97
|
+
</div>
|
|
98
|
+
|
|
99
|
+
{!isCollapsed && (
|
|
100
|
+
<div className="flex-1 overflow-hidden">
|
|
101
|
+
<h1 className="text-base font-bold bg-gradient-to-r from-primary to-amber-500 bg-clip-text text-transparent tracking-tight whitespace-nowrap">
|
|
102
|
+
NitroStudio
|
|
103
|
+
</h1>
|
|
104
|
+
<p className="text-[9px] text-muted-foreground font-medium uppercase tracking-wider">
|
|
105
|
+
MCP Suite
|
|
106
|
+
</p>
|
|
107
|
+
</div>
|
|
108
|
+
)}
|
|
109
|
+
</div>
|
|
110
|
+
|
|
111
|
+
{/* Toggle Button */}
|
|
112
|
+
<button
|
|
113
|
+
onClick={toggleSidebar}
|
|
114
|
+
className="flex-shrink-0 w-7 h-7 rounded-md bg-muted/50 hover:bg-muted border border-border/50 hover:border-primary/30 flex items-center justify-center transition-all duration-300 group/toggle"
|
|
115
|
+
title={isCollapsed ? 'Expand sidebar' : 'Collapse sidebar'}
|
|
116
|
+
>
|
|
117
|
+
<svg
|
|
118
|
+
width="14"
|
|
119
|
+
height="14"
|
|
120
|
+
viewBox="0 0 24 24"
|
|
121
|
+
fill="none"
|
|
122
|
+
className={`text-muted-foreground group-hover/toggle:text-primary transition-all duration-300 ${
|
|
123
|
+
isCollapsed ? 'rotate-180' : ''
|
|
124
|
+
}`}
|
|
125
|
+
>
|
|
126
|
+
<path d="M15 18L9 12L15 6" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"/>
|
|
127
|
+
</svg>
|
|
128
|
+
</button>
|
|
129
|
+
</div>
|
|
130
|
+
|
|
131
|
+
{/* Compact Status Indicators */}
|
|
132
|
+
{!isCollapsed ? (
|
|
133
|
+
<div className="space-y-1.5">
|
|
134
|
+
{/* Connection Status */}
|
|
135
|
+
<div className="relative group">
|
|
136
|
+
<div className="relative flex items-center gap-2 px-2 py-1.5 rounded-md bg-card/60 border border-border/50 backdrop-blur-sm group-hover:border-primary/30 transition-all duration-300">
|
|
137
|
+
<div className="relative flex items-center justify-center flex-shrink-0">
|
|
138
|
+
{getConnectionStatus() === 'connected' && (
|
|
139
|
+
<div className="absolute inset-0 rounded-full bg-emerald-500/30 animate-ping" />
|
|
140
|
+
)}
|
|
141
|
+
<div
|
|
142
|
+
className={`relative w-2 h-2 rounded-full transition-all duration-300 ${
|
|
143
|
+
getConnectionStatus() === 'connected'
|
|
144
|
+
? 'bg-emerald-500'
|
|
145
|
+
: getConnectionStatus() === 'connecting'
|
|
146
|
+
? 'bg-amber-500 animate-pulse'
|
|
147
|
+
: 'bg-rose-500'
|
|
148
|
+
}`}
|
|
149
|
+
style={{
|
|
150
|
+
boxShadow: getConnectionStatus() === 'connected'
|
|
151
|
+
? '0 0 8px rgba(34, 197, 94, 0.6)'
|
|
152
|
+
: getConnectionStatus() === 'connecting'
|
|
153
|
+
? '0 0 8px rgba(245, 158, 11, 0.6)'
|
|
154
|
+
: '0 0 8px rgba(239, 68, 68, 0.6)'
|
|
155
|
+
}}
|
|
156
|
+
/>
|
|
157
|
+
</div>
|
|
158
|
+
<span className="text-[10px] font-semibold uppercase tracking-wide text-foreground">
|
|
159
|
+
{connection.status}
|
|
160
|
+
</span>
|
|
161
|
+
</div>
|
|
162
|
+
</div>
|
|
163
|
+
|
|
164
|
+
{/* Transport Mode */}
|
|
165
|
+
<div className="relative group">
|
|
166
|
+
<div className="relative px-2 py-1.5 rounded-md bg-primary/5 border border-primary/20 backdrop-blur-sm group-hover:border-primary/30 transition-all duration-300">
|
|
167
|
+
<div className="flex items-center justify-between text-[10px]">
|
|
168
|
+
<span className="font-medium text-muted-foreground">Transport:</span>
|
|
169
|
+
<span className="font-bold text-primary uppercase">
|
|
170
|
+
STDIO
|
|
171
|
+
</span>
|
|
172
|
+
</div>
|
|
173
|
+
</div>
|
|
174
|
+
</div>
|
|
175
|
+
</div>
|
|
176
|
+
) : (
|
|
177
|
+
<div className="flex flex-col items-center gap-1.5">
|
|
178
|
+
{/* Collapsed: Just the status dot */}
|
|
179
|
+
<div
|
|
180
|
+
className="relative w-7 h-7 rounded-md bg-card/60 border border-border/50 flex items-center justify-center group hover:border-primary/30 transition-all"
|
|
181
|
+
title={`Status: ${connection.status}`}
|
|
182
|
+
>
|
|
183
|
+
{getConnectionStatus() === 'connected' && (
|
|
184
|
+
<div className="absolute inset-0 rounded-md bg-emerald-500/20 animate-ping" />
|
|
185
|
+
)}
|
|
186
|
+
<div
|
|
187
|
+
className={`relative w-2 h-2 rounded-full ${
|
|
188
|
+
getConnectionStatus() === 'connected'
|
|
189
|
+
? 'bg-emerald-500'
|
|
190
|
+
: getConnectionStatus() === 'connecting'
|
|
191
|
+
? 'bg-amber-500 animate-pulse'
|
|
192
|
+
: 'bg-rose-500'
|
|
193
|
+
}`}
|
|
194
|
+
style={{
|
|
195
|
+
boxShadow: getConnectionStatus() === 'connected'
|
|
196
|
+
? '0 0 8px rgba(34, 197, 94, 0.8)'
|
|
197
|
+
: getConnectionStatus() === 'connecting'
|
|
198
|
+
? '0 0 8px rgba(245, 158, 11, 0.8)'
|
|
199
|
+
: '0 0 8px rgba(239, 68, 68, 0.8)'
|
|
200
|
+
}}
|
|
201
|
+
/>
|
|
202
|
+
</div>
|
|
203
|
+
</div>
|
|
204
|
+
)}
|
|
205
|
+
</div>
|
|
206
|
+
|
|
207
|
+
{/* Compact Navigation */}
|
|
208
|
+
<div className="flex-1 overflow-y-auto py-2 px-2 scrollbar-thin scrollbar-thumb-border scrollbar-track-transparent">
|
|
209
|
+
<div className={`space-y-0.5 ${isCollapsed ? 'flex flex-col items-center' : ''}`}>
|
|
210
|
+
{navItems.map((item) => {
|
|
211
|
+
const Icon = item.icon;
|
|
212
|
+
const isActive = pathname === item.path;
|
|
213
|
+
|
|
214
|
+
return (
|
|
215
|
+
<button
|
|
216
|
+
key={item.id}
|
|
217
|
+
onClick={() => handleNavigation(item.path)}
|
|
218
|
+
title={isCollapsed ? item.label : undefined}
|
|
219
|
+
className={`
|
|
220
|
+
relative flex items-center gap-2 text-sm font-medium rounded-lg transition-all duration-300 group overflow-hidden
|
|
221
|
+
${isCollapsed ? 'w-10 h-10 justify-center' : 'w-full px-3 py-2'}
|
|
222
|
+
${isActive
|
|
223
|
+
? 'bg-gradient-to-r from-primary/15 to-amber-500/10 text-primary shadow-md ring-1 ring-primary/30'
|
|
224
|
+
: 'text-foreground/70 hover:bg-primary/5 hover:text-primary'
|
|
225
|
+
}
|
|
226
|
+
`}
|
|
227
|
+
>
|
|
228
|
+
{/* Active indicator */}
|
|
229
|
+
{isActive && !isCollapsed && (
|
|
230
|
+
<div className="absolute left-0 top-1/2 -translate-y-1/2 w-0.5 h-6 bg-gradient-to-b from-primary to-amber-500 rounded-r-full" />
|
|
231
|
+
)}
|
|
232
|
+
|
|
233
|
+
{/* Icon */}
|
|
234
|
+
<div className={`relative flex-shrink-0 ${isActive ? 'scale-105' : 'group-hover:scale-105'} transition-transform duration-300`}>
|
|
235
|
+
<Icon
|
|
236
|
+
className={`w-4 h-4 ${
|
|
237
|
+
isActive
|
|
238
|
+
? 'text-primary'
|
|
239
|
+
: 'text-muted-foreground group-hover:text-primary'
|
|
240
|
+
}`}
|
|
241
|
+
strokeWidth={isActive ? 2.5 : 2}
|
|
242
|
+
/>
|
|
243
|
+
</div>
|
|
244
|
+
|
|
245
|
+
{/* Label - only show when expanded */}
|
|
246
|
+
{!isCollapsed && (
|
|
247
|
+
<span className="text-xs whitespace-nowrap overflow-hidden">
|
|
248
|
+
{item.label}
|
|
249
|
+
</span>
|
|
250
|
+
)}
|
|
251
|
+
|
|
252
|
+
{/* Subtle shine effect */}
|
|
253
|
+
<div className="absolute inset-0 bg-gradient-to-r from-transparent via-white/5 to-transparent -translate-x-full group-hover:translate-x-full transition-transform duration-700 ease-out pointer-events-none" />
|
|
254
|
+
</button>
|
|
255
|
+
);
|
|
256
|
+
})}
|
|
257
|
+
</div>
|
|
258
|
+
</div>
|
|
259
|
+
|
|
260
|
+
{/* Compact Footer */}
|
|
261
|
+
<div className="p-2 border-t border-border/50 bg-gradient-to-t from-card/60 to-transparent backdrop-blur-sm">
|
|
262
|
+
{!isCollapsed ? (
|
|
263
|
+
<div className="space-y-1.5">
|
|
264
|
+
{/* Version info */}
|
|
265
|
+
<div className="px-2 py-1.5 rounded-md bg-muted/30 border border-border/30">
|
|
266
|
+
<div className="flex items-center justify-between text-[9px]">
|
|
267
|
+
<span className="font-medium text-muted-foreground uppercase tracking-wide">MCP v1.0</span>
|
|
268
|
+
<span className="font-bold text-foreground">NitroStack</span>
|
|
269
|
+
</div>
|
|
270
|
+
</div>
|
|
271
|
+
|
|
272
|
+
{/* Copyright */}
|
|
273
|
+
<div className="text-center">
|
|
274
|
+
<p className="text-[8px] text-muted-foreground/50 font-medium">
|
|
275
|
+
© 2025 NitroCloud
|
|
276
|
+
</p>
|
|
277
|
+
</div>
|
|
278
|
+
</div>
|
|
279
|
+
) : (
|
|
280
|
+
<div className="flex flex-col items-center">
|
|
281
|
+
{/* Collapsed: Minimal version indicator */}
|
|
282
|
+
<div
|
|
283
|
+
className="w-10 h-10 rounded-md bg-muted/30 border border-border/30 flex items-center justify-center group hover:border-primary/30 transition-all"
|
|
284
|
+
title="MCP v1.0 • NitroStack"
|
|
285
|
+
>
|
|
286
|
+
<span className="text-[9px] font-bold text-primary group-hover:scale-110 transition-transform">
|
|
287
|
+
v1
|
|
288
|
+
</span>
|
|
289
|
+
</div>
|
|
290
|
+
</div>
|
|
291
|
+
)}
|
|
292
|
+
</div>
|
|
293
|
+
</nav>
|
|
294
|
+
);
|
|
295
|
+
}
|
|
@@ -0,0 +1,139 @@
|
|
|
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, MessageSquare } 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 && widgetUri.trim().length > 0;
|
|
25
|
+
|
|
26
|
+
// Get example data for preview - check both examples and _meta
|
|
27
|
+
const exampleData = tool.examples?.response || tool._meta?.['tool/examples']?.response;
|
|
28
|
+
|
|
29
|
+
// Debug logging for widget detection
|
|
30
|
+
if (hasWidget) {
|
|
31
|
+
console.log('ToolCard - Widget detected:', {
|
|
32
|
+
toolName: tool.name,
|
|
33
|
+
widgetUri,
|
|
34
|
+
hasExampleData: !!exampleData,
|
|
35
|
+
exampleDataType: exampleData ? typeof exampleData : 'none',
|
|
36
|
+
toolExamples: tool.examples,
|
|
37
|
+
toolMeta: tool._meta,
|
|
38
|
+
metaExamples: tool._meta?.['tool/examples'],
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const handleUseInChat = (e: React.MouseEvent) => {
|
|
43
|
+
e.stopPropagation();
|
|
44
|
+
|
|
45
|
+
// Build the tool execution message
|
|
46
|
+
const toolMessage = `Use the ${tool.name} tool`;
|
|
47
|
+
|
|
48
|
+
// Store the message in localStorage
|
|
49
|
+
if (typeof window !== 'undefined') {
|
|
50
|
+
window.localStorage.setItem('chatInput', toolMessage);
|
|
51
|
+
window.localStorage.setItem('suggestedTool', tool.name);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
router.push('/chat');
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
const handleEnlarge = (e: React.MouseEvent) => {
|
|
58
|
+
e.stopPropagation();
|
|
59
|
+
openEnlargeModal('tool', { ...tool, responseData: exampleData });
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
return (
|
|
63
|
+
<div
|
|
64
|
+
className="card card-hover p-6 animate-fade-in cursor-pointer"
|
|
65
|
+
onClick={() => onExecute(tool)}
|
|
66
|
+
>
|
|
67
|
+
{/* Header */}
|
|
68
|
+
<div className="flex items-start justify-between mb-4">
|
|
69
|
+
<div className="flex items-center gap-3">
|
|
70
|
+
<div className={`w-12 h-12 rounded-lg flex items-center justify-center ${hasWidget ? 'bg-purple-500/10' : 'bg-primary/10'}`}>
|
|
71
|
+
{hasWidget ? (
|
|
72
|
+
<Palette className="w-6 h-6 text-purple-500" />
|
|
73
|
+
) : (
|
|
74
|
+
<Zap className="w-6 h-6 text-primary" />
|
|
75
|
+
)}
|
|
76
|
+
</div>
|
|
77
|
+
<div>
|
|
78
|
+
<h3 className="font-semibold text-lg text-foreground">
|
|
79
|
+
{tool.name}
|
|
80
|
+
</h3>
|
|
81
|
+
<span className={`badge ${hasWidget ? 'badge-secondary' : 'badge-primary'} text-xs mt-1`}>
|
|
82
|
+
{hasWidget ? 'tool + widget' : 'tool'}
|
|
83
|
+
</span>
|
|
84
|
+
</div>
|
|
85
|
+
</div>
|
|
86
|
+
</div>
|
|
87
|
+
|
|
88
|
+
{/* Description */}
|
|
89
|
+
<p className="text-sm text-muted-foreground mb-4 line-clamp-2">
|
|
90
|
+
{tool.description || 'No description'}
|
|
91
|
+
</p>
|
|
92
|
+
|
|
93
|
+
{/* Widget Preview - Show if widget exists AND has example data */}
|
|
94
|
+
{hasWidget && widgetUri && exampleData && (
|
|
95
|
+
<div className="relative mb-4 rounded-lg overflow-hidden border border-border bg-muted/20">
|
|
96
|
+
<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">
|
|
97
|
+
<Sparkles className="w-3 h-3" />
|
|
98
|
+
Widget Preview
|
|
99
|
+
</div>
|
|
100
|
+
<div className="h-64 relative">
|
|
101
|
+
<WidgetRenderer
|
|
102
|
+
uri={widgetUri}
|
|
103
|
+
data={exampleData}
|
|
104
|
+
className="w-full h-full"
|
|
105
|
+
/>
|
|
106
|
+
</div>
|
|
107
|
+
</div>
|
|
108
|
+
)}
|
|
109
|
+
|
|
110
|
+
{/* Action Buttons */}
|
|
111
|
+
<div className="flex flex-wrap items-center gap-2" onClick={(e) => e.stopPropagation()}>
|
|
112
|
+
{hasWidget && (
|
|
113
|
+
<button
|
|
114
|
+
onClick={handleEnlarge}
|
|
115
|
+
className="btn btn-secondary flex-1 min-w-[90px] text-xs sm:text-sm gap-1.5 px-2.5 py-1.5 sm:px-4 sm:py-2"
|
|
116
|
+
>
|
|
117
|
+
<Maximize2 className="w-3.5 h-3.5 sm:w-4 sm:h-4 flex-shrink-0" />
|
|
118
|
+
<span className="truncate">Enlarge</span>
|
|
119
|
+
</button>
|
|
120
|
+
)}
|
|
121
|
+
<button
|
|
122
|
+
onClick={() => onExecute(tool)}
|
|
123
|
+
className="btn btn-primary flex-1 min-w-[90px] text-xs sm:text-sm gap-1.5 px-2.5 py-1.5 sm:px-4 sm:py-2"
|
|
124
|
+
>
|
|
125
|
+
<Play className="w-3.5 h-3.5 sm:w-4 sm:h-4 flex-shrink-0" />
|
|
126
|
+
<span className="truncate">Execute</span>
|
|
127
|
+
</button>
|
|
128
|
+
<button
|
|
129
|
+
onClick={handleUseInChat}
|
|
130
|
+
className="btn btn-secondary flex-1 min-w-[90px] text-xs sm:text-sm gap-1.5 px-2.5 py-1.5 sm:px-4 sm:py-2"
|
|
131
|
+
title="Use in Chat"
|
|
132
|
+
>
|
|
133
|
+
<MessageSquare className="w-3.5 h-3.5 sm:w-4 sm:h-4 flex-shrink-0" />
|
|
134
|
+
<span className="truncate">Chat</span>
|
|
135
|
+
</button>
|
|
136
|
+
</div>
|
|
137
|
+
</div>
|
|
138
|
+
);
|
|
139
|
+
}
|