nitrostack 1.0.70 → 1.0.71
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 +30 -14
- package/src/studio/app/chat/page.tsx +52 -34
- package/src/studio/lib/llm-service.ts +104 -1
- package/src/studio/lib/store.ts +21 -21
- package/src/studio/lib/types.ts +1 -1
- 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/package.json
CHANGED
|
@@ -14,8 +14,8 @@ export async function POST(request: NextRequest) {
|
|
|
14
14
|
mcpApiKey?: string; // MCP server API key
|
|
15
15
|
};
|
|
16
16
|
|
|
17
|
-
console.log('Received chat request:', {
|
|
18
|
-
provider,
|
|
17
|
+
console.log('Received chat request:', {
|
|
18
|
+
provider,
|
|
19
19
|
messagesCount: messages?.length,
|
|
20
20
|
messages: JSON.stringify(messages),
|
|
21
21
|
hasApiKey: !!apiKey,
|
|
@@ -30,7 +30,7 @@ export async function POST(request: NextRequest) {
|
|
|
30
30
|
{ status: 400 }
|
|
31
31
|
);
|
|
32
32
|
}
|
|
33
|
-
|
|
33
|
+
|
|
34
34
|
if (messages.length === 0) {
|
|
35
35
|
return NextResponse.json(
|
|
36
36
|
{ error: 'Messages array is empty' },
|
|
@@ -46,13 +46,13 @@ export async function POST(request: NextRequest) {
|
|
|
46
46
|
description: tool.description || '',
|
|
47
47
|
inputSchema: tool.inputSchema || {},
|
|
48
48
|
})) || [];
|
|
49
|
-
|
|
49
|
+
|
|
50
50
|
// Add synthetic tools for prompts and resources
|
|
51
51
|
const promptsList = await client.listPrompts().catch(() => ({ prompts: [] }));
|
|
52
52
|
const resourcesList = await client.listResources().catch(() => ({ resources: [] }));
|
|
53
|
-
|
|
53
|
+
|
|
54
54
|
const syntheticTools = [];
|
|
55
|
-
|
|
55
|
+
|
|
56
56
|
// Add a tool to list available prompts
|
|
57
57
|
if (promptsList.prompts && promptsList.prompts.length > 0) {
|
|
58
58
|
syntheticTools.push({
|
|
@@ -63,7 +63,7 @@ export async function POST(request: NextRequest) {
|
|
|
63
63
|
properties: {},
|
|
64
64
|
},
|
|
65
65
|
});
|
|
66
|
-
|
|
66
|
+
|
|
67
67
|
// Add a tool to execute prompts
|
|
68
68
|
syntheticTools.push({
|
|
69
69
|
name: 'execute_prompt',
|
|
@@ -84,7 +84,7 @@ export async function POST(request: NextRequest) {
|
|
|
84
84
|
},
|
|
85
85
|
});
|
|
86
86
|
}
|
|
87
|
-
|
|
87
|
+
|
|
88
88
|
// Add a tool to list available resources
|
|
89
89
|
if (resourcesList.resources && resourcesList.resources.length > 0) {
|
|
90
90
|
syntheticTools.push({
|
|
@@ -95,7 +95,7 @@ export async function POST(request: NextRequest) {
|
|
|
95
95
|
properties: {},
|
|
96
96
|
},
|
|
97
97
|
});
|
|
98
|
-
|
|
98
|
+
|
|
99
99
|
// Add a tool to read resources
|
|
100
100
|
syntheticTools.push({
|
|
101
101
|
name: 'read_resource',
|
|
@@ -112,7 +112,7 @@ export async function POST(request: NextRequest) {
|
|
|
112
112
|
},
|
|
113
113
|
});
|
|
114
114
|
}
|
|
115
|
-
|
|
115
|
+
|
|
116
116
|
const tools = [...mcpTools, ...syntheticTools];
|
|
117
117
|
|
|
118
118
|
// Call LLM
|
|
@@ -126,7 +126,7 @@ export async function POST(request: NextRequest) {
|
|
|
126
126
|
try {
|
|
127
127
|
let result: any;
|
|
128
128
|
let toolContent = '';
|
|
129
|
-
|
|
129
|
+
|
|
130
130
|
// Handle synthetic tools
|
|
131
131
|
if (toolCall.name === 'list_prompts') {
|
|
132
132
|
const prompts = await client.listPrompts();
|
|
@@ -172,21 +172,37 @@ export async function POST(request: NextRequest) {
|
|
|
172
172
|
// Regular MCP tool execution
|
|
173
173
|
// Inject auth tokens into tool arguments if available
|
|
174
174
|
const toolArgs = { ...toolCall.arguments };
|
|
175
|
+
|
|
176
|
+
// Check if we have a file in the last user message and if this is a convert_temperature tool
|
|
177
|
+
if (toolCall.name === 'convert_temperature') {
|
|
178
|
+
const lastUserMessage = messages.slice().reverse().find(m => m.role === 'user');
|
|
179
|
+
if (lastUserMessage && lastUserMessage.file) {
|
|
180
|
+
// Inject file content if not already provided by LLM (or overwrite it to be safe)
|
|
181
|
+
console.log('📂 Injecting file content into convert_temperature tool call');
|
|
182
|
+
toolArgs.file_content = lastUserMessage.file.data;
|
|
183
|
+
toolArgs.file_type = lastUserMessage.file.type;
|
|
184
|
+
|
|
185
|
+
if (!toolArgs.file_name) {
|
|
186
|
+
toolArgs.file_name = lastUserMessage.file.name;
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
|
|
175
191
|
if (jwtToken || mcpApiKey) {
|
|
176
192
|
toolArgs._meta = {
|
|
177
193
|
...(toolArgs._meta || {}),
|
|
178
194
|
};
|
|
179
|
-
|
|
195
|
+
|
|
180
196
|
if (jwtToken) {
|
|
181
197
|
toolArgs._meta._jwt = jwtToken;
|
|
182
198
|
toolArgs._meta.authorization = `Bearer ${jwtToken}`;
|
|
183
199
|
}
|
|
184
|
-
|
|
200
|
+
|
|
185
201
|
if (mcpApiKey) {
|
|
186
202
|
toolArgs._meta.apiKey = mcpApiKey;
|
|
187
203
|
toolArgs._meta['x-api-key'] = mcpApiKey;
|
|
188
204
|
}
|
|
189
|
-
|
|
205
|
+
|
|
190
206
|
console.log(`🔐 Executing tool "${toolCall.name}" with auth:`, {
|
|
191
207
|
hasJwt: !!jwtToken,
|
|
192
208
|
hasMcpApiKey: !!mcpApiKey,
|
|
@@ -30,8 +30,8 @@ export default function ChatPage() {
|
|
|
30
30
|
clearChat,
|
|
31
31
|
currentProvider,
|
|
32
32
|
setCurrentProvider,
|
|
33
|
-
|
|
34
|
-
|
|
33
|
+
currentFile,
|
|
34
|
+
setCurrentFile,
|
|
35
35
|
tools,
|
|
36
36
|
setTools,
|
|
37
37
|
} = useStudioStore();
|
|
@@ -362,7 +362,7 @@ export default function ChatPage() {
|
|
|
362
362
|
}
|
|
363
363
|
};
|
|
364
364
|
|
|
365
|
-
const
|
|
365
|
+
const handleFileUpload = (e: React.ChangeEvent<HTMLInputElement>) => {
|
|
366
366
|
const file = e.target.files?.[0];
|
|
367
367
|
if (!file) return;
|
|
368
368
|
|
|
@@ -373,7 +373,7 @@ export default function ChatPage() {
|
|
|
373
373
|
|
|
374
374
|
const reader = new FileReader();
|
|
375
375
|
reader.onload = (event) => {
|
|
376
|
-
|
|
376
|
+
setCurrentFile({
|
|
377
377
|
data: event.target?.result as string,
|
|
378
378
|
type: file.type,
|
|
379
379
|
name: file.name,
|
|
@@ -383,7 +383,7 @@ export default function ChatPage() {
|
|
|
383
383
|
};
|
|
384
384
|
|
|
385
385
|
const handleSend = async () => {
|
|
386
|
-
if (!inputValue.trim() && !
|
|
386
|
+
if (!inputValue.trim() && !currentFile) return;
|
|
387
387
|
|
|
388
388
|
const apiKey = localStorage.getItem(`${currentProvider}_api_key`);
|
|
389
389
|
if (!apiKey) {
|
|
@@ -397,13 +397,13 @@ export default function ChatPage() {
|
|
|
397
397
|
content: inputValue,
|
|
398
398
|
};
|
|
399
399
|
|
|
400
|
-
if (
|
|
401
|
-
userMessage.
|
|
400
|
+
if (currentFile) {
|
|
401
|
+
userMessage.file = currentFile;
|
|
402
402
|
}
|
|
403
403
|
|
|
404
404
|
addChatMessage(userMessage);
|
|
405
405
|
setInputValue('');
|
|
406
|
-
|
|
406
|
+
setCurrentFile(null);
|
|
407
407
|
setLoading(true);
|
|
408
408
|
|
|
409
409
|
try {
|
|
@@ -425,9 +425,9 @@ export default function ChatPage() {
|
|
|
425
425
|
}
|
|
426
426
|
|
|
427
427
|
// Skip image property for now (not supported by OpenAI chat completions)
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
428
|
+
if (msg.file) {
|
|
429
|
+
cleaned.file = msg.file;
|
|
430
|
+
}
|
|
431
431
|
|
|
432
432
|
return cleaned;
|
|
433
433
|
});
|
|
@@ -536,7 +536,7 @@ export default function ChatPage() {
|
|
|
536
536
|
}
|
|
537
537
|
};
|
|
538
538
|
|
|
539
|
-
const continueChatWithToolResults = async (apiKey: string, messages?:
|
|
539
|
+
const continueChatWithToolResults = async (apiKey: string, messages?: ChatMessage[]) => {
|
|
540
540
|
try {
|
|
541
541
|
// Use provided messages or fall back to store (for recursive calls)
|
|
542
542
|
const messagesToUse = messages || chatMessages;
|
|
@@ -579,7 +579,7 @@ export default function ChatPage() {
|
|
|
579
579
|
|
|
580
580
|
// Recursive tool calls
|
|
581
581
|
if (response.toolCalls && response.toolResults) {
|
|
582
|
-
const newToolResults:
|
|
582
|
+
const newToolResults: ChatMessage[] = [];
|
|
583
583
|
for (const result of response.toolResults) {
|
|
584
584
|
addChatMessage(result);
|
|
585
585
|
newToolResults.push(result);
|
|
@@ -908,19 +908,25 @@ export default function ChatPage() {
|
|
|
908
908
|
{/* ChatGPT-style Input Area - Fixed at bottom */}
|
|
909
909
|
<div className="sticky bottom-0 border-t border-border/50 bg-background/95 backdrop-blur-md shadow-[0_-2px_10px_rgba(0,0,0,0.1)]">
|
|
910
910
|
<div className="max-w-5xl mx-auto px-3 sm:px-4 py-3 sm:py-4">
|
|
911
|
-
{
|
|
911
|
+
{currentFile && (
|
|
912
912
|
<div className="mb-3 p-3 bg-card rounded-xl flex items-start gap-3 border border-border/50 animate-fade-in">
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
913
|
+
{currentFile.type.startsWith('image/') ? (
|
|
914
|
+
<img
|
|
915
|
+
src={currentFile.data}
|
|
916
|
+
alt={currentFile.name}
|
|
917
|
+
className="w-20 h-20 object-cover rounded-lg border border-border"
|
|
918
|
+
/>
|
|
919
|
+
) : (
|
|
920
|
+
<div className="w-20 h-20 rounded-lg border border-border bg-muted flex items-center justify-center">
|
|
921
|
+
<FileText className="w-8 h-8 text-muted-foreground" />
|
|
922
|
+
</div>
|
|
923
|
+
)}
|
|
918
924
|
<div className="flex-1 min-w-0">
|
|
919
|
-
<p className="text-sm font-medium text-foreground truncate">{
|
|
920
|
-
<p className="text-xs text-muted-foreground">{
|
|
925
|
+
<p className="text-sm font-medium text-foreground truncate">{currentFile.name}</p>
|
|
926
|
+
<p className="text-xs text-muted-foreground">{currentFile.type}</p>
|
|
921
927
|
</div>
|
|
922
928
|
<button
|
|
923
|
-
onClick={() =>
|
|
929
|
+
onClick={() => setCurrentFile(null)}
|
|
924
930
|
className="w-7 h-7 rounded-lg flex items-center justify-center bg-muted/50 hover:bg-muted text-muted-foreground hover:text-foreground transition-all flex-shrink-0"
|
|
925
931
|
>
|
|
926
932
|
<X className="w-4 h-4" />
|
|
@@ -931,14 +937,14 @@ export default function ChatPage() {
|
|
|
931
937
|
<input
|
|
932
938
|
type="file"
|
|
933
939
|
ref={fileInputRef}
|
|
934
|
-
onChange={
|
|
935
|
-
accept="image
|
|
940
|
+
onChange={handleFileUpload}
|
|
941
|
+
accept="image/*,.pdf,.txt,.md,.json,.csv,.docx"
|
|
936
942
|
className="hidden"
|
|
937
943
|
/>
|
|
938
944
|
<button
|
|
939
945
|
onClick={() => fileInputRef.current?.click()}
|
|
940
946
|
className="h-11 w-11 rounded-xl flex items-center justify-center bg-muted/50 hover:bg-muted text-muted-foreground hover:text-foreground transition-all flex-shrink-0"
|
|
941
|
-
title="Upload
|
|
947
|
+
title="Upload file"
|
|
942
948
|
>
|
|
943
949
|
<ImageIcon className="w-5 h-5" />
|
|
944
950
|
</button>
|
|
@@ -966,7 +972,7 @@ export default function ChatPage() {
|
|
|
966
972
|
</div>
|
|
967
973
|
<button
|
|
968
974
|
onClick={handleSend}
|
|
969
|
-
disabled={loading || (!inputValue.trim() && !
|
|
975
|
+
disabled={loading || (!inputValue.trim() && !currentFile)}
|
|
970
976
|
className="h-11 w-11 rounded-xl flex items-center justify-center bg-gradient-to-br from-primary to-amber-500 text-white shadow-lg hover:shadow-xl disabled:opacity-50 disabled:cursor-not-allowed transition-all flex-shrink-0 hover:scale-105 active:scale-95"
|
|
971
977
|
title="Send message (Enter)"
|
|
972
978
|
>
|
|
@@ -1097,14 +1103,26 @@ function ChatMessageComponent({ message, tools }: { message: ChatMessage; tools:
|
|
|
1097
1103
|
|
|
1098
1104
|
{/* Message Content */}
|
|
1099
1105
|
<div className="flex-1 min-w-0">
|
|
1100
|
-
{/*
|
|
1101
|
-
{message.
|
|
1102
|
-
<div className="mb-3 rounded-xl overflow-hidden border border-border/50 shadow-sm">
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1106
|
+
{/* File if present */}
|
|
1107
|
+
{message.file && (
|
|
1108
|
+
<div className="mb-3 rounded-xl overflow-hidden border border-border/50 shadow-sm max-w-sm">
|
|
1109
|
+
{message.file.type.startsWith('image/') ? (
|
|
1110
|
+
<img
|
|
1111
|
+
src={message.file.data}
|
|
1112
|
+
alt={message.file.name}
|
|
1113
|
+
className="max-w-full"
|
|
1114
|
+
/>
|
|
1115
|
+
) : (
|
|
1116
|
+
<div className="p-4 bg-muted/30 flex items-center gap-3">
|
|
1117
|
+
<div className="w-10 h-10 rounded-lg bg-primary/10 flex items-center justify-center">
|
|
1118
|
+
<FileText className="w-5 h-5 text-primary" />
|
|
1119
|
+
</div>
|
|
1120
|
+
<div className="flex-1 min-w-0">
|
|
1121
|
+
<p className="text-sm font-medium text-foreground truncate">{message.file.name}</p>
|
|
1122
|
+
<p className="text-xs text-muted-foreground">{message.file.type}</p>
|
|
1123
|
+
</div>
|
|
1124
|
+
</div>
|
|
1125
|
+
)}
|
|
1108
1126
|
</div>
|
|
1109
1127
|
)}
|
|
1110
1128
|
|
|
@@ -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;
|
|
@@ -102,8 +102,8 @@ export const useStudioStore = create<StudioState>((set) => ({
|
|
|
102
102
|
clearChat: () => set({ chatMessages: [] }),
|
|
103
103
|
currentProvider: 'gemini',
|
|
104
104
|
setCurrentProvider: (currentProvider) => set({ currentProvider }),
|
|
105
|
-
|
|
106
|
-
|
|
105
|
+
currentFile: null,
|
|
106
|
+
setCurrentFile: (currentFile) => set({ currentFile }),
|
|
107
107
|
|
|
108
108
|
// Auth
|
|
109
109
|
jwtToken: typeof window !== 'undefined' ? localStorage.getItem('mcp_jwt_token') : null,
|
|
@@ -130,32 +130,32 @@ export const useStudioStore = create<StudioState>((set) => ({
|
|
|
130
130
|
set({ apiKey });
|
|
131
131
|
},
|
|
132
132
|
|
|
133
|
-
oauthState: typeof window !== 'undefined'
|
|
133
|
+
oauthState: typeof window !== 'undefined'
|
|
134
134
|
? JSON.parse(localStorage.getItem('mcp_oauth_state') || 'null') || {
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
135
|
+
authServerUrl: null,
|
|
136
|
+
resourceMetadata: null,
|
|
137
|
+
authServerMetadata: null,
|
|
138
|
+
clientRegistration: null,
|
|
139
|
+
selectedScopes: [],
|
|
140
|
+
currentToken: null,
|
|
141
|
+
}
|
|
142
142
|
: {
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
143
|
+
authServerUrl: null,
|
|
144
|
+
resourceMetadata: null,
|
|
145
|
+
authServerMetadata: null,
|
|
146
|
+
clientRegistration: null,
|
|
147
|
+
selectedScopes: [],
|
|
148
|
+
currentToken: null,
|
|
149
|
+
},
|
|
150
150
|
setOAuthState: (newState) => {
|
|
151
151
|
const updatedState = (state: any) => {
|
|
152
152
|
const newOAuthState = { ...state.oauthState, ...newState };
|
|
153
|
-
|
|
153
|
+
|
|
154
154
|
// Persist to localStorage
|
|
155
155
|
if (typeof window !== 'undefined') {
|
|
156
156
|
localStorage.setItem('mcp_oauth_state', JSON.stringify(newOAuthState));
|
|
157
157
|
}
|
|
158
|
-
|
|
158
|
+
|
|
159
159
|
return { oauthState: newOAuthState };
|
|
160
160
|
};
|
|
161
161
|
set(updatedState);
|