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.
@@ -30,8 +30,8 @@ export default function ChatPage() {
30
30
  clearChat,
31
31
  currentProvider,
32
32
  setCurrentProvider,
33
- currentImage,
34
- setCurrentImage,
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 handleImageUpload = (e: React.ChangeEvent<HTMLInputElement>) => {
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
- setCurrentImage({
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() && !currentImage) return;
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 (currentImage) {
325
- userMessage.image = currentImage;
400
+ if (currentFile) {
401
+ userMessage.file = currentFile;
326
402
  }
327
403
 
328
404
  addChatMessage(userMessage);
329
405
  setInputValue('');
330
- setCurrentImage(null);
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
- // if (msg.image) {
353
- // cleaned.image = msg.image;
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?: Message[]) => {
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: Message[] = [];
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
- {currentImage && (
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
- <img
838
- src={currentImage.data}
839
- alt={currentImage.name}
840
- className="w-20 h-20 object-cover rounded-lg border border-border"
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">{currentImage.name}</p>
844
- <p className="text-xs text-muted-foreground">{currentImage.type}</p>
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={() => setCurrentImage(null)}
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={handleImageUpload}
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 image"
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() && !currentImage)}
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
- {/* Image if present */}
1025
- {message.image && (
1026
- <div className="mb-3 rounded-xl overflow-hidden border border-border/50 shadow-sm">
1027
- <img
1028
- src={message.image.data}
1029
- alt={message.image.name}
1030
- className="max-w-full"
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: [{ 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;
@@ -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
- currentImage: null,
106
- setCurrentImage: (currentImage) => set({ currentImage }),
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
- authServerUrl: null,
136
- resourceMetadata: null,
137
- authServerMetadata: null,
138
- clientRegistration: null,
139
- selectedScopes: [],
140
- currentToken: null,
141
- }
135
+ authServerUrl: null,
136
+ resourceMetadata: null,
137
+ authServerMetadata: null,
138
+ clientRegistration: null,
139
+ selectedScopes: [],
140
+ currentToken: null,
141
+ }
142
142
  : {
143
- authServerUrl: null,
144
- resourceMetadata: null,
145
- authServerMetadata: null,
146
- clientRegistration: null,
147
- selectedScopes: [],
148
- currentToken: null,
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);
@@ -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;