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.
- package/package.json +1 -1
- package/src/studio/app/api/chat/route.ts +33 -15
- package/src/studio/app/auth/callback/page.tsx +6 -6
- package/src/studio/app/chat/page.tsx +1124 -415
- 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/llm-service.ts +104 -1
- package/src/studio/lib/store.ts +36 -21
- package/src/studio/lib/types.ts +1 -1
- 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/templates/typescript-starter/package-lock.json +4112 -0
- package/templates/typescript-starter/package.json +2 -3
- package/templates/typescript-starter/src/modules/calculator/calculator.tools.ts +100 -5
- package/src/studio/app/auth/page.tsx +0 -560
- 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
|
|
442
|
+
parts,
|
|
340
443
|
});
|
|
341
444
|
i++;
|
|
342
445
|
}
|
package/src/studio/lib/store.ts
CHANGED
|
@@ -36,8 +36,8 @@ interface StudioState {
|
|
|
36
36
|
clearChat: () => void;
|
|
37
37
|
currentProvider: 'openai' | 'gemini';
|
|
38
38
|
setCurrentProvider: (provider: 'openai' | 'gemini') => void;
|
|
39
|
-
|
|
40
|
-
|
|
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
|
-
|
|
106
|
-
|
|
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
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
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
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
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);
|