nitrostack 1.0.70 → 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.
Files changed (35) hide show
  1. package/package.json +1 -1
  2. package/src/studio/app/api/chat/route.ts +33 -15
  3. package/src/studio/app/auth/callback/page.tsx +6 -6
  4. package/src/studio/app/chat/page.tsx +1124 -415
  5. package/src/studio/app/chat/page.tsx.backup +1046 -187
  6. package/src/studio/app/globals.css +361 -191
  7. package/src/studio/app/health/page.tsx +72 -76
  8. package/src/studio/app/layout.tsx +9 -11
  9. package/src/studio/app/logs/page.tsx +29 -30
  10. package/src/studio/app/page.tsx +134 -230
  11. package/src/studio/app/prompts/page.tsx +115 -97
  12. package/src/studio/app/resources/page.tsx +115 -124
  13. package/src/studio/app/settings/page.tsx +1080 -125
  14. package/src/studio/app/tools/page.tsx +343 -0
  15. package/src/studio/components/EnlargeModal.tsx +76 -65
  16. package/src/studio/components/LogMessage.tsx +5 -5
  17. package/src/studio/components/MarkdownRenderer.tsx +4 -4
  18. package/src/studio/components/Sidebar.tsx +150 -210
  19. package/src/studio/components/SplashScreen.tsx +109 -0
  20. package/src/studio/components/ToolCard.tsx +50 -41
  21. package/src/studio/components/VoiceOrbOverlay.tsx +469 -0
  22. package/src/studio/components/WidgetRenderer.tsx +8 -3
  23. package/src/studio/components/tools/ToolsCanvas.tsx +327 -0
  24. package/src/studio/lib/llm-service.ts +104 -1
  25. package/src/studio/lib/store.ts +36 -21
  26. package/src/studio/lib/types.ts +1 -1
  27. package/src/studio/package-lock.json +3303 -0
  28. package/src/studio/package.json +3 -1
  29. package/src/studio/public/NitroStudio Isotype Color.png +0 -0
  30. package/src/studio/tailwind.config.ts +63 -17
  31. package/templates/typescript-starter/package-lock.json +4112 -0
  32. package/templates/typescript-starter/package.json +2 -3
  33. package/templates/typescript-starter/src/modules/calculator/calculator.tools.ts +100 -5
  34. package/src/studio/app/auth/page.tsx +0 -560
  35. package/src/studio/app/ping/page.tsx +0 -209
@@ -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
+ }
@@ -9,6 +9,11 @@ export interface ChatMessage {
9
9
  toolCalls?: ToolCall[];
10
10
  toolCallId?: string; // For tool responses - the ID of the call being responded to
11
11
  toolName?: string; // For tool responses - the name of the tool (required by Gemini)
12
+ file?: {
13
+ data: string;
14
+ type: string;
15
+ name: string;
16
+ };
12
17
  }
13
18
 
14
19
  export interface ToolCall {
@@ -187,6 +192,53 @@ User: "list all resources"
187
192
  };
188
193
  }
189
194
 
195
+ if (msg.role === 'user' && msg.file) {
196
+ // Handle file attachments for OpenAI
197
+ const contentParts: any[] = [
198
+ { type: 'text', text: msg.content || ' ' } // Ensure some text exists
199
+ ];
200
+
201
+ if (msg.file.type.startsWith('image/')) {
202
+ contentParts.push({
203
+ type: 'image_url',
204
+ image_url: {
205
+ url: msg.file.data,
206
+ }
207
+ });
208
+ } else {
209
+ // For non-image files, append as text if it's a text file, or note unavailable
210
+ // OpenAI (without Vision/File Search) can't natively handle arbitrary files in chat completions
211
+ // So we append it as context if it looks like text
212
+ if (msg.file.type.startsWith('text/') || msg.file.name.endsWith('.txt') || msg.file.name.endsWith('.md') || msg.file.name.endsWith('.json')) {
213
+ // For text files, we might need to decode base64 if it's base64 encoded
214
+ // Assuming data is "data:mime;base64,..."
215
+ try {
216
+ const base64Content = msg.file.data.split(',')[1];
217
+ const textContent = Buffer.from(base64Content, 'base64').toString('utf-8');
218
+ contentParts.push({
219
+ type: 'text',
220
+ text: `\n\n[Attached File: ${msg.file.name}]\n${textContent}`
221
+ });
222
+ } catch (e) {
223
+ contentParts.push({
224
+ type: 'text',
225
+ text: `\n\n[Attached File: ${msg.file.name}] (Could not decode content)`
226
+ });
227
+ }
228
+ } else {
229
+ contentParts.push({
230
+ type: 'text',
231
+ text: `\n\n[Attached File: ${msg.file.name}] (Type: ${msg.file.type})`
232
+ });
233
+ }
234
+ }
235
+
236
+ return {
237
+ role: msg.role,
238
+ content: contentParts,
239
+ };
240
+ }
241
+
190
242
  return {
191
243
  role: msg.role,
192
244
  content: msg.content,
@@ -334,9 +386,60 @@ User: "list all resources"
334
386
  i++;
335
387
  } else {
336
388
  // Regular user or assistant message
389
+ const parts: any[] = [];
390
+
391
+ if (msg.content) {
392
+ parts.push({ text: msg.content });
393
+ }
394
+
395
+ if (msg.role === 'user' && msg.file) {
396
+ // Extract base64 and mime type
397
+ const matches = msg.file.data.match(/^data:([^;]+);base64,(.+)$/);
398
+ if (matches) {
399
+ const mimeType = matches[1];
400
+ const data = matches[2];
401
+
402
+ // Gemini supports: PDF, image/*, video/*, audio/*
403
+ const isSupported =
404
+ mimeType === 'application/pdf' ||
405
+ mimeType.startsWith('image/') ||
406
+ mimeType.startsWith('video/') ||
407
+ mimeType.startsWith('audio/');
408
+
409
+ if (isSupported) {
410
+ parts.push({
411
+ inlineData: {
412
+ mimeType: mimeType,
413
+ data: data
414
+ }
415
+ });
416
+ } else {
417
+ // For unsupported types (DOCX, TXT, JSON, etc.), try to decode and pass as text context
418
+ // This acts as a "poor man's" file processing for text-based formats
419
+ try {
420
+ const textContent = Buffer.from(data, 'base64').toString('utf-8');
421
+ // Clean up non-printable characters if it's a binary file like DOCX appearing as text
422
+ // Note: For real DOCX parsing we'd need a library, but for now we pass raw or decoded text
423
+ // If the user wants to process it with a tool, the *tool* will get the raw base64.
424
+ // Here we just want to avoid crashing Gemini.
425
+
426
+ parts.push({
427
+ text: `\n\n[Attached File: ${msg.file.name} (${mimeType})]\n(File content is available to tools via file_content parameter)`
428
+ });
429
+ } catch (e) {
430
+ parts.push({
431
+ text: `\n\n[Attached File: ${msg.file.name} (${mimeType})]\n(Content available to tools)`
432
+ });
433
+ }
434
+ }
435
+ } else {
436
+ console.warn('Could not parse file data URI:', msg.file.name);
437
+ }
438
+ }
439
+
337
440
  contents.push({
338
441
  role: msg.role === 'assistant' ? 'model' : 'user',
339
- parts: [{ text: msg.content }],
442
+ parts,
340
443
  });
341
444
  i++;
342
445
  }
@@ -36,8 +36,8 @@ interface StudioState {
36
36
  clearChat: () => void;
37
37
  currentProvider: 'openai' | 'gemini';
38
38
  setCurrentProvider: (provider: 'openai' | 'gemini') => void;
39
- currentImage: { data: string; type: string; name: string } | null;
40
- setCurrentImage: (image: { data: string; type: string; name: string } | null) => void;
39
+ currentFile: { data: string; type: string; name: string } | null;
40
+ setCurrentFile: (file: { data: string; type: string; name: string } | null) => void;
41
41
 
42
42
  // Auth
43
43
  jwtToken: string | null;
@@ -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
 
@@ -102,8 +105,8 @@ export const useStudioStore = create<StudioState>((set) => ({
102
105
  clearChat: () => set({ chatMessages: [] }),
103
106
  currentProvider: 'gemini',
104
107
  setCurrentProvider: (currentProvider) => set({ currentProvider }),
105
- currentImage: null,
106
- setCurrentImage: (currentImage) => set({ currentImage }),
108
+ currentFile: null,
109
+ setCurrentFile: (currentFile) => set({ currentFile }),
107
110
 
108
111
  // Auth
109
112
  jwtToken: typeof window !== 'undefined' ? localStorage.getItem('mcp_jwt_token') : null,
@@ -130,32 +133,44 @@ export const useStudioStore = create<StudioState>((set) => ({
130
133
  set({ apiKey });
131
134
  },
132
135
 
133
- oauthState: typeof window !== 'undefined'
134
- ? JSON.parse(localStorage.getItem('mcp_oauth_state') || 'null') || {
135
- authServerUrl: null,
136
- resourceMetadata: null,
137
- authServerMetadata: null,
138
- clientRegistration: null,
139
- selectedScopes: [],
140
- currentToken: null,
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');
141
143
  }
144
+ }
145
+ set({ elevenLabsApiKey });
146
+ },
147
+
148
+ oauthState: typeof window !== 'undefined'
149
+ ? JSON.parse(localStorage.getItem('mcp_oauth_state') || 'null') || {
150
+ authServerUrl: null,
151
+ resourceMetadata: null,
152
+ authServerMetadata: null,
153
+ clientRegistration: null,
154
+ selectedScopes: [],
155
+ currentToken: null,
156
+ }
142
157
  : {
143
- authServerUrl: null,
144
- resourceMetadata: null,
145
- authServerMetadata: null,
146
- clientRegistration: null,
147
- selectedScopes: [],
148
- currentToken: null,
149
- },
158
+ authServerUrl: null,
159
+ resourceMetadata: null,
160
+ authServerMetadata: null,
161
+ clientRegistration: null,
162
+ selectedScopes: [],
163
+ currentToken: null,
164
+ },
150
165
  setOAuthState: (newState) => {
151
166
  const updatedState = (state: any) => {
152
167
  const newOAuthState = { ...state.oauthState, ...newState };
153
-
168
+
154
169
  // Persist to localStorage
155
170
  if (typeof window !== 'undefined') {
156
171
  localStorage.setItem('mcp_oauth_state', JSON.stringify(newOAuthState));
157
172
  }
158
-
173
+
159
174
  return { oauthState: newOAuthState };
160
175
  };
161
176
  set(updatedState);
@@ -54,7 +54,7 @@ export interface ChatMessage {
54
54
  content: string;
55
55
  toolCalls?: ToolCall[];
56
56
  toolCallId?: string;
57
- image?: {
57
+ file?: {
58
58
  data: string;
59
59
  type: string;
60
60
  name: string;