nitrostack 1.0.65 → 1.0.66

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 (55) hide show
  1. package/package.json +2 -1
  2. package/src/studio/README.md +140 -0
  3. package/src/studio/app/api/auth/fetch-metadata/route.ts +71 -0
  4. package/src/studio/app/api/auth/register-client/route.ts +67 -0
  5. package/src/studio/app/api/chat/route.ts +250 -0
  6. package/src/studio/app/api/health/checks/route.ts +42 -0
  7. package/src/studio/app/api/health/route.ts +13 -0
  8. package/src/studio/app/api/init/route.ts +109 -0
  9. package/src/studio/app/api/ping/route.ts +13 -0
  10. package/src/studio/app/api/prompts/[name]/route.ts +21 -0
  11. package/src/studio/app/api/prompts/route.ts +13 -0
  12. package/src/studio/app/api/resources/[...uri]/route.ts +18 -0
  13. package/src/studio/app/api/resources/route.ts +13 -0
  14. package/src/studio/app/api/roots/route.ts +13 -0
  15. package/src/studio/app/api/sampling/route.ts +14 -0
  16. package/src/studio/app/api/tools/[name]/call/route.ts +41 -0
  17. package/src/studio/app/api/tools/route.ts +23 -0
  18. package/src/studio/app/api/widget-examples/route.ts +44 -0
  19. package/src/studio/app/auth/callback/page.tsx +175 -0
  20. package/src/studio/app/auth/page.tsx +560 -0
  21. package/src/studio/app/chat/page.tsx +1133 -0
  22. package/src/studio/app/chat/page.tsx.backup +390 -0
  23. package/src/studio/app/globals.css +486 -0
  24. package/src/studio/app/health/page.tsx +179 -0
  25. package/src/studio/app/layout.tsx +68 -0
  26. package/src/studio/app/logs/page.tsx +279 -0
  27. package/src/studio/app/page.tsx +351 -0
  28. package/src/studio/app/page.tsx.backup +346 -0
  29. package/src/studio/app/ping/page.tsx +209 -0
  30. package/src/studio/app/prompts/page.tsx +230 -0
  31. package/src/studio/app/resources/page.tsx +315 -0
  32. package/src/studio/app/settings/page.tsx +199 -0
  33. package/src/studio/branding.md +807 -0
  34. package/src/studio/components/EnlargeModal.tsx +138 -0
  35. package/src/studio/components/LogMessage.tsx +153 -0
  36. package/src/studio/components/MarkdownRenderer.tsx +410 -0
  37. package/src/studio/components/Sidebar.tsx +295 -0
  38. package/src/studio/components/ToolCard.tsx +139 -0
  39. package/src/studio/components/WidgetRenderer.tsx +346 -0
  40. package/src/studio/lib/api.ts +207 -0
  41. package/src/studio/lib/http-client-transport.ts +222 -0
  42. package/src/studio/lib/llm-service.ts +480 -0
  43. package/src/studio/lib/log-manager.ts +76 -0
  44. package/src/studio/lib/mcp-client.ts +258 -0
  45. package/src/studio/lib/store.ts +192 -0
  46. package/src/studio/lib/theme-provider.tsx +50 -0
  47. package/src/studio/lib/types.ts +107 -0
  48. package/src/studio/lib/widget-loader.ts +90 -0
  49. package/src/studio/middleware.ts +27 -0
  50. package/src/studio/next.config.js +38 -0
  51. package/src/studio/package.json +35 -0
  52. package/src/studio/postcss.config.mjs +10 -0
  53. package/src/studio/public/nitrocloud.png +0 -0
  54. package/src/studio/tailwind.config.ts +67 -0
  55. package/src/studio/tsconfig.json +42 -0
@@ -0,0 +1,346 @@
1
+ 'use client';
2
+
3
+ import { useEffect, useRef, useState } from 'react';
4
+ import { getWidgetUrl, createWidgetHTML, postMessageToWidget } from '@/lib/widget-loader';
5
+
6
+ interface WidgetRendererProps {
7
+ uri: string;
8
+ data: any;
9
+ className?: string;
10
+ }
11
+
12
+ export function WidgetRenderer({ uri, data, className = '' }: WidgetRendererProps) {
13
+ const iframeRef = useRef<HTMLIFrameElement>(null);
14
+ const [contentHeight, setContentHeight] = useState<number>(200); // Start with smaller default
15
+
16
+ // Check if we're in dev mode (localhost)
17
+ const isDevMode =
18
+ typeof window !== 'undefined' &&
19
+ (window.location.hostname === 'localhost' || window.location.hostname === '127.0.0.1');
20
+
21
+ useEffect(() => {
22
+ if (!iframeRef.current) return;
23
+
24
+ const injectOpenAiRuntime = (iframe: HTMLIFrameElement) => {
25
+ if (!iframe.contentWindow) return;
26
+
27
+ // Detect system theme
28
+ const getSystemTheme = (): 'light' | 'dark' => {
29
+ if (typeof window === 'undefined') return 'light';
30
+ return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light';
31
+ };
32
+
33
+ // Detect device capabilities
34
+ const getUserAgent = () => ({
35
+ device: {
36
+ type: (window.matchMedia('(max-width: 768px)').matches
37
+ ? 'mobile'
38
+ : window.matchMedia('(max-width: 1024px)').matches
39
+ ? 'tablet'
40
+ : 'desktop') as 'mobile' | 'tablet' | 'desktop',
41
+ },
42
+ capabilities: {
43
+ hover: window.matchMedia('(hover: hover)').matches,
44
+ touch: window.matchMedia('(pointer: coarse)').matches,
45
+ },
46
+ });
47
+
48
+ // Create window.openai polyfill
49
+ const openaiRuntime = {
50
+ // Data
51
+ toolInput: {},
52
+ toolOutput: data,
53
+ toolResponseMetadata: null,
54
+ widgetState: null,
55
+
56
+ // Visuals
57
+ theme: getSystemTheme(),
58
+ locale: navigator.language || 'en-US',
59
+ userAgent: getUserAgent(),
60
+
61
+ // Layout
62
+ maxHeight: 450, // Match iframe height
63
+ displayMode: 'inline' as const,
64
+ safeArea: {
65
+ insets: { top: 0, bottom: 0, left: 0, right: 0 },
66
+ },
67
+
68
+ // State management
69
+ setWidgetState: async (state: any) => {
70
+ console.log('📦 Widget state updated:', state);
71
+ openaiRuntime.widgetState = state;
72
+
73
+ // Dispatch event for hooks to react
74
+ const event = new CustomEvent('openai:set_globals', {
75
+ detail: { globals: { widgetState: state } },
76
+ });
77
+ iframe.contentWindow?.dispatchEvent(event);
78
+
79
+ // TODO: Persist to chat message context
80
+ },
81
+
82
+ // Actions
83
+ callTool: async (name: string, args: Record<string, unknown>) => {
84
+ console.log('🔧 Widget calling tool:', name, args);
85
+ // TODO: Implement tool call via studio API
86
+ return { result: 'Tool call not yet implemented' };
87
+ },
88
+
89
+ sendFollowUpMessage: async ({ prompt }: { prompt: string }) => {
90
+ console.log('💬 Widget sending follow-up:', prompt);
91
+ // TODO: Implement follow-up message
92
+ },
93
+
94
+ openExternal: ({ href }: { href: string }) => {
95
+ window.open(href, '_blank', 'noopener,noreferrer');
96
+ },
97
+
98
+ requestClose: () => {
99
+ console.log('❌ Widget requested close');
100
+ // TODO: Implement widget close
101
+ },
102
+
103
+ requestDisplayMode: async ({ mode }: { mode: 'inline' | 'pip' | 'fullscreen' }) => {
104
+ console.log('🖼️ Widget requested display mode:', mode);
105
+ // TODO: Implement display mode change
106
+ return { mode: 'inline' as const };
107
+ },
108
+ };
109
+
110
+ // Prepare serializable data (no functions)
111
+ const openaiData = {
112
+ // Data
113
+ toolInput: {},
114
+ toolOutput: data,
115
+ toolResponseMetadata: null,
116
+ widgetState: null,
117
+
118
+ // Visuals
119
+ theme: getSystemTheme(),
120
+ locale: navigator.language || 'en-US',
121
+ userAgent: getUserAgent(),
122
+
123
+ // Layout
124
+ maxHeight: 450,
125
+ displayMode: 'inline' as const,
126
+ safeArea: {
127
+ insets: { top: 0, bottom: 0, left: 0, right: 0 },
128
+ },
129
+ };
130
+
131
+ // Send openai data to iframe via postMessage (cross-origin safe)
132
+ const sendData = () => {
133
+ try {
134
+ iframe.contentWindow?.postMessage({
135
+ type: 'NITRO_INJECT_OPENAI',
136
+ data: openaiData
137
+ }, '*');
138
+ console.log('✅ window.openai data sent to widget via postMessage');
139
+ } catch (e) {
140
+ console.error('❌ Failed to send window.openai:', e);
141
+ }
142
+ };
143
+
144
+ // Send immediately
145
+ sendData();
146
+
147
+ // Retry after delays to ensure widget React app has mounted
148
+ setTimeout(sendData, 100);
149
+ setTimeout(sendData, 300);
150
+ setTimeout(sendData, 500);
151
+
152
+ // Set up message listener for widget RPC calls
153
+ const handleWidgetMessage = (event: MessageEvent) => {
154
+ if (event.data?.type === 'NITRO_WIDGET_RPC') {
155
+ const { method, args, id } = event.data;
156
+
157
+ switch (method) {
158
+ case 'setWidgetState':
159
+ console.log('📦 Widget state updated:', args[0]);
160
+ openaiData.widgetState = args[0];
161
+ // Send response
162
+ iframe.contentWindow?.postMessage({
163
+ type: 'NITRO_WIDGET_RPC_RESPONSE',
164
+ id,
165
+ result: null
166
+ }, '*');
167
+ break;
168
+
169
+ case 'callTool':
170
+ console.log('🔧 Widget calling tool:', args[0], args[1]);
171
+ // Dispatch event for chat page to handle
172
+ window.dispatchEvent(new CustomEvent('widget-tool-call', {
173
+ detail: { toolName: args[0], toolArgs: args[1] }
174
+ }));
175
+ iframe.contentWindow?.postMessage({
176
+ type: 'NITRO_WIDGET_RPC_RESPONSE',
177
+ id,
178
+ result: { success: true }
179
+ }, '*');
180
+ break;
181
+
182
+ case 'openExternal':
183
+ window.open(args[0].href, '_blank', 'noopener,noreferrer');
184
+ iframe.contentWindow?.postMessage({
185
+ type: 'NITRO_WIDGET_RPC_RESPONSE',
186
+ id,
187
+ result: null
188
+ }, '*');
189
+ break;
190
+
191
+ case 'requestDisplayMode':
192
+ console.log('🖼️ Widget requested display mode:', args[0].mode);
193
+ if (args[0].mode === 'fullscreen') {
194
+ // Dispatch custom event for chat page to handle
195
+ window.dispatchEvent(new CustomEvent('widget-fullscreen-request', {
196
+ detail: { uri, data }
197
+ }));
198
+ }
199
+ iframe.contentWindow?.postMessage({
200
+ type: 'NITRO_WIDGET_RPC_RESPONSE',
201
+ id,
202
+ result: { mode: args[0].mode }
203
+ }, '*');
204
+ break;
205
+ }
206
+ }
207
+
208
+ // Handle resize messages from widget
209
+ if (event.data?.type === 'NITRO_WIDGET_RESIZE') {
210
+ const { height } = event.data;
211
+ console.log('📏 Received widget resize:', height);
212
+ if (height && typeof height === 'number') {
213
+ const newHeight = Math.min(height, 400);
214
+ console.log('📏 Setting content height to:', newHeight);
215
+ setContentHeight(newHeight); // Cap at 400px max
216
+ }
217
+ }
218
+ };
219
+
220
+ window.addEventListener('message', handleWidgetMessage);
221
+ };
222
+
223
+ if (isDevMode) {
224
+ // Dev mode: load from widget dev server (port 3001)
225
+ let widgetPath = uri;
226
+
227
+ // Handle ui://widget/ URIs
228
+ if (widgetPath.startsWith('ui://widget/')) {
229
+ widgetPath = widgetPath.replace('ui://widget/', '');
230
+ widgetPath = widgetPath.replace(/\.html$/, '');
231
+ }
232
+ // Handle /widgets/ prefix (legacy)
233
+ else if (widgetPath.startsWith('/widgets/')) {
234
+ widgetPath = widgetPath.replace('/widgets/', '');
235
+ }
236
+ // Handle leading slash
237
+ else if (widgetPath.startsWith('/')) {
238
+ widgetPath = widgetPath.substring(1);
239
+ }
240
+
241
+ const widgetUrl = `http://localhost:3001/${widgetPath}`;
242
+
243
+ console.log('Loading widget in dev mode:', { uri, widgetPath, widgetUrl, data });
244
+
245
+ // Set up onload handler BEFORE setting src
246
+ iframeRef.current.onload = () => {
247
+ console.log('Widget iframe loaded, injecting window.openai...');
248
+
249
+ // Inject window.openai runtime
250
+ if (iframeRef.current) {
251
+ injectOpenAiRuntime(iframeRef.current);
252
+ }
253
+
254
+ // Also send legacy postMessage for backward compatibility
255
+ setTimeout(() => {
256
+ try {
257
+ iframeRef.current?.contentWindow?.postMessage(
258
+ {
259
+ type: 'toolOutput',
260
+ data,
261
+ },
262
+ '*'
263
+ );
264
+ console.log('✅ Legacy data posted to widget:', data);
265
+ } catch (e) {
266
+ console.error('❌ Failed to post message to widget:', e);
267
+ }
268
+ }, 300);
269
+ };
270
+
271
+ // Set src AFTER onload handler is set
272
+ iframeRef.current.src = widgetUrl;
273
+ } else {
274
+ // Production mode: fetch and render
275
+ const loadProductionWidget = async () => {
276
+ try {
277
+ // Convert widget route/path to resource URI format if needed
278
+ let resourceUri = uri;
279
+ if (!uri.startsWith('widget://') && !uri.startsWith('http://') && !uri.startsWith('https://')) {
280
+ const widgetPath = uri.startsWith('/') ? uri.substring(1) : uri;
281
+ resourceUri = `widget://${widgetPath}`;
282
+ }
283
+
284
+ console.log('Loading widget in production mode:', { originalUri: uri, resourceUri, data });
285
+
286
+ const response = await fetch(`/api/resources/${encodeURIComponent(resourceUri)}`);
287
+ const result = await response.json();
288
+
289
+ if (result.contents && result.contents.length > 0) {
290
+ const html = result.contents[0].text || '';
291
+ const completeHtml = createWidgetHTML(html, data);
292
+
293
+ // Use Blob URL
294
+ const blob = new Blob([completeHtml], { type: 'text/html' });
295
+ const blobUrl = URL.createObjectURL(blob);
296
+
297
+ if (iframeRef.current) {
298
+ iframeRef.current.src = blobUrl;
299
+ iframeRef.current.onload = () => {
300
+ // Inject window.openai runtime
301
+ if (iframeRef.current) {
302
+ injectOpenAiRuntime(iframeRef.current);
303
+ }
304
+
305
+ URL.revokeObjectURL(blobUrl);
306
+ };
307
+ }
308
+ console.log('✅ Widget loaded successfully:', { resourceUri, hasHtml: !!html, hasData: !!data });
309
+ } else {
310
+ console.warn('⚠️ Widget resource found but no content:', { resourceUri, result });
311
+ }
312
+ } catch (error) {
313
+ console.error('❌ Failed to load widget:', {
314
+ originalUri: uri,
315
+ error: error instanceof Error ? error.message : String(error),
316
+ stack: error instanceof Error ? error.stack : undefined
317
+ });
318
+ }
319
+ };
320
+
321
+ loadProductionWidget();
322
+ }
323
+ }, [uri, data, isDevMode]);
324
+
325
+ const isInChat = className?.includes('widget-in-chat');
326
+ const finalHeight = isInChat ? `${contentHeight}px` : '100%';
327
+
328
+ return (
329
+ <iframe
330
+ ref={iframeRef}
331
+ className={className}
332
+ sandbox="allow-scripts allow-same-origin"
333
+ style={{
334
+ width: '100%',
335
+ height: finalHeight,
336
+ maxHeight: isInChat ? '400px' : '100%',
337
+ border: 'none',
338
+ background: 'transparent',
339
+ overflow: 'hidden',
340
+ transition: 'height 0.2s ease-out',
341
+ display: 'block',
342
+ }}
343
+ />
344
+ );
345
+ }
346
+
@@ -0,0 +1,207 @@
1
+ // NitroStack Studio API Client
2
+
3
+ const API_BASE = typeof window !== 'undefined' ? window.location.origin : 'http://localhost:3000';
4
+
5
+ export class StudioAPI {
6
+ private baseUrl: string;
7
+ private initialized: boolean = false;
8
+
9
+ constructor(baseUrl: string = API_BASE) {
10
+ this.baseUrl = baseUrl;
11
+ }
12
+
13
+ // Initialize MCP connection (call once on app start)
14
+ async initialize() {
15
+ if (this.initialized) return;
16
+ try {
17
+ await fetch(`${this.baseUrl}/api/init`, { method: 'POST' });
18
+ this.initialized = true;
19
+ } catch (error) {
20
+ console.error('Failed to initialize MCP connection:', error);
21
+ }
22
+ }
23
+
24
+ // Connection
25
+ async checkConnection() {
26
+ const response = await fetch(`${this.baseUrl}/api/health`);
27
+ return response.json();
28
+ }
29
+
30
+ // Tools
31
+ async listTools() {
32
+ const response = await fetch(`${this.baseUrl}/api/tools`);
33
+ return response.json();
34
+ }
35
+
36
+ async getTools() {
37
+ return this.listTools();
38
+ }
39
+
40
+ async callTool(name: string, args: any, jwtToken?: string, apiKey?: string) {
41
+ const response = await fetch(`${this.baseUrl}/api/tools/${name}/call`, {
42
+ method: 'POST',
43
+ headers: { 'Content-Type': 'application/json' },
44
+ body: JSON.stringify({ args, jwtToken, apiKey }),
45
+ });
46
+ return response.json();
47
+ }
48
+
49
+ // Resources
50
+ async getResources() {
51
+ const response = await fetch(`${this.baseUrl}/api/resources`);
52
+ return response.json();
53
+ }
54
+
55
+ async getResource(uri: string) {
56
+ const response = await fetch(`${this.baseUrl}/api/resources/${encodeURIComponent(uri)}`);
57
+ return response.json();
58
+ }
59
+
60
+ // Prompts
61
+ async getPrompts() {
62
+ const response = await fetch(`${this.baseUrl}/api/prompts`);
63
+ return response.json();
64
+ }
65
+
66
+ async executePrompt(name: string, args: any) {
67
+ const response = await fetch(`${this.baseUrl}/api/prompts/${name}`, {
68
+ method: 'POST',
69
+ headers: { 'Content-Type': 'application/json' },
70
+ body: JSON.stringify(args),
71
+ });
72
+ return response.json();
73
+ }
74
+
75
+ // Ping
76
+ async ping() {
77
+ const response = await fetch(`${this.baseUrl}/api/ping`);
78
+ return response.json();
79
+ }
80
+
81
+ // Sampling
82
+ async sample(params: { prompt: string; maxTokens: number; model?: string }) {
83
+ const response = await fetch(`${this.baseUrl}/api/sampling`, {
84
+ method: 'POST',
85
+ headers: { 'Content-Type': 'application/json' },
86
+ body: JSON.stringify(params),
87
+ });
88
+ return response.json();
89
+ }
90
+
91
+ // Roots
92
+ async getRoots() {
93
+ const response = await fetch(`${this.baseUrl}/api/roots`);
94
+ return response.json();
95
+ }
96
+
97
+ // Chat
98
+ async chat(params: {
99
+ provider: string;
100
+ messages: any[];
101
+ apiKey: string; // LLM API key (OpenAI/Gemini)
102
+ jwtToken?: string; // MCP server JWT token
103
+ mcpApiKey?: string; // MCP server API key
104
+ }) {
105
+ const response = await fetch(`${this.baseUrl}/api/chat`, {
106
+ method: 'POST',
107
+ headers: { 'Content-Type': 'application/json' },
108
+ body: JSON.stringify(params),
109
+ });
110
+ return response.json();
111
+ }
112
+
113
+ // Auth (OAuth 2.1)
114
+ async discoverAuth(url: string, type: 'resource' | 'auth-server') {
115
+ const response = await fetch(`${this.baseUrl}/api/auth/fetch-metadata`, {
116
+ method: 'POST',
117
+ headers: { 'Content-Type': 'application/json' },
118
+ body: JSON.stringify({ url, type }),
119
+ });
120
+ return response.json();
121
+ }
122
+
123
+ async registerClient(endpoint: string, metadata: any) {
124
+ const response = await fetch(`${this.baseUrl}/api/auth/register-client`, {
125
+ method: 'POST',
126
+ headers: { 'Content-Type': 'application/json' },
127
+ body: JSON.stringify({ endpoint, metadata }),
128
+ });
129
+ return response.json();
130
+ }
131
+
132
+ async startOAuthFlow(params: {
133
+ authorizationEndpoint: string;
134
+ clientId: string;
135
+ redirectUri: string;
136
+ scope: string;
137
+ resource: string;
138
+ }) {
139
+ const response = await fetch(`${this.baseUrl}/api/auth/start-flow`, {
140
+ method: 'POST',
141
+ headers: { 'Content-Type': 'application/json' },
142
+ body: JSON.stringify(params),
143
+ });
144
+ return response.json();
145
+ }
146
+
147
+ async exchangeToken(params: {
148
+ code: string;
149
+ pkce: any;
150
+ tokenEndpoint: string;
151
+ clientId: string;
152
+ clientSecret?: string;
153
+ redirectUri: string;
154
+ resource: string;
155
+ }) {
156
+ const response = await fetch(`${this.baseUrl}/api/auth/exchange-token`, {
157
+ method: 'POST',
158
+ headers: { 'Content-Type': 'application/json' },
159
+ body: JSON.stringify(params),
160
+ });
161
+ return response.json();
162
+ }
163
+
164
+ async refreshToken(params: {
165
+ refreshToken: string;
166
+ tokenEndpoint: string;
167
+ clientId: string;
168
+ clientSecret?: string;
169
+ resource: string;
170
+ }) {
171
+ const response = await fetch(`${this.baseUrl}/api/auth/refresh-token`, {
172
+ method: 'POST',
173
+ headers: { 'Content-Type': 'application/json' },
174
+ body: JSON.stringify(params),
175
+ });
176
+ return response.json();
177
+ }
178
+
179
+ async revokeToken(params: {
180
+ token: string;
181
+ revocationEndpoint: string;
182
+ clientId: string;
183
+ clientSecret?: string;
184
+ }) {
185
+ const response = await fetch(`${this.baseUrl}/api/auth/revoke-token`, {
186
+ method: 'POST',
187
+ headers: { 'Content-Type': 'application/json' },
188
+ body: JSON.stringify(params),
189
+ });
190
+ return response.json();
191
+ }
192
+
193
+ // Health
194
+ async getHealth() {
195
+ const response = await fetch(`${this.baseUrl}/api/health/checks`);
196
+ return response.json();
197
+ }
198
+
199
+ // Widget Examples
200
+ async getWidgetExamples() {
201
+ const response = await fetch(`${this.baseUrl}/api/widget-examples`);
202
+ return response.json();
203
+ }
204
+ }
205
+
206
+ export const api = new StudioAPI();
207
+