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.
@@ -0,0 +1,327 @@
1
+ 'use client';
2
+
3
+ import { useCallback, useEffect, useMemo } from 'react';
4
+ import ReactFlow, {
5
+ Background,
6
+ Controls,
7
+ Edge,
8
+ Node,
9
+ ReactFlowProvider,
10
+ useNodesState,
11
+ useEdgesState,
12
+ Handle,
13
+ Position,
14
+ MarkerType,
15
+ } from 'reactflow';
16
+ import 'reactflow/dist/style.css';
17
+ import { Tool, Resource, Prompt } from '@/lib/types';
18
+ import { WrenchScrewdriverIcon, BoltIcon, SparklesIcon, CubeIcon, DocumentTextIcon, GlobeAltIcon } from '@heroicons/react/24/outline';
19
+
20
+ interface ToolsCanvasProps {
21
+ tools: Tool[];
22
+ resources?: Resource[];
23
+ prompts?: Prompt[];
24
+ onToolClick?: (tool: Tool) => void;
25
+ onResourceClick?: (resource: Resource) => void;
26
+ onPromptClick?: (prompt: Prompt) => void;
27
+ }
28
+
29
+ // Custom Agent Node (Center)
30
+ const AgentNode = ({ data }: { data: { label: string } }) => {
31
+ return (
32
+ <div className="relative group">
33
+ {/* Glow Effect */}
34
+ <div className="absolute -inset-4 bg-primary/20 rounded-full blur-xl group-hover:bg-primary/30 transition-all duration-500"></div>
35
+
36
+ <div className="relative w-32 h-32 rounded-full border-2 border-primary bg-card/90 backdrop-blur-md shadow-2xl flex flex-col items-center justify-center z-10 animate-pulse-slow">
37
+ <div className="w-12 h-12 rounded-xl bg-primary/10 flex items-center justify-center mb-2">
38
+ <CubeIcon className="w-7 h-7 text-primary" />
39
+ </div>
40
+ <div className="text-[10px] font-mono text-primary uppercase tracking-widest mb-1">AGENT</div>
41
+ <div className="text-xs font-bold text-foreground text-center px-4 leading-tight">{data.label}</div>
42
+
43
+ <Handle type="source" position={Position.Top} className="!bg-primary !w-3 !h-3 !border-2 !border-background" />
44
+ <Handle type="source" position={Position.Right} className="!bg-primary !w-3 !h-3 !border-2 !border-background" />
45
+ <Handle type="source" position={Position.Bottom} className="!bg-primary !w-3 !h-3 !border-2 !border-background" />
46
+ <Handle type="source" position={Position.Left} className="!bg-primary !w-3 !h-3 !border-2 !border-background" />
47
+ </div>
48
+ </div>
49
+ );
50
+ };
51
+
52
+ // Custom Tool Node (Satellite)
53
+ const ToolNode = ({ data }: { data: { tool: Tool } }) => {
54
+ const Icon = data.tool.name.toLowerCase().includes('run') || data.tool.name.toLowerCase().includes('exec')
55
+ ? BoltIcon
56
+ : data.tool.description?.toLowerCase().includes('ai')
57
+ ? SparklesIcon
58
+ : WrenchScrewdriverIcon;
59
+
60
+ return (
61
+ <div className="w-[200px] rounded-lg border border-teal-500/20 bg-card/80 backdrop-blur-sm shadow-xl p-4 transition-all duration-300 hover:border-teal-500/60 hover:shadow-teal-500/10 group cursor-pointer">
62
+ <Handle type="target" position={Position.Left} className="!bg-muted-foreground !w-2 !h-2" />
63
+
64
+ <div className="flex items-start gap-3">
65
+ <div className="w-8 h-8 rounded-lg bg-teal-500/10 flex items-center justify-center shrink-0 group-hover:bg-teal-500/20 transition-colors">
66
+ <Icon className="w-4 h-4 text-teal-500 group-hover:text-teal-400 transition-colors" />
67
+ </div>
68
+ <div>
69
+ <div className="text-[10px] font-mono text-teal-500 uppercase tracking-wider mb-0.5">TOOL</div>
70
+ <div className="font-semibold text-foreground text-sm line-clamp-1">{data.tool.name}</div>
71
+ <div className="text-xs text-muted-foreground line-clamp-2 mt-1">{data.tool.description}</div>
72
+ </div>
73
+ </div>
74
+
75
+ {/* Argument Pills */}
76
+ {data.tool.inputSchema?.properties && Object.keys(data.tool.inputSchema.properties).length > 0 && (
77
+ <div className="flex flex-wrap gap-1 mt-3">
78
+ {Object.keys(data.tool.inputSchema.properties).slice(0, 3).map(arg => (
79
+ <span key={arg} className="px-1.5 py-0.5 rounded text-[10px] bg-muted/50 text-muted-foreground border border-border">
80
+ {arg}
81
+ </span>
82
+ ))}
83
+ {Object.keys(data.tool.inputSchema.properties).length > 3 && (
84
+ <span className="px-1.5 py-0.5 rounded text-[10px] bg-muted/50 text-muted-foreground border border-border">+{Object.keys(data.tool.inputSchema.properties).length - 3}</span>
85
+ )}
86
+ </div>
87
+ )}
88
+ </div>
89
+ );
90
+ };
91
+
92
+ // Custom Resource Node
93
+ const ResourceNode = ({ data }: { data: { resource: Resource } }) => {
94
+ return (
95
+ <div className="w-[200px] rounded-lg border border-indigo-500/20 bg-card/80 backdrop-blur-sm shadow-xl p-4 transition-all duration-300 hover:border-indigo-500/60 hover:shadow-indigo-500/10 group cursor-pointer">
96
+ <Handle type="target" position={Position.Left} className="!bg-muted-foreground !w-2 !h-2" />
97
+
98
+ <div className="flex items-start gap-3">
99
+ <div className="w-8 h-8 rounded-lg bg-indigo-500/10 flex items-center justify-center shrink-0 group-hover:bg-indigo-500/20 transition-colors">
100
+ <CubeIcon className="w-4 h-4 text-indigo-500 group-hover:text-indigo-400 transition-colors" />
101
+ </div>
102
+ <div>
103
+ <div className="text-[10px] font-mono text-indigo-500 uppercase tracking-wider mb-0.5">RESOURCE</div>
104
+ <div className="font-semibold text-foreground text-sm line-clamp-1">{data.resource.name}</div>
105
+ <div className="text-xs text-muted-foreground line-clamp-2 mt-1">{data.resource.uri}</div>
106
+ </div>
107
+ </div>
108
+ <div className="flex flex-wrap gap-1 mt-3">
109
+ <span className="px-1.5 py-0.5 rounded text-[10px] bg-indigo-500/5 text-indigo-500 border border-indigo-500/10">
110
+ {data.resource.mimeType || 'text/plain'}
111
+ </span>
112
+ </div>
113
+ </div>
114
+ );
115
+ };
116
+
117
+ // Custom Prompt Node
118
+ const PromptNode = ({ data }: { data: { prompt: Prompt } }) => {
119
+ return (
120
+ <div className="w-[200px] rounded-lg border border-amber-500/20 bg-card/80 backdrop-blur-sm shadow-xl p-4 transition-all duration-300 hover:border-amber-500/60 hover:shadow-amber-500/10 group cursor-pointer">
121
+ <Handle type="target" position={Position.Left} className="!bg-muted-foreground !w-2 !h-2" />
122
+
123
+ <div className="flex items-start gap-3">
124
+ <div className="w-8 h-8 rounded-lg bg-amber-500/10 flex items-center justify-center shrink-0 group-hover:bg-amber-500/20 transition-colors">
125
+ <DocumentTextIcon className="w-4 h-4 text-amber-500 group-hover:text-amber-400 transition-colors" />
126
+ </div>
127
+ <div>
128
+ <div className="text-[10px] font-mono text-amber-500 uppercase tracking-wider mb-0.5">PROMPT</div>
129
+ <div className="font-semibold text-foreground text-sm line-clamp-1">{data.prompt.name}</div>
130
+ <div className="text-xs text-muted-foreground line-clamp-2 mt-1">{data.prompt.description || 'No description'}</div>
131
+ </div>
132
+ </div>
133
+
134
+ {/* Arguments */}
135
+ {data.prompt.arguments && data.prompt.arguments.length > 0 && (
136
+ <div className="flex flex-wrap gap-1 mt-3">
137
+ {data.prompt.arguments.slice(0, 3).map(arg => (
138
+ <span key={arg.name} className="px-1.5 py-0.5 rounded text-[10px] bg-amber-500/5 text-amber-500 border border-amber-500/10">
139
+ {arg.name}
140
+ </span>
141
+ ))}
142
+ {data.prompt.arguments.length > 3 && (
143
+ <span className="px-1.5 py-0.5 rounded text-[10px] bg-muted/50 text-muted-foreground border border-border">+{data.prompt.arguments.length - 3}</span>
144
+ )}
145
+ </div>
146
+ )}
147
+ </div>
148
+ );
149
+ };
150
+
151
+ const nodeTypes = {
152
+ agent: AgentNode,
153
+ tool: ToolNode,
154
+ resource: ResourceNode,
155
+ prompt: PromptNode,
156
+ };
157
+
158
+ export function ToolsCanvas({ tools, resources = [], prompts = [], onToolClick, onResourceClick, onPromptClick }: ToolsCanvasProps) {
159
+ const [nodes, setNodes, onNodesChange] = useNodesState([]);
160
+ const [edges, setEdges, onEdgesChange] = useEdgesState([]);
161
+
162
+ // Calculate Layout (Categorical Zones)
163
+ useEffect(() => {
164
+ const centerX = 400;
165
+ const centerY = 300;
166
+ const zoneRadius = 350; // Distance of zone centers from main center
167
+
168
+ const newNodes: Node[] = [
169
+ {
170
+ id: 'agent-root',
171
+ type: 'agent',
172
+ position: { x: centerX - 64, y: centerY - 64 }, // Center minus half width/height
173
+ data: { label: 'NitroStack Agent' },
174
+ },
175
+ ];
176
+
177
+ const newEdges: Edge[] = [];
178
+
179
+ // Helper to place items in a grid within a zone
180
+ const placeItemsInZone = (items: any[], type: string, startX: number, startY: number, cols: number = 3) => {
181
+ items.forEach((item, index) => {
182
+ const col = index % cols;
183
+ const row = Math.floor(index / cols);
184
+
185
+ // Grid spacing
186
+ const gapX = 240;
187
+ const gapY = 150;
188
+
189
+ const x = startX + (col * gapX);
190
+ const y = startY + (row * gapY);
191
+
192
+ let nodeId = '';
193
+ let nodeData = {};
194
+ let strokeColor = '#52525b';
195
+
196
+ if (type === 'tool') {
197
+ const tool = item as Tool;
198
+ nodeId = `tool-${tool.name}`;
199
+ nodeData = { tool };
200
+ strokeColor = '#14b8a6'; // Teal
201
+ } else if (type === 'resource') {
202
+ const resource = item as Resource;
203
+ nodeId = `resource-${resource.uri}`;
204
+ nodeData = { resource };
205
+ strokeColor = '#6366f1'; // Indigo
206
+ } else if (type === 'prompt') {
207
+ const prompt = item as Prompt;
208
+ nodeId = `prompt-${prompt.name}`;
209
+ nodeData = { prompt };
210
+ strokeColor = '#f59e0b'; // Amber
211
+ }
212
+
213
+ newNodes.push({
214
+ id: nodeId,
215
+ type: type,
216
+ position: { x, y },
217
+ data: nodeData,
218
+ });
219
+
220
+ newEdges.push({
221
+ id: `e-root-${nodeId}`,
222
+ source: 'agent-root',
223
+ target: nodeId,
224
+ type: 'default',
225
+ animated: true,
226
+ style: { stroke: strokeColor, strokeWidth: 1.5, opacity: 0.3 },
227
+ markerEnd: {
228
+ type: MarkerType.ArrowClosed,
229
+ color: strokeColor,
230
+ }
231
+ });
232
+ });
233
+ }
234
+
235
+ // 1. Prompts (Top Left)
236
+ // Center of zone is approx (-zoneRadius, -zoneRadius) relative to main center
237
+ // We'll define start point for the grid
238
+ if (prompts && prompts.length > 0) {
239
+ const startX = centerX - 600;
240
+ const startY = centerY - 400;
241
+ placeItemsInZone(prompts, 'prompt', startX, startY, 2);
242
+ }
243
+
244
+ // 2. Resources (Top Right)
245
+ if (resources && resources.length > 0) {
246
+ const startX = centerX + 200;
247
+ const startY = centerY - 400;
248
+ placeItemsInZone(resources, 'resource', startX, startY, 2);
249
+ }
250
+
251
+ // 3. Tools (Bottom)
252
+ if (tools && tools.length > 0) {
253
+ // Center the tools grid at the bottom
254
+ const cols = 4;
255
+ const gridWidth = Math.min(tools.length, cols) * 240;
256
+ const startX = centerX - (gridWidth / 2) + 100; // Adjust to center
257
+ const startY = centerY + 200;
258
+ placeItemsInZone(tools, 'tool', startX, startY, cols);
259
+ }
260
+
261
+ setNodes(newNodes);
262
+ setEdges(newEdges);
263
+ }, [tools, resources, prompts, setNodes, setEdges]); // Re-calc when any data changes
264
+
265
+ const handleNodeClick = useCallback((event: React.MouseEvent, node: Node) => {
266
+ if (node.type === 'tool' && node.data.tool && onToolClick) {
267
+ onToolClick(node.data.tool);
268
+ } else if (node.type === 'resource' && node.data.resource && onResourceClick) {
269
+ onResourceClick(node.data.resource);
270
+ } else if (node.type === 'prompt' && node.data.prompt && onPromptClick) {
271
+ onPromptClick(node.data.prompt);
272
+ }
273
+ }, [onToolClick, onResourceClick, onPromptClick]);
274
+
275
+ return (
276
+ <div className="w-full h-full min-h-[600px] bg-[#09090b] rounded-xl overflow-hidden border border-border/50 shadow-inner relative group/canvas">
277
+ <ReactFlow
278
+ nodes={nodes}
279
+ edges={edges}
280
+ onNodesChange={onNodesChange}
281
+ onEdgesChange={onEdgesChange}
282
+ nodeTypes={nodeTypes}
283
+ onNodeClick={handleNodeClick}
284
+ fitView
285
+ className="bg-[#09090b]" // Zinc-950 dark background
286
+ >
287
+ <Background
288
+ color="#27272a" // Zinc-800 dots
289
+ gap={20}
290
+ size={1.5}
291
+ variant={"dots" as any}
292
+ />
293
+ <Controls
294
+ position="bottom-right"
295
+ showInteractive={false}
296
+ className="!flex !flex-row !gap-1 !p-1 !bg-card/90 !backdrop-blur !border !border-border !rounded-lg !shadow-lg !m-4"
297
+ />
298
+ </ReactFlow>
299
+
300
+ {/* Legend / Key */}
301
+ <div className="absolute top-4 left-4 flex gap-3 pointer-events-none">
302
+ <div className="bg-card/80 backdrop-blur px-3 py-1.5 rounded-lg border border-border text-xs text-muted-foreground">
303
+ Interactive Canvas
304
+ </div>
305
+ <div className="flex gap-2">
306
+ <span className="flex items-center gap-1.5 px-2 py-1 bg-teal-500/10 border border-teal-500/20 rounded-md text-[10px] text-teal-500 uppercase tracking-wide font-medium">
307
+ <WrenchScrewdriverIcon className="w-3 h-3" /> Tools
308
+ </span>
309
+ <span className="flex items-center gap-1.5 px-2 py-1 bg-indigo-500/10 border border-indigo-500/20 rounded-md text-[10px] text-indigo-500 uppercase tracking-wide font-medium">
310
+ <CubeIcon className="w-3 h-3" /> Resources
311
+ </span>
312
+ <span className="flex items-center gap-1.5 px-2 py-1 bg-amber-500/10 border border-amber-500/20 rounded-md text-[10px] text-amber-500 uppercase tracking-wide font-medium">
313
+ <DocumentTextIcon className="w-3 h-3" /> Prompts
314
+ </span>
315
+ </div>
316
+ </div>
317
+ </div>
318
+ );
319
+ }
320
+
321
+ export default function ToolsCanvasWrapper(props: ToolsCanvasProps) {
322
+ return (
323
+ <ReactFlowProvider>
324
+ <ToolsCanvas {...props} />
325
+ </ReactFlowProvider>
326
+ )
327
+ }
@@ -46,6 +46,9 @@ interface StudioState {
46
46
  apiKey: string | null;
47
47
  setApiKey: (key: string | null) => void;
48
48
 
49
+ elevenLabsApiKey: string | null;
50
+ setElevenLabsApiKey: (key: string | null) => void;
51
+
49
52
  oauthState: OAuthState;
50
53
  setOAuthState: (state: Partial<OAuthState>) => void;
51
54
 
@@ -130,6 +133,18 @@ export const useStudioStore = create<StudioState>((set) => ({
130
133
  set({ apiKey });
131
134
  },
132
135
 
136
+ elevenLabsApiKey: typeof window !== 'undefined' ? localStorage.getItem('mcp_elevenlabs_key') : null,
137
+ setElevenLabsApiKey: (elevenLabsApiKey) => {
138
+ if (typeof window !== 'undefined') {
139
+ if (elevenLabsApiKey) {
140
+ localStorage.setItem('mcp_elevenlabs_key', elevenLabsApiKey);
141
+ } else {
142
+ localStorage.removeItem('mcp_elevenlabs_key');
143
+ }
144
+ }
145
+ set({ elevenLabsApiKey });
146
+ },
147
+
133
148
  oauthState: typeof window !== 'undefined'
134
149
  ? JSON.parse(localStorage.getItem('mcp_oauth_state') || 'null') || {
135
150
  authServerUrl: null,