nitrostack 1.0.69 → 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/README.md +5 -7
- package/dist/core/builders.d.ts +1 -1
- package/dist/core/builders.d.ts.map +1 -1
- package/dist/core/builders.js +6 -3
- package/dist/core/builders.js.map +1 -1
- package/dist/core/decorators.d.ts +21 -0
- package/dist/core/decorators.d.ts.map +1 -1
- package/dist/core/decorators.js +28 -0
- package/dist/core/decorators.js.map +1 -1
- package/dist/core/index.d.ts +1 -1
- package/dist/core/index.d.ts.map +1 -1
- package/dist/core/index.js +1 -1
- package/dist/core/index.js.map +1 -1
- package/dist/core/tool.d.ts +2 -0
- package/dist/core/tool.d.ts.map +1 -1
- package/dist/core/tool.js +6 -0
- package/dist/core/tool.js.map +1 -1
- package/package.json +1 -1
- package/src/studio/app/api/chat/route.ts +30 -14
- package/src/studio/app/chat/page.tsx +128 -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
|
@@ -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();
|
|
@@ -57,6 +57,7 @@ export default function ChatPage() {
|
|
|
57
57
|
const messagesEndRef = useRef<HTMLDivElement>(null);
|
|
58
58
|
const fileInputRef = useRef<HTMLInputElement>(null);
|
|
59
59
|
const textareaRef = useRef<HTMLTextAreaElement>(null);
|
|
60
|
+
const initialToolExecuted = useRef(false);
|
|
60
61
|
|
|
61
62
|
useEffect(() => {
|
|
62
63
|
loadTools();
|
|
@@ -74,6 +75,12 @@ export default function ChatPage() {
|
|
|
74
75
|
}
|
|
75
76
|
}, []);
|
|
76
77
|
|
|
78
|
+
useEffect(() => {
|
|
79
|
+
if (tools.length > 0 && !initialToolExecuted.current) {
|
|
80
|
+
checkAndRunInitialTool();
|
|
81
|
+
}
|
|
82
|
+
}, [tools]);
|
|
83
|
+
|
|
77
84
|
useEffect(() => {
|
|
78
85
|
messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' });
|
|
79
86
|
}, [chatMessages]);
|
|
@@ -225,6 +232,75 @@ export default function ChatPage() {
|
|
|
225
232
|
}
|
|
226
233
|
};
|
|
227
234
|
|
|
235
|
+
const checkAndRunInitialTool = async () => {
|
|
236
|
+
// Find initial tool using specific metadata key
|
|
237
|
+
const initialTool = tools.find(t => t._meta?.['tool/initial'] === true);
|
|
238
|
+
if (!initialTool) return;
|
|
239
|
+
|
|
240
|
+
// Check for API keys (Gemini or OpenAI)
|
|
241
|
+
const geminiKey = localStorage.getItem('gemini_api_key');
|
|
242
|
+
const openaiKey = localStorage.getItem('openai_api_key');
|
|
243
|
+
const hasKey = (geminiKey && geminiKey !== '••••••••') || (openaiKey && openaiKey !== '••••••••');
|
|
244
|
+
|
|
245
|
+
if (!hasKey) return;
|
|
246
|
+
|
|
247
|
+
// Mark as executed immediately to prevent double run
|
|
248
|
+
initialToolExecuted.current = true;
|
|
249
|
+
console.log('🚀 Auto-executing initial tool:', initialTool.name);
|
|
250
|
+
|
|
251
|
+
// Initial message
|
|
252
|
+
const autoMsg: ChatMessage = {
|
|
253
|
+
role: 'user',
|
|
254
|
+
content: `(Auto) Executing initial tool: ${initialTool.name}`,
|
|
255
|
+
};
|
|
256
|
+
addChatMessage(autoMsg);
|
|
257
|
+
setLoading(true);
|
|
258
|
+
|
|
259
|
+
try {
|
|
260
|
+
const { jwtToken, mcpApiKey } = getAuthTokens();
|
|
261
|
+
const effectiveToken = jwtToken || useStudioStore.getState().oauthState?.currentToken;
|
|
262
|
+
|
|
263
|
+
// Call the tool
|
|
264
|
+
const result = await api.callTool(
|
|
265
|
+
initialTool.name,
|
|
266
|
+
{},
|
|
267
|
+
effectiveToken,
|
|
268
|
+
mcpApiKey || undefined
|
|
269
|
+
);
|
|
270
|
+
|
|
271
|
+
// Add assistant message with tool call info
|
|
272
|
+
const toolCallId = `call_${Date.now()}`;
|
|
273
|
+
const assistantMsg: ChatMessage = {
|
|
274
|
+
role: 'assistant',
|
|
275
|
+
content: `Invoking ${initialTool.name}...`,
|
|
276
|
+
toolCalls: [{
|
|
277
|
+
id: toolCallId,
|
|
278
|
+
name: initialTool.name,
|
|
279
|
+
arguments: {},
|
|
280
|
+
result // Attach result here for widget rendering
|
|
281
|
+
}]
|
|
282
|
+
};
|
|
283
|
+
addChatMessage(assistantMsg);
|
|
284
|
+
|
|
285
|
+
// Add tool result message
|
|
286
|
+
const toolResultMsg: ChatMessage = {
|
|
287
|
+
role: 'tool',
|
|
288
|
+
content: JSON.stringify(result),
|
|
289
|
+
toolCallId: toolCallId
|
|
290
|
+
};
|
|
291
|
+
addChatMessage(toolResultMsg);
|
|
292
|
+
|
|
293
|
+
} catch (error) {
|
|
294
|
+
console.error('Initial tool execution failed:', error);
|
|
295
|
+
addChatMessage({
|
|
296
|
+
role: 'assistant',
|
|
297
|
+
content: `Failed to execute initial tool ${initialTool.name}: ${error instanceof Error ? error.message : String(error)}`
|
|
298
|
+
});
|
|
299
|
+
} finally {
|
|
300
|
+
setLoading(false);
|
|
301
|
+
}
|
|
302
|
+
};
|
|
303
|
+
|
|
228
304
|
const handleExecutePrompt = async () => {
|
|
229
305
|
if (!selectedPrompt) return;
|
|
230
306
|
|
|
@@ -286,7 +362,7 @@ export default function ChatPage() {
|
|
|
286
362
|
}
|
|
287
363
|
};
|
|
288
364
|
|
|
289
|
-
const
|
|
365
|
+
const handleFileUpload = (e: React.ChangeEvent<HTMLInputElement>) => {
|
|
290
366
|
const file = e.target.files?.[0];
|
|
291
367
|
if (!file) return;
|
|
292
368
|
|
|
@@ -297,7 +373,7 @@ export default function ChatPage() {
|
|
|
297
373
|
|
|
298
374
|
const reader = new FileReader();
|
|
299
375
|
reader.onload = (event) => {
|
|
300
|
-
|
|
376
|
+
setCurrentFile({
|
|
301
377
|
data: event.target?.result as string,
|
|
302
378
|
type: file.type,
|
|
303
379
|
name: file.name,
|
|
@@ -307,7 +383,7 @@ export default function ChatPage() {
|
|
|
307
383
|
};
|
|
308
384
|
|
|
309
385
|
const handleSend = async () => {
|
|
310
|
-
if (!inputValue.trim() && !
|
|
386
|
+
if (!inputValue.trim() && !currentFile) return;
|
|
311
387
|
|
|
312
388
|
const apiKey = localStorage.getItem(`${currentProvider}_api_key`);
|
|
313
389
|
if (!apiKey) {
|
|
@@ -321,13 +397,13 @@ export default function ChatPage() {
|
|
|
321
397
|
content: inputValue,
|
|
322
398
|
};
|
|
323
399
|
|
|
324
|
-
if (
|
|
325
|
-
userMessage.
|
|
400
|
+
if (currentFile) {
|
|
401
|
+
userMessage.file = currentFile;
|
|
326
402
|
}
|
|
327
403
|
|
|
328
404
|
addChatMessage(userMessage);
|
|
329
405
|
setInputValue('');
|
|
330
|
-
|
|
406
|
+
setCurrentFile(null);
|
|
331
407
|
setLoading(true);
|
|
332
408
|
|
|
333
409
|
try {
|
|
@@ -349,9 +425,9 @@ export default function ChatPage() {
|
|
|
349
425
|
}
|
|
350
426
|
|
|
351
427
|
// Skip image property for now (not supported by OpenAI chat completions)
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
428
|
+
if (msg.file) {
|
|
429
|
+
cleaned.file = msg.file;
|
|
430
|
+
}
|
|
355
431
|
|
|
356
432
|
return cleaned;
|
|
357
433
|
});
|
|
@@ -460,7 +536,7 @@ export default function ChatPage() {
|
|
|
460
536
|
}
|
|
461
537
|
};
|
|
462
538
|
|
|
463
|
-
const continueChatWithToolResults = async (apiKey: string, messages?:
|
|
539
|
+
const continueChatWithToolResults = async (apiKey: string, messages?: ChatMessage[]) => {
|
|
464
540
|
try {
|
|
465
541
|
// Use provided messages or fall back to store (for recursive calls)
|
|
466
542
|
const messagesToUse = messages || chatMessages;
|
|
@@ -503,7 +579,7 @@ export default function ChatPage() {
|
|
|
503
579
|
|
|
504
580
|
// Recursive tool calls
|
|
505
581
|
if (response.toolCalls && response.toolResults) {
|
|
506
|
-
const newToolResults:
|
|
582
|
+
const newToolResults: ChatMessage[] = [];
|
|
507
583
|
for (const result of response.toolResults) {
|
|
508
584
|
addChatMessage(result);
|
|
509
585
|
newToolResults.push(result);
|
|
@@ -832,19 +908,25 @@ export default function ChatPage() {
|
|
|
832
908
|
{/* ChatGPT-style Input Area - Fixed at bottom */}
|
|
833
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)]">
|
|
834
910
|
<div className="max-w-5xl mx-auto px-3 sm:px-4 py-3 sm:py-4">
|
|
835
|
-
{
|
|
911
|
+
{currentFile && (
|
|
836
912
|
<div className="mb-3 p-3 bg-card rounded-xl flex items-start gap-3 border border-border/50 animate-fade-in">
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
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
|
+
)}
|
|
842
924
|
<div className="flex-1 min-w-0">
|
|
843
|
-
<p className="text-sm font-medium text-foreground truncate">{
|
|
844
|
-
<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>
|
|
845
927
|
</div>
|
|
846
928
|
<button
|
|
847
|
-
onClick={() =>
|
|
929
|
+
onClick={() => setCurrentFile(null)}
|
|
848
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"
|
|
849
931
|
>
|
|
850
932
|
<X className="w-4 h-4" />
|
|
@@ -855,14 +937,14 @@ export default function ChatPage() {
|
|
|
855
937
|
<input
|
|
856
938
|
type="file"
|
|
857
939
|
ref={fileInputRef}
|
|
858
|
-
onChange={
|
|
859
|
-
accept="image
|
|
940
|
+
onChange={handleFileUpload}
|
|
941
|
+
accept="image/*,.pdf,.txt,.md,.json,.csv,.docx"
|
|
860
942
|
className="hidden"
|
|
861
943
|
/>
|
|
862
944
|
<button
|
|
863
945
|
onClick={() => fileInputRef.current?.click()}
|
|
864
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"
|
|
865
|
-
title="Upload
|
|
947
|
+
title="Upload file"
|
|
866
948
|
>
|
|
867
949
|
<ImageIcon className="w-5 h-5" />
|
|
868
950
|
</button>
|
|
@@ -890,7 +972,7 @@ export default function ChatPage() {
|
|
|
890
972
|
</div>
|
|
891
973
|
<button
|
|
892
974
|
onClick={handleSend}
|
|
893
|
-
disabled={loading || (!inputValue.trim() && !
|
|
975
|
+
disabled={loading || (!inputValue.trim() && !currentFile)}
|
|
894
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"
|
|
895
977
|
title="Send message (Enter)"
|
|
896
978
|
>
|
|
@@ -1021,14 +1103,26 @@ function ChatMessageComponent({ message, tools }: { message: ChatMessage; tools:
|
|
|
1021
1103
|
|
|
1022
1104
|
{/* Message Content */}
|
|
1023
1105
|
<div className="flex-1 min-w-0">
|
|
1024
|
-
{/*
|
|
1025
|
-
{message.
|
|
1026
|
-
<div className="mb-3 rounded-xl overflow-hidden border border-border/50 shadow-sm">
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
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
|
+
)}
|
|
1032
1126
|
</div>
|
|
1033
1127
|
)}
|
|
1034
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);
|