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.
- package/package.json +2 -1
- package/src/studio/README.md +140 -0
- package/src/studio/app/api/auth/fetch-metadata/route.ts +71 -0
- package/src/studio/app/api/auth/register-client/route.ts +67 -0
- package/src/studio/app/api/chat/route.ts +250 -0
- package/src/studio/app/api/health/checks/route.ts +42 -0
- package/src/studio/app/api/health/route.ts +13 -0
- package/src/studio/app/api/init/route.ts +109 -0
- package/src/studio/app/api/ping/route.ts +13 -0
- package/src/studio/app/api/prompts/[name]/route.ts +21 -0
- package/src/studio/app/api/prompts/route.ts +13 -0
- package/src/studio/app/api/resources/[...uri]/route.ts +18 -0
- package/src/studio/app/api/resources/route.ts +13 -0
- package/src/studio/app/api/roots/route.ts +13 -0
- package/src/studio/app/api/sampling/route.ts +14 -0
- package/src/studio/app/api/tools/[name]/call/route.ts +41 -0
- package/src/studio/app/api/tools/route.ts +23 -0
- package/src/studio/app/api/widget-examples/route.ts +44 -0
- package/src/studio/app/auth/callback/page.tsx +175 -0
- package/src/studio/app/auth/page.tsx +560 -0
- package/src/studio/app/chat/page.tsx +1133 -0
- package/src/studio/app/chat/page.tsx.backup +390 -0
- package/src/studio/app/globals.css +486 -0
- package/src/studio/app/health/page.tsx +179 -0
- package/src/studio/app/layout.tsx +68 -0
- package/src/studio/app/logs/page.tsx +279 -0
- package/src/studio/app/page.tsx +351 -0
- package/src/studio/app/page.tsx.backup +346 -0
- package/src/studio/app/ping/page.tsx +209 -0
- package/src/studio/app/prompts/page.tsx +230 -0
- package/src/studio/app/resources/page.tsx +315 -0
- package/src/studio/app/settings/page.tsx +199 -0
- package/src/studio/branding.md +807 -0
- package/src/studio/components/EnlargeModal.tsx +138 -0
- package/src/studio/components/LogMessage.tsx +153 -0
- package/src/studio/components/MarkdownRenderer.tsx +410 -0
- package/src/studio/components/Sidebar.tsx +295 -0
- package/src/studio/components/ToolCard.tsx +139 -0
- package/src/studio/components/WidgetRenderer.tsx +346 -0
- package/src/studio/lib/api.ts +207 -0
- package/src/studio/lib/http-client-transport.ts +222 -0
- package/src/studio/lib/llm-service.ts +480 -0
- package/src/studio/lib/log-manager.ts +76 -0
- package/src/studio/lib/mcp-client.ts +258 -0
- package/src/studio/lib/store.ts +192 -0
- package/src/studio/lib/theme-provider.tsx +50 -0
- package/src/studio/lib/types.ts +107 -0
- package/src/studio/lib/widget-loader.ts +90 -0
- package/src/studio/middleware.ts +27 -0
- package/src/studio/next.config.js +38 -0
- package/src/studio/package.json +35 -0
- package/src/studio/postcss.config.mjs +10 -0
- package/src/studio/public/nitrocloud.png +0 -0
- package/src/studio/tailwind.config.ts +67 -0
- 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
|
+
|