nitrostack 1.0.71 → 1.0.72
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/src/studio/app/api/chat/route.ts +3 -1
- package/src/studio/app/auth/callback/page.tsx +6 -6
- package/src/studio/app/chat/page.tsx +1099 -408
- package/src/studio/app/chat/page.tsx.backup +1046 -187
- package/src/studio/app/globals.css +361 -191
- package/src/studio/app/health/page.tsx +72 -76
- package/src/studio/app/layout.tsx +9 -11
- package/src/studio/app/logs/page.tsx +29 -30
- package/src/studio/app/page.tsx +134 -230
- package/src/studio/app/prompts/page.tsx +115 -97
- package/src/studio/app/resources/page.tsx +115 -124
- package/src/studio/app/settings/page.tsx +1080 -125
- package/src/studio/app/tools/page.tsx +343 -0
- package/src/studio/components/EnlargeModal.tsx +76 -65
- package/src/studio/components/LogMessage.tsx +5 -5
- package/src/studio/components/MarkdownRenderer.tsx +4 -4
- package/src/studio/components/Sidebar.tsx +150 -210
- package/src/studio/components/SplashScreen.tsx +109 -0
- package/src/studio/components/ToolCard.tsx +50 -41
- package/src/studio/components/VoiceOrbOverlay.tsx +469 -0
- package/src/studio/components/WidgetRenderer.tsx +8 -3
- package/src/studio/components/tools/ToolsCanvas.tsx +327 -0
- package/src/studio/lib/store.ts +15 -0
- package/src/studio/package-lock.json +3303 -0
- package/src/studio/package.json +3 -1
- package/src/studio/public/NitroStudio Isotype Color.png +0 -0
- package/src/studio/tailwind.config.ts +63 -17
- package/src/studio/app/auth/page.tsx +0 -560
- package/src/studio/app/ping/page.tsx +0 -209
|
@@ -0,0 +1,343 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { useEffect, useState } from 'react';
|
|
4
|
+
import { useStudioStore } from '@/lib/store';
|
|
5
|
+
import { api } from '@/lib/api';
|
|
6
|
+
import { ToolCard } from '@/components/ToolCard';
|
|
7
|
+
import { WidgetRenderer } from '@/components/WidgetRenderer';
|
|
8
|
+
import type { Tool } from '@/lib/types';
|
|
9
|
+
import { WrenchScrewdriverIcon, ArrowPathIcon, XMarkIcon, PlayIcon, ExclamationCircleIcon } from '@heroicons/react/24/outline';
|
|
10
|
+
|
|
11
|
+
export default function DetailedToolsPage() {
|
|
12
|
+
const { tools, setTools, loading, setLoading, connection, setConnection, jwtToken, apiKey, oauthState } = useStudioStore();
|
|
13
|
+
const [searchQuery, setSearchQuery] = useState('');
|
|
14
|
+
const [selectedTool, setSelectedTool] = useState<Tool | null>(null);
|
|
15
|
+
const [toolArgs, setToolArgs] = useState<Record<string, any>>({});
|
|
16
|
+
const [toolResult, setToolResult] = useState<any>(null);
|
|
17
|
+
const [executingTool, setExecutingTool] = useState(false);
|
|
18
|
+
|
|
19
|
+
// Get effective token - check both jwtToken and OAuth token
|
|
20
|
+
const effectiveToken = jwtToken || oauthState?.currentToken;
|
|
21
|
+
|
|
22
|
+
// Initialize MCP, load tools and check connection on mount
|
|
23
|
+
useEffect(() => {
|
|
24
|
+
const init = async () => {
|
|
25
|
+
// Connection is handled by Sidebar globally
|
|
26
|
+
await loadTools();
|
|
27
|
+
};
|
|
28
|
+
init();
|
|
29
|
+
}, []);
|
|
30
|
+
|
|
31
|
+
const loadTools = async () => {
|
|
32
|
+
setLoading('tools', true);
|
|
33
|
+
try {
|
|
34
|
+
const data = await api.getTools();
|
|
35
|
+
setTools(data.tools || []);
|
|
36
|
+
} catch (error) {
|
|
37
|
+
console.error('Failed to load tools:', error);
|
|
38
|
+
} finally {
|
|
39
|
+
setLoading('tools', false);
|
|
40
|
+
}
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
const handleExecuteTool = (tool: Tool) => {
|
|
44
|
+
setSelectedTool(tool);
|
|
45
|
+
setToolArgs({});
|
|
46
|
+
setToolResult(null);
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
const handleSubmitTool = async (e: React.FormEvent) => {
|
|
50
|
+
e.preventDefault();
|
|
51
|
+
if (!selectedTool) return;
|
|
52
|
+
|
|
53
|
+
setExecutingTool(true);
|
|
54
|
+
setToolResult(null);
|
|
55
|
+
|
|
56
|
+
try {
|
|
57
|
+
const result = await api.callTool(selectedTool.name, toolArgs, effectiveToken || undefined, apiKey || undefined);
|
|
58
|
+
setToolResult(result);
|
|
59
|
+
|
|
60
|
+
if (result.content) {
|
|
61
|
+
try {
|
|
62
|
+
const content = result.content[0]?.text;
|
|
63
|
+
if (content) {
|
|
64
|
+
const parsed = JSON.parse(content);
|
|
65
|
+
const token = parsed.token || parsed.access_token || parsed.jwt || parsed.data?.token;
|
|
66
|
+
if (token) {
|
|
67
|
+
console.log('🔐 Token received from tool, saving to global state');
|
|
68
|
+
useStudioStore.getState().setJwtToken(token);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
} catch (e) {
|
|
72
|
+
// Ignore parsing errors
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
} catch (error) {
|
|
76
|
+
console.error('Tool execution failed:', error);
|
|
77
|
+
setToolResult({ error: 'Tool execution failed' });
|
|
78
|
+
} finally {
|
|
79
|
+
setExecutingTool(false);
|
|
80
|
+
}
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
const filteredTools = tools.filter((tool) =>
|
|
84
|
+
tool.name.toLowerCase().includes(searchQuery.toLowerCase())
|
|
85
|
+
);
|
|
86
|
+
|
|
87
|
+
return (
|
|
88
|
+
<>
|
|
89
|
+
<div className="fixed inset-0 flex flex-col bg-background" style={{ left: 'var(--sidebar-width, 15rem)' }}>
|
|
90
|
+
{/* Minimal Professional Header */}
|
|
91
|
+
<div className="sticky top-0 z-10 border-b border-border/50 px-6 py-4 flex items-center justify-between bg-card/50 backdrop-blur-sm">
|
|
92
|
+
<div className="flex items-center gap-6">
|
|
93
|
+
<h1 className="text-lg font-semibold text-foreground">Tools Browser</h1>
|
|
94
|
+
</div>
|
|
95
|
+
|
|
96
|
+
<button onClick={loadTools} className="btn btn-primary text-sm px-4 py-2 gap-2">
|
|
97
|
+
<ArrowPathIcon className="h-4 w-4" />
|
|
98
|
+
Refresh
|
|
99
|
+
</button>
|
|
100
|
+
</div>
|
|
101
|
+
|
|
102
|
+
{/* Content - ONLY this scrolls */}
|
|
103
|
+
<div className="flex-1 overflow-y-auto overflow-x-hidden relative">
|
|
104
|
+
<div className="max-w-7xl mx-auto px-6 py-6">
|
|
105
|
+
{/* Search */}
|
|
106
|
+
<div className="mb-6">
|
|
107
|
+
<input
|
|
108
|
+
type="text"
|
|
109
|
+
placeholder="Search tools..."
|
|
110
|
+
value={searchQuery}
|
|
111
|
+
onChange={(e) => setSearchQuery(e.target.value)}
|
|
112
|
+
className="input w-full"
|
|
113
|
+
/>
|
|
114
|
+
</div>
|
|
115
|
+
|
|
116
|
+
{/* Tools Grid */}
|
|
117
|
+
{loading.tools ? (
|
|
118
|
+
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
|
|
119
|
+
{[1, 2, 3].map((i) => (
|
|
120
|
+
<div key={i} className="card skeleton h-64"></div>
|
|
121
|
+
))}
|
|
122
|
+
</div>
|
|
123
|
+
) : filteredTools.length === 0 ? (
|
|
124
|
+
<div className="empty-state">
|
|
125
|
+
<ExclamationCircleIcon className="empty-state-icon" />
|
|
126
|
+
<p className="empty-state-title">
|
|
127
|
+
{searchQuery ? 'No tools found matching your search' : 'No tools available'}
|
|
128
|
+
</p>
|
|
129
|
+
<p className="empty-state-description">
|
|
130
|
+
{searchQuery ? 'Try a different search term' : 'No MCP tools have been registered'}
|
|
131
|
+
</p>
|
|
132
|
+
</div>
|
|
133
|
+
) : (
|
|
134
|
+
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
|
|
135
|
+
{filteredTools.map((tool) => (
|
|
136
|
+
<ToolCard key={tool.name} tool={tool} onExecute={handleExecuteTool} />
|
|
137
|
+
))}
|
|
138
|
+
</div>
|
|
139
|
+
)}
|
|
140
|
+
</div>
|
|
141
|
+
</div>
|
|
142
|
+
</div>
|
|
143
|
+
|
|
144
|
+
{/* Tool Executor Side Drawer */}
|
|
145
|
+
{selectedTool && (
|
|
146
|
+
<div
|
|
147
|
+
className="fixed inset-0 z-50 flex justify-end bg-background/80 backdrop-blur-sm animate-fade-in"
|
|
148
|
+
onClick={() => setSelectedTool(null)}
|
|
149
|
+
>
|
|
150
|
+
<div
|
|
151
|
+
className="relative w-full max-w-2xl h-full bg-card border-l border-border shadow-2xl overflow-hidden animate-slide-in-right flex flex-col"
|
|
152
|
+
onClick={(e) => e.stopPropagation()}
|
|
153
|
+
>
|
|
154
|
+
{/* Header */}
|
|
155
|
+
<div className="flex items-center justify-between px-6 py-4 border-b border-border bg-card">
|
|
156
|
+
<div className="flex items-center gap-3">
|
|
157
|
+
<div className="w-10 h-10 rounded-lg bg-primary/10 flex items-center justify-center">
|
|
158
|
+
<WrenchScrewdriverIcon className="w-5 h-5 text-primary" />
|
|
159
|
+
</div>
|
|
160
|
+
<div>
|
|
161
|
+
<h2 className="text-lg font-bold text-foreground">{selectedTool.name}</h2>
|
|
162
|
+
<p className="text-xs text-muted-foreground">Tool Executor</p>
|
|
163
|
+
</div>
|
|
164
|
+
</div>
|
|
165
|
+
<button
|
|
166
|
+
onClick={() => setSelectedTool(null)}
|
|
167
|
+
className="btn btn-ghost w-8 h-8 p-0 flex items-center justify-center rounded-full hover:bg-muted"
|
|
168
|
+
aria-label="Close"
|
|
169
|
+
>
|
|
170
|
+
<XMarkIcon className="w-5 h-5" />
|
|
171
|
+
</button>
|
|
172
|
+
</div>
|
|
173
|
+
|
|
174
|
+
{/* Scrollable Content */}
|
|
175
|
+
<div className="flex-1 overflow-y-auto bg-muted/5 p-6">
|
|
176
|
+
|
|
177
|
+
<div className="bg-card p-4 rounded-xl border border-border mb-6">
|
|
178
|
+
<p className="text-sm text-foreground leading-relaxed">
|
|
179
|
+
{selectedTool.description || 'No description available'}
|
|
180
|
+
</p>
|
|
181
|
+
</div>
|
|
182
|
+
|
|
183
|
+
<form onSubmit={handleSubmitTool} className="space-y-6">
|
|
184
|
+
{/* Generate form inputs from schema */}
|
|
185
|
+
{selectedTool.inputSchema?.properties && typeof selectedTool.inputSchema.properties === 'object' && Object.keys(selectedTool.inputSchema.properties).length > 0 ? (
|
|
186
|
+
<div className="space-y-4">
|
|
187
|
+
{Object.entries(selectedTool.inputSchema.properties).map(([key, prop]: [string, any]) => {
|
|
188
|
+
const isRequired = selectedTool.inputSchema?.required?.includes(key);
|
|
189
|
+
|
|
190
|
+
// Handle different input types
|
|
191
|
+
if (prop.enum) {
|
|
192
|
+
return (
|
|
193
|
+
<div key={key}>
|
|
194
|
+
<label className="block text-sm font-medium text-foreground mb-2">
|
|
195
|
+
{prop.title || key}
|
|
196
|
+
{isRequired && <span className="text-destructive ml-1">*</span>}
|
|
197
|
+
</label>
|
|
198
|
+
<select
|
|
199
|
+
className="input w-full"
|
|
200
|
+
value={toolArgs[key] || prop.default || ''}
|
|
201
|
+
onChange={(e) => setToolArgs({ ...toolArgs, [key]: e.target.value })}
|
|
202
|
+
required={isRequired}
|
|
203
|
+
>
|
|
204
|
+
<option value="">Select...</option>
|
|
205
|
+
{prop.enum.map((val: any) => (
|
|
206
|
+
<option key={val} value={val}>
|
|
207
|
+
{val}
|
|
208
|
+
</option>
|
|
209
|
+
))}
|
|
210
|
+
</select>
|
|
211
|
+
{prop.description && (
|
|
212
|
+
<p className="text-xs text-muted-foreground mt-1">{prop.description}</p>
|
|
213
|
+
)}
|
|
214
|
+
</div>
|
|
215
|
+
);
|
|
216
|
+
} else if (prop.type === 'boolean') {
|
|
217
|
+
return (
|
|
218
|
+
<div key={key}>
|
|
219
|
+
<label className="flex items-center gap-2 cursor-pointer p-3 bg-muted/30 rounded-lg border border-border hover:bg-muted/50 transition-colors">
|
|
220
|
+
<input
|
|
221
|
+
type="checkbox"
|
|
222
|
+
className="checkbox checkbox-primary w-4 h-4"
|
|
223
|
+
checked={toolArgs[key] || false}
|
|
224
|
+
onChange={(e) => setToolArgs({ ...toolArgs, [key]: e.target.checked })}
|
|
225
|
+
/>
|
|
226
|
+
<span className="text-sm font-medium text-foreground">
|
|
227
|
+
{prop.title || key}
|
|
228
|
+
{isRequired && <span className="text-destructive ml-1">*</span>}
|
|
229
|
+
</span>
|
|
230
|
+
</label>
|
|
231
|
+
{prop.description && (
|
|
232
|
+
<p className="text-xs text-muted-foreground mt-1 ml-1">{prop.description}</p>
|
|
233
|
+
)}
|
|
234
|
+
</div>
|
|
235
|
+
);
|
|
236
|
+
} else {
|
|
237
|
+
return (
|
|
238
|
+
<div key={key}>
|
|
239
|
+
<label className="block text-sm font-medium text-foreground mb-2">
|
|
240
|
+
{prop.title || key}
|
|
241
|
+
{isRequired && <span className="text-destructive ml-1">*</span>}
|
|
242
|
+
</label>
|
|
243
|
+
<input
|
|
244
|
+
type={prop.type === 'number' || prop.type === 'integer' ? 'number' : 'text'}
|
|
245
|
+
className="input w-full"
|
|
246
|
+
value={toolArgs[key] || prop.default || ''}
|
|
247
|
+
onChange={(e) => {
|
|
248
|
+
const value = prop.type === 'number' || prop.type === 'integer'
|
|
249
|
+
? (e.target.value ? Number(e.target.value) : '')
|
|
250
|
+
: e.target.value;
|
|
251
|
+
setToolArgs({ ...toolArgs, [key]: value });
|
|
252
|
+
}}
|
|
253
|
+
required={isRequired}
|
|
254
|
+
placeholder={prop.description}
|
|
255
|
+
min={prop.minimum}
|
|
256
|
+
max={prop.maximum}
|
|
257
|
+
/>
|
|
258
|
+
{prop.description && (
|
|
259
|
+
<p className="text-xs text-muted-foreground mt-1">{prop.description}</p>
|
|
260
|
+
)}
|
|
261
|
+
</div>
|
|
262
|
+
);
|
|
263
|
+
}
|
|
264
|
+
})}
|
|
265
|
+
</div>
|
|
266
|
+
) : (
|
|
267
|
+
<div className="p-4 bg-muted/30 rounded-lg border border-border border-dashed text-center">
|
|
268
|
+
<p className="text-sm text-muted-foreground">No arguments required for this tool</p>
|
|
269
|
+
</div>
|
|
270
|
+
)}
|
|
271
|
+
|
|
272
|
+
<div className="sticky bottom-0 -mx-6 -mb-6 p-6 bg-card border-t border-border mt-auto">
|
|
273
|
+
<button
|
|
274
|
+
type="submit"
|
|
275
|
+
className="btn btn-primary w-full gap-2 py-3"
|
|
276
|
+
disabled={executingTool}
|
|
277
|
+
>
|
|
278
|
+
<PlayIcon className="w-4 h-4" />
|
|
279
|
+
{executingTool ? 'Executing...' : 'Execute Tool'}
|
|
280
|
+
</button>
|
|
281
|
+
</div>
|
|
282
|
+
</form>
|
|
283
|
+
|
|
284
|
+
{/* Result */}
|
|
285
|
+
{toolResult && (
|
|
286
|
+
<div className="mt-8 pt-8 border-t border-border animate-fade-in">
|
|
287
|
+
<div className="space-y-6">
|
|
288
|
+
<div>
|
|
289
|
+
<div className="flex items-center justify-between mb-3">
|
|
290
|
+
<h3 className="font-semibold text-foreground">Result Output</h3>
|
|
291
|
+
<div className="badge badge-success text-xs">Success</div>
|
|
292
|
+
</div>
|
|
293
|
+
<div className="bg-muted/30 rounded-xl border border-border overflow-hidden">
|
|
294
|
+
<pre className="p-4 text-xs font-mono text-foreground overflow-auto max-h-96">
|
|
295
|
+
{JSON.stringify(toolResult, null, 2)}
|
|
296
|
+
</pre>
|
|
297
|
+
</div>
|
|
298
|
+
</div>
|
|
299
|
+
|
|
300
|
+
{/* Widget UI Rendering */}
|
|
301
|
+
{(() => {
|
|
302
|
+
const widgetUri =
|
|
303
|
+
selectedTool.widget?.route ||
|
|
304
|
+
selectedTool.outputTemplate ||
|
|
305
|
+
selectedTool._meta?.['ui/template'] ||
|
|
306
|
+
selectedTool._meta?.['openai/outputTemplate'];
|
|
307
|
+
|
|
308
|
+
return widgetUri && toolResult ? (
|
|
309
|
+
<div>
|
|
310
|
+
<h3 className="font-semibold text-foreground mb-3 text-sm uppercase tracking-wider">Preview Component</h3>
|
|
311
|
+
<div className="border border-border rounded-xl overflow-hidden bg-background shadow-sm transition-all duration-200">
|
|
312
|
+
<WidgetRenderer
|
|
313
|
+
uri={widgetUri}
|
|
314
|
+
data={(() => {
|
|
315
|
+
if (toolResult.content?.[0]?.text) {
|
|
316
|
+
try {
|
|
317
|
+
const parsed = JSON.parse(toolResult.content[0].text);
|
|
318
|
+
if (parsed.success !== undefined && parsed.data !== undefined) {
|
|
319
|
+
return parsed.data;
|
|
320
|
+
}
|
|
321
|
+
return parsed;
|
|
322
|
+
} catch {
|
|
323
|
+
return { message: toolResult.content[0].text };
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
return toolResult;
|
|
327
|
+
})()}
|
|
328
|
+
className="w-full widget-in-chat widget-expanded"
|
|
329
|
+
/>
|
|
330
|
+
</div>
|
|
331
|
+
</div>
|
|
332
|
+
) : null;
|
|
333
|
+
})()}
|
|
334
|
+
</div>
|
|
335
|
+
</div>
|
|
336
|
+
)}
|
|
337
|
+
</div>
|
|
338
|
+
</div>
|
|
339
|
+
</div>
|
|
340
|
+
)}
|
|
341
|
+
</>
|
|
342
|
+
);
|
|
343
|
+
}
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
import { useStudioStore } from '@/lib/store';
|
|
4
4
|
import { WidgetRenderer } from './WidgetRenderer';
|
|
5
|
-
import {
|
|
5
|
+
import { XMarkIcon, ChatBubbleLeftIcon } from '@heroicons/react/24/outline';
|
|
6
6
|
import { useRouter } from 'next/navigation';
|
|
7
7
|
|
|
8
8
|
export function EnlargeModal() {
|
|
@@ -12,7 +12,7 @@ export function EnlargeModal() {
|
|
|
12
12
|
if (!enlargeModal.open || !enlargeModal.item) return null;
|
|
13
13
|
|
|
14
14
|
const { type, item } = enlargeModal;
|
|
15
|
-
|
|
15
|
+
|
|
16
16
|
// Get widget URI
|
|
17
17
|
const componentUri =
|
|
18
18
|
type === 'tool'
|
|
@@ -20,12 +20,12 @@ export function EnlargeModal() {
|
|
|
20
20
|
: item.uri;
|
|
21
21
|
|
|
22
22
|
// Get data from item's examples or responseData - check both examples and _meta
|
|
23
|
-
const widgetData =
|
|
24
|
-
item.examples?.response ||
|
|
25
|
-
item._meta?.['tool/examples']?.response ||
|
|
26
|
-
item.responseData ||
|
|
23
|
+
const widgetData =
|
|
24
|
+
item.examples?.response ||
|
|
25
|
+
item._meta?.['tool/examples']?.response ||
|
|
26
|
+
item.responseData ||
|
|
27
27
|
{};
|
|
28
|
-
|
|
28
|
+
|
|
29
29
|
// Debug logging
|
|
30
30
|
console.log('EnlargeModal - Widget info:', {
|
|
31
31
|
type,
|
|
@@ -40,96 +40,107 @@ export function EnlargeModal() {
|
|
|
40
40
|
|
|
41
41
|
const handleUseInChat = () => {
|
|
42
42
|
if (type !== 'tool') return;
|
|
43
|
-
|
|
43
|
+
|
|
44
44
|
closeEnlargeModal();
|
|
45
|
-
|
|
45
|
+
|
|
46
46
|
// Build the tool execution message
|
|
47
47
|
const toolMessage = `Use the ${item.name} tool`;
|
|
48
|
-
|
|
48
|
+
|
|
49
49
|
// Store both the tool name and the message
|
|
50
50
|
if (typeof window !== 'undefined') {
|
|
51
51
|
window.localStorage.setItem('suggestedTool', item.name);
|
|
52
52
|
window.localStorage.setItem('chatInput', toolMessage);
|
|
53
53
|
}
|
|
54
|
-
|
|
54
|
+
|
|
55
55
|
setCurrentTab('chat');
|
|
56
56
|
router.push('/chat');
|
|
57
57
|
};
|
|
58
58
|
|
|
59
59
|
return (
|
|
60
60
|
<div
|
|
61
|
-
className="fixed inset-0 z-50 flex
|
|
62
|
-
style={{ backgroundColor: 'rgba(0, 0, 0, 0.85)' }}
|
|
61
|
+
className="fixed inset-0 z-50 flex justify-end bg-background/80 backdrop-blur-sm animate-fade-in"
|
|
63
62
|
onClick={closeEnlargeModal}
|
|
64
63
|
>
|
|
65
64
|
<div
|
|
66
|
-
className="relative w-
|
|
65
|
+
className="relative w-full max-w-2xl h-full bg-card border-l border-border shadow-2xl overflow-hidden animate-slide-in-right flex flex-col"
|
|
67
66
|
onClick={(e) => e.stopPropagation()}
|
|
68
67
|
>
|
|
69
68
|
{/* Header */}
|
|
70
|
-
<div className="flex
|
|
71
|
-
<div className="flex items-
|
|
72
|
-
<
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
69
|
+
<div className="flex items-center justify-between px-6 py-4 border-b border-border bg-card">
|
|
70
|
+
<div className="flex items-center gap-3">
|
|
71
|
+
<div className="w-10 h-10 rounded-lg bg-primary/10 flex items-center justify-center">
|
|
72
|
+
<span className="text-xl">{type === 'tool' ? '⚡' : '🎨'}</span>
|
|
73
|
+
</div>
|
|
74
|
+
<div>
|
|
75
|
+
<h2 className="text-lg font-semibold text-foreground truncate max-w-[300px]">{item.name}</h2>
|
|
76
|
+
<p className="text-xs text-muted-foreground">
|
|
77
|
+
{type === 'tool' ? 'Tool Details' : 'Component Preview'}
|
|
77
78
|
</p>
|
|
78
79
|
</div>
|
|
79
80
|
</div>
|
|
80
81
|
|
|
81
|
-
<
|
|
82
|
-
{
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
<span className="hidden sm:inline">Use in Chat</span>
|
|
89
|
-
<span className="sm:hidden">Chat</span>
|
|
90
|
-
</button>
|
|
91
|
-
)}
|
|
92
|
-
<button
|
|
93
|
-
onClick={closeEnlargeModal}
|
|
94
|
-
className="btn btn-ghost w-10 h-10 p-0 flex items-center justify-center flex-shrink-0"
|
|
95
|
-
aria-label="Close"
|
|
96
|
-
>
|
|
97
|
-
<X className="w-5 h-5" />
|
|
98
|
-
</button>
|
|
99
|
-
</div>
|
|
82
|
+
<button
|
|
83
|
+
onClick={closeEnlargeModal}
|
|
84
|
+
className="btn btn-ghost w-8 h-8 p-0 flex items-center justify-center rounded-full hover:bg-muted"
|
|
85
|
+
aria-label="Close"
|
|
86
|
+
>
|
|
87
|
+
<XMarkIcon className="w-5 h-5" />
|
|
88
|
+
</button>
|
|
100
89
|
</div>
|
|
101
90
|
|
|
102
|
-
{/*
|
|
103
|
-
<div className="
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
91
|
+
{/* Content */}
|
|
92
|
+
<div className="flex-1 overflow-y-auto bg-muted/5 p-6">
|
|
93
|
+
<div className="space-y-6">
|
|
94
|
+
{/* Description */}
|
|
95
|
+
<div className="bg-card p-4 rounded-xl border border-border">
|
|
96
|
+
<p className="text-sm text-foreground leading-relaxed">
|
|
97
|
+
{item.description || 'No description available'}
|
|
98
|
+
</p>
|
|
107
99
|
</div>
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
100
|
+
|
|
101
|
+
{/* Preview/Widget */}
|
|
102
|
+
{componentUri && widgetData ? (
|
|
103
|
+
<div className="space-y-2">
|
|
104
|
+
<h3 className="text-xs font-medium text-muted-foreground uppercase tracking-wider">Preview</h3>
|
|
105
|
+
<div className="w-full rounded-xl overflow-hidden border border-border bg-background shadow-sm transition-all duration-200">
|
|
106
|
+
<WidgetRenderer uri={componentUri} data={widgetData} className="w-full widget-in-chat widget-expanded" />
|
|
107
|
+
</div>
|
|
108
|
+
</div>
|
|
109
|
+
) : (
|
|
110
|
+
<div className="bg-muted/20 p-8 rounded-xl border border-border border-dashed text-center">
|
|
111
|
+
<p className="text-sm text-muted-foreground mb-1">
|
|
112
|
+
{!componentUri ? 'No visual widget available' : 'No preview data'}
|
|
113
113
|
</p>
|
|
114
|
-
<p className="text-xs text-muted-foreground">
|
|
115
|
-
{type === 'tool' ? 'This tool
|
|
114
|
+
<p className="text-xs text-muted-foreground/70">
|
|
115
|
+
{type === 'tool' ? 'This tool operates without a UI component' : 'Preview data is missing'}
|
|
116
116
|
</p>
|
|
117
117
|
</div>
|
|
118
|
-
|
|
119
|
-
|
|
118
|
+
)}
|
|
119
|
+
|
|
120
|
+
{/* Input Schema */}
|
|
121
|
+
{type === 'tool' && item.inputSchema && (
|
|
122
|
+
<div className="space-y-2">
|
|
123
|
+
<h3 className="text-xs font-medium text-muted-foreground uppercase tracking-wider">Input Schema</h3>
|
|
124
|
+
<div className="bg-muted/30 rounded-xl border border-border overflow-hidden">
|
|
125
|
+
<pre className="p-4 text-xs font-mono text-foreground overflow-x-auto bg-transparent">
|
|
126
|
+
{JSON.stringify(item.inputSchema, null, 2)}
|
|
127
|
+
</pre>
|
|
128
|
+
</div>
|
|
129
|
+
</div>
|
|
130
|
+
)}
|
|
131
|
+
</div>
|
|
120
132
|
</div>
|
|
121
133
|
|
|
122
|
-
{/*
|
|
123
|
-
{type === 'tool' &&
|
|
124
|
-
<div className="
|
|
125
|
-
<
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
<
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
</details>
|
|
134
|
+
{/* Footer Actions */}
|
|
135
|
+
{type === 'tool' && (
|
|
136
|
+
<div className="p-4 border-t border-border bg-card">
|
|
137
|
+
<button
|
|
138
|
+
onClick={handleUseInChat}
|
|
139
|
+
className="btn btn-primary w-full flex items-center justify-center gap-2 py-3 text-sm font-medium"
|
|
140
|
+
>
|
|
141
|
+
<ChatBubbleLeftIcon className="w-4 h-4" />
|
|
142
|
+
Use in Chat
|
|
143
|
+
</button>
|
|
133
144
|
</div>
|
|
134
145
|
)}
|
|
135
146
|
</div>
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
|
|
3
3
|
import { useState } from 'react';
|
|
4
|
-
import {
|
|
4
|
+
import { ClipboardDocumentIcon, CheckIcon, ChevronDownIcon, ChevronRightIcon } from '@heroicons/react/24/outline';
|
|
5
5
|
|
|
6
6
|
interface LogMessageProps {
|
|
7
7
|
message: string;
|
|
@@ -81,9 +81,9 @@ export function LogMessage({ message }: LogMessageProps) {
|
|
|
81
81
|
className="text-slate-400 hover:text-slate-200 transition-colors"
|
|
82
82
|
>
|
|
83
83
|
{isExpanded ? (
|
|
84
|
-
<
|
|
84
|
+
<ChevronDownIcon className="w-4 h-4" />
|
|
85
85
|
) : (
|
|
86
|
-
<
|
|
86
|
+
<ChevronRightIcon className="w-4 h-4" />
|
|
87
87
|
)}
|
|
88
88
|
</button>
|
|
89
89
|
<span className="text-slate-500 text-xs font-semibold">JSON Response</span>
|
|
@@ -93,9 +93,9 @@ export function LogMessage({ message }: LogMessageProps) {
|
|
|
93
93
|
title={copied ? 'Copied!' : 'Copy JSON'}
|
|
94
94
|
>
|
|
95
95
|
{copied ? (
|
|
96
|
-
<
|
|
96
|
+
<CheckIcon className="w-3.5 h-3.5 text-green-400" />
|
|
97
97
|
) : (
|
|
98
|
-
<
|
|
98
|
+
<ClipboardDocumentIcon className="w-3.5 h-3.5" />
|
|
99
99
|
)}
|
|
100
100
|
</button>
|
|
101
101
|
</div>
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
|
|
3
3
|
import { useState } from 'react';
|
|
4
|
-
import {
|
|
4
|
+
import { ClipboardDocumentIcon, CheckIcon } from '@heroicons/react/24/outline';
|
|
5
5
|
|
|
6
6
|
interface MarkdownRendererProps {
|
|
7
7
|
content: string;
|
|
@@ -80,7 +80,7 @@ export function MarkdownRenderer({ content }: MarkdownRendererProps) {
|
|
|
80
80
|
const language = lang || 'text';
|
|
81
81
|
const trimmedCode = code.trim();
|
|
82
82
|
const highlightedCode = language !== 'text' ? highlightCode(trimmedCode, language) : escapeHtml(trimmedCode);
|
|
83
|
-
|
|
83
|
+
|
|
84
84
|
return `<div class="code-block-wrapper">
|
|
85
85
|
<div class="code-block-header">
|
|
86
86
|
<span class="code-language">${language}</span>
|
|
@@ -189,12 +189,12 @@ export function MarkdownRenderer({ content }: MarkdownRendererProps) {
|
|
|
189
189
|
>
|
|
190
190
|
{copied ? (
|
|
191
191
|
<>
|
|
192
|
-
<
|
|
192
|
+
<CheckIcon className="w-3.5 h-3.5 text-green-400" />
|
|
193
193
|
<span className="text-green-400">Copied!</span>
|
|
194
194
|
</>
|
|
195
195
|
) : (
|
|
196
196
|
<>
|
|
197
|
-
<
|
|
197
|
+
<ClipboardDocumentIcon className="w-3.5 h-3.5 text-slate-300" />
|
|
198
198
|
<span className="text-slate-300">Copy all</span>
|
|
199
199
|
</>
|
|
200
200
|
)}
|