nitrostack 1.0.0 → 1.0.2
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/CHANGELOG.md +30 -0
- package/dist/cli/index.js +4 -1
- package/dist/cli/index.js.map +1 -1
- package/package.json +1 -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 +123 -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 +85 -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 +160 -0
- package/src/studio/app/auth/page.tsx +543 -0
- package/src/studio/app/chat/page.tsx +530 -0
- package/src/studio/app/chat/page.tsx.backup +390 -0
- package/src/studio/app/globals.css +410 -0
- package/src/studio/app/health/page.tsx +177 -0
- package/src/studio/app/layout.tsx +48 -0
- package/src/studio/app/page.tsx +337 -0
- package/src/studio/app/page.tsx.backup +346 -0
- package/src/studio/app/ping/page.tsx +204 -0
- package/src/studio/app/prompts/page.tsx +228 -0
- package/src/studio/app/resources/page.tsx +313 -0
- package/src/studio/components/EnlargeModal.tsx +116 -0
- package/src/studio/components/Sidebar.tsx +133 -0
- package/src/studio/components/ToolCard.tsx +108 -0
- package/src/studio/components/WidgetRenderer.tsx +99 -0
- package/src/studio/lib/api.ts +207 -0
- package/src/studio/lib/llm-service.ts +361 -0
- package/src/studio/lib/mcp-client.ts +168 -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 +16 -0
- package/src/studio/package-lock.json +2696 -0
- package/src/studio/package.json +34 -0
- package/src/studio/postcss.config.mjs +10 -0
- package/src/studio/tailwind.config.ts +67 -0
- package/src/studio/tsconfig.json +41 -0
- package/templates/typescript-auth/.env.example +23 -0
- package/templates/typescript-auth/src/app.module.ts +103 -0
- package/templates/typescript-auth/src/db/database.ts +163 -0
- package/templates/typescript-auth/src/db/seed.ts +374 -0
- package/templates/typescript-auth/src/db/setup.ts +87 -0
- package/templates/typescript-auth/src/events/analytics.service.ts +52 -0
- package/templates/typescript-auth/src/events/notification.service.ts +40 -0
- package/templates/typescript-auth/src/filters/global-exception.filter.ts +28 -0
- package/templates/typescript-auth/src/guards/README.md +75 -0
- package/templates/typescript-auth/src/guards/jwt.guard.ts +105 -0
- package/templates/typescript-auth/src/health/database.health.ts +41 -0
- package/templates/typescript-auth/src/index.ts +26 -0
- package/templates/typescript-auth/src/interceptors/transform.interceptor.ts +24 -0
- package/templates/typescript-auth/src/middleware/logging.middleware.ts +42 -0
- package/templates/typescript-auth/src/modules/addresses/addresses.module.ts +16 -0
- package/templates/typescript-auth/src/modules/addresses/addresses.prompts.ts +114 -0
- package/templates/typescript-auth/src/modules/addresses/addresses.resources.ts +40 -0
- package/templates/typescript-auth/src/modules/addresses/addresses.tools.ts +241 -0
- package/templates/typescript-auth/src/modules/auth/auth.module.ts +16 -0
- package/templates/typescript-auth/src/modules/auth/auth.prompts.ts +147 -0
- package/templates/typescript-auth/src/modules/auth/auth.resources.ts +84 -0
- package/templates/typescript-auth/src/modules/auth/auth.tools.ts +139 -0
- package/templates/typescript-auth/src/modules/cart/cart.module.ts +16 -0
- package/templates/typescript-auth/src/modules/cart/cart.prompts.ts +95 -0
- package/templates/typescript-auth/src/modules/cart/cart.resources.ts +44 -0
- package/templates/typescript-auth/src/modules/cart/cart.tools.ts +281 -0
- package/templates/typescript-auth/src/modules/orders/orders.module.ts +16 -0
- package/templates/typescript-auth/src/modules/orders/orders.prompts.ts +88 -0
- package/templates/typescript-auth/src/modules/orders/orders.resources.ts +48 -0
- package/templates/typescript-auth/src/modules/orders/orders.tools.ts +281 -0
- package/templates/typescript-auth/src/modules/products/products.module.ts +16 -0
- package/templates/typescript-auth/src/modules/products/products.prompts.ts +146 -0
- package/templates/typescript-auth/src/modules/products/products.resources.ts +98 -0
- package/templates/typescript-auth/src/modules/products/products.tools.ts +266 -0
- package/templates/typescript-auth/src/pipes/validation.pipe.ts +42 -0
- package/templates/typescript-auth/src/services/database.service.ts +90 -0
- package/templates/typescript-auth/src/widgets/app/add-to-cart/page.tsx +122 -0
- package/templates/typescript-auth/src/widgets/app/address-added/page.tsx +116 -0
- package/templates/typescript-auth/src/widgets/app/address-deleted/page.tsx +105 -0
- package/templates/typescript-auth/src/widgets/app/address-list/page.tsx +139 -0
- package/templates/typescript-auth/src/widgets/app/address-updated/page.tsx +153 -0
- package/templates/typescript-auth/src/widgets/app/cart-cleared/page.tsx +86 -0
- package/templates/typescript-auth/src/widgets/app/cart-updated/page.tsx +116 -0
- package/templates/typescript-auth/src/widgets/app/categories/page.tsx +134 -0
- package/templates/typescript-auth/src/widgets/app/layout.tsx +21 -0
- package/templates/typescript-auth/src/widgets/app/login-result/page.tsx +129 -0
- package/templates/typescript-auth/src/widgets/app/order-confirmation/page.tsx +206 -0
- package/templates/typescript-auth/src/widgets/app/order-details/page.tsx +225 -0
- package/templates/typescript-auth/src/widgets/app/order-history/page.tsx +218 -0
- package/templates/typescript-auth/src/widgets/app/product-card/page.tsx +121 -0
- package/templates/typescript-auth/src/widgets/app/products-grid/page.tsx +173 -0
- package/templates/typescript-auth/src/widgets/app/shopping-cart/page.tsx +187 -0
- package/templates/typescript-auth/src/widgets/app/whoami/page.tsx +165 -0
- package/templates/typescript-auth/src/widgets/next.config.js +38 -0
- package/templates/typescript-auth/src/widgets/package.json +18 -0
- package/templates/typescript-auth/src/widgets/styles/ecommerce.ts +169 -0
- package/templates/typescript-auth/src/widgets/tsconfig.json +28 -0
- package/templates/typescript-auth/src/widgets/types/tool-data.ts +141 -0
- package/templates/typescript-auth/src/widgets/widget-manifest.json +464 -0
- package/templates/typescript-auth/tsconfig.json +27 -0
- package/templates/typescript-auth-api-key/.env +15 -0
- package/templates/typescript-auth-api-key/.env.example +4 -0
- package/templates/typescript-auth-api-key/src/app.module.ts +38 -0
- package/templates/typescript-auth-api-key/src/guards/apikey.guard.ts +47 -0
- package/templates/typescript-auth-api-key/src/guards/multi-auth.guard.ts +157 -0
- package/templates/typescript-auth-api-key/src/health/system.health.ts +55 -0
- package/templates/typescript-auth-api-key/src/index.ts +47 -0
- package/templates/typescript-auth-api-key/src/modules/calculator/calculator.module.ts +12 -0
- package/templates/typescript-auth-api-key/src/modules/calculator/calculator.prompts.ts +73 -0
- package/templates/typescript-auth-api-key/src/modules/calculator/calculator.resources.ts +60 -0
- package/templates/typescript-auth-api-key/src/modules/calculator/calculator.tools.ts +71 -0
- package/templates/typescript-auth-api-key/src/modules/demo/demo.module.ts +18 -0
- package/templates/typescript-auth-api-key/src/modules/demo/demo.tools.ts +155 -0
- package/templates/typescript-auth-api-key/src/modules/demo/multi-auth.tools.ts +123 -0
- package/templates/typescript-auth-api-key/src/widgets/app/calculator-operations/page.tsx +133 -0
- package/templates/typescript-auth-api-key/src/widgets/app/calculator-result/page.tsx +134 -0
- package/templates/typescript-auth-api-key/src/widgets/app/layout.tsx +14 -0
- package/templates/typescript-auth-api-key/src/widgets/next.config.js +37 -0
- package/templates/typescript-auth-api-key/src/widgets/package.json +24 -0
- package/templates/typescript-auth-api-key/src/widgets/tsconfig.json +28 -0
- package/templates/typescript-auth-api-key/src/widgets/widget-manifest.json +48 -0
- package/templates/typescript-auth-api-key/tsconfig.json +23 -0
- package/templates/typescript-oauth/.env.example +91 -0
- package/templates/typescript-oauth/src/app.module.ts +89 -0
- package/templates/typescript-oauth/src/guards/oauth.guard.ts +127 -0
- package/templates/typescript-oauth/src/index.ts +74 -0
- package/templates/typescript-oauth/src/modules/demo/demo.module.ts +16 -0
- package/templates/typescript-oauth/src/modules/demo/demo.tools.ts +190 -0
- package/templates/typescript-oauth/src/widgets/app/calculator-operations/page.tsx +133 -0
- package/templates/typescript-oauth/src/widgets/app/calculator-result/page.tsx +134 -0
- package/templates/typescript-oauth/src/widgets/app/layout.tsx +14 -0
- package/templates/typescript-oauth/src/widgets/next.config.js +37 -0
- package/templates/typescript-oauth/src/widgets/package.json +24 -0
- package/templates/typescript-oauth/src/widgets/tsconfig.json +28 -0
- package/templates/typescript-oauth/src/widgets/widget-manifest.json +48 -0
- package/templates/typescript-oauth/tsconfig.json +23 -0
- package/templates/typescript-starter/.env.example +4 -0
- package/templates/typescript-starter/src/app.module.ts +34 -0
- package/templates/typescript-starter/src/health/system.health.ts +55 -0
- package/templates/typescript-starter/src/index.ts +27 -0
- package/templates/typescript-starter/src/modules/calculator/calculator.module.ts +12 -0
- package/templates/typescript-starter/src/modules/calculator/calculator.prompts.ts +73 -0
- package/templates/typescript-starter/src/modules/calculator/calculator.resources.ts +60 -0
- package/templates/typescript-starter/src/modules/calculator/calculator.tools.ts +71 -0
- package/templates/typescript-starter/src/widgets/app/calculator-operations/page.tsx +133 -0
- package/templates/typescript-starter/src/widgets/app/calculator-result/page.tsx +134 -0
- package/templates/typescript-starter/src/widgets/app/layout.tsx +14 -0
- package/templates/typescript-starter/src/widgets/next.config.js +37 -0
- package/templates/typescript-starter/src/widgets/package.json +24 -0
- package/templates/typescript-starter/src/widgets/tsconfig.json +28 -0
- package/templates/typescript-starter/src/widgets/widget-manifest.json +48 -0
- package/templates/typescript-starter/tsconfig.json +23 -0
- package/LICENSE_URLS_UPDATE_COMPLETE.md +0 -388
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { useEffect, useRef } 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
|
+
|
|
15
|
+
// Check if we're in dev mode (localhost)
|
|
16
|
+
const isDevMode =
|
|
17
|
+
typeof window !== 'undefined' &&
|
|
18
|
+
(window.location.hostname === 'localhost' || window.location.hostname === '127.0.0.1');
|
|
19
|
+
|
|
20
|
+
useEffect(() => {
|
|
21
|
+
if (!iframeRef.current) return;
|
|
22
|
+
|
|
23
|
+
if (isDevMode) {
|
|
24
|
+
// Dev mode: load from widget dev server (port 3001)
|
|
25
|
+
// Widget URIs are like "/calculator-result" or "calculator-result"
|
|
26
|
+
// Remove leading slash if present
|
|
27
|
+
const widgetPath = uri.startsWith('/') ? uri.substring(1) : uri;
|
|
28
|
+
const widgetUrl = `http://localhost:3001/${widgetPath}`;
|
|
29
|
+
|
|
30
|
+
console.log('Loading widget in dev mode:', { uri, widgetPath, widgetUrl, data });
|
|
31
|
+
|
|
32
|
+
// Set up onload handler BEFORE setting src
|
|
33
|
+
iframeRef.current.onload = () => {
|
|
34
|
+
console.log('Widget iframe loaded, posting data...');
|
|
35
|
+
// Post message after a short delay to ensure widget is ready
|
|
36
|
+
setTimeout(() => {
|
|
37
|
+
try {
|
|
38
|
+
iframeRef.current?.contentWindow?.postMessage(
|
|
39
|
+
{
|
|
40
|
+
type: 'toolOutput',
|
|
41
|
+
data,
|
|
42
|
+
},
|
|
43
|
+
'*'
|
|
44
|
+
);
|
|
45
|
+
console.log('✅ Data posted to widget:', data);
|
|
46
|
+
} catch (e) {
|
|
47
|
+
console.error('❌ Failed to post message to widget:', e);
|
|
48
|
+
}
|
|
49
|
+
}, 300);
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
// Set src AFTER onload handler is set
|
|
53
|
+
iframeRef.current.src = widgetUrl;
|
|
54
|
+
} else {
|
|
55
|
+
// Production mode: fetch and render
|
|
56
|
+
const loadProductionWidget = async () => {
|
|
57
|
+
try {
|
|
58
|
+
const response = await fetch(`/api/resources/${encodeURIComponent(uri)}`);
|
|
59
|
+
const result = await response.json();
|
|
60
|
+
|
|
61
|
+
if (result.contents && result.contents.length > 0) {
|
|
62
|
+
const html = result.contents[0].text || '';
|
|
63
|
+
const completeHtml = createWidgetHTML(html, data);
|
|
64
|
+
|
|
65
|
+
// Use Blob URL
|
|
66
|
+
const blob = new Blob([completeHtml], { type: 'text/html' });
|
|
67
|
+
const blobUrl = URL.createObjectURL(blob);
|
|
68
|
+
|
|
69
|
+
if (iframeRef.current) {
|
|
70
|
+
iframeRef.current.src = blobUrl;
|
|
71
|
+
iframeRef.current.onload = () => {
|
|
72
|
+
URL.revokeObjectURL(blobUrl);
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
} catch (error) {
|
|
77
|
+
console.error('Failed to load widget:', error);
|
|
78
|
+
}
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
loadProductionWidget();
|
|
82
|
+
}
|
|
83
|
+
}, [uri, data, isDevMode]);
|
|
84
|
+
|
|
85
|
+
return (
|
|
86
|
+
<iframe
|
|
87
|
+
ref={iframeRef}
|
|
88
|
+
className={className}
|
|
89
|
+
sandbox="allow-scripts allow-same-origin"
|
|
90
|
+
style={{
|
|
91
|
+
width: '100%',
|
|
92
|
+
height: '100%',
|
|
93
|
+
border: 'none',
|
|
94
|
+
background: 'transparent',
|
|
95
|
+
}}
|
|
96
|
+
/>
|
|
97
|
+
);
|
|
98
|
+
}
|
|
99
|
+
|
|
@@ -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
|
+
|
|
@@ -0,0 +1,361 @@
|
|
|
1
|
+
// LLM Service for Studio
|
|
2
|
+
// Supports OpenAI and Gemini
|
|
3
|
+
|
|
4
|
+
export type LLMProvider = 'openai' | 'gemini';
|
|
5
|
+
|
|
6
|
+
export interface ChatMessage {
|
|
7
|
+
role: 'user' | 'assistant' | 'tool' | 'system';
|
|
8
|
+
content: string;
|
|
9
|
+
toolCalls?: ToolCall[];
|
|
10
|
+
toolCallId?: string; // For tool responses - the ID of the call being responded to
|
|
11
|
+
toolName?: string; // For tool responses - the name of the tool (required by Gemini)
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export interface ToolCall {
|
|
15
|
+
id: string;
|
|
16
|
+
name: string;
|
|
17
|
+
arguments: any;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export interface ChatResponse {
|
|
21
|
+
message: ChatMessage;
|
|
22
|
+
toolCalls?: ToolCall[];
|
|
23
|
+
finishReason?: string;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export class LLMService {
|
|
27
|
+
async chat(
|
|
28
|
+
provider: LLMProvider,
|
|
29
|
+
messages: ChatMessage[],
|
|
30
|
+
tools: any[],
|
|
31
|
+
apiKey: string
|
|
32
|
+
): Promise<ChatResponse> {
|
|
33
|
+
if (provider === 'openai') {
|
|
34
|
+
return this.chatOpenAI(messages, tools, apiKey);
|
|
35
|
+
} else if (provider === 'gemini') {
|
|
36
|
+
return this.chatGemini(messages, tools, apiKey);
|
|
37
|
+
}
|
|
38
|
+
throw new Error(`Unsupported provider: ${provider}`);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
private async chatOpenAI(
|
|
42
|
+
messages: ChatMessage[],
|
|
43
|
+
tools: any[],
|
|
44
|
+
apiKey: string
|
|
45
|
+
): Promise<ChatResponse> {
|
|
46
|
+
const formattedMessages = messages.map((msg) => {
|
|
47
|
+
if (msg.role === 'tool') {
|
|
48
|
+
return {
|
|
49
|
+
role: 'tool' as const,
|
|
50
|
+
content: msg.content,
|
|
51
|
+
tool_call_id: msg.toolCallId,
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// Handle assistant messages with tool calls
|
|
56
|
+
if (msg.role === 'assistant' && msg.toolCalls && msg.toolCalls.length > 0) {
|
|
57
|
+
return {
|
|
58
|
+
role: 'assistant' as const,
|
|
59
|
+
content: msg.content || null,
|
|
60
|
+
tool_calls: msg.toolCalls.map(tc => ({
|
|
61
|
+
id: tc.id,
|
|
62
|
+
type: 'function' as const,
|
|
63
|
+
function: {
|
|
64
|
+
name: tc.name,
|
|
65
|
+
arguments: typeof tc.arguments === 'string' ? tc.arguments : JSON.stringify(tc.arguments),
|
|
66
|
+
},
|
|
67
|
+
})),
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
return {
|
|
72
|
+
role: msg.role,
|
|
73
|
+
content: msg.content,
|
|
74
|
+
};
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
console.log('Formatted messages for OpenAI:', JSON.stringify(formattedMessages, null, 2));
|
|
78
|
+
|
|
79
|
+
const requestBody: any = {
|
|
80
|
+
model: 'gpt-4-turbo-preview',
|
|
81
|
+
messages: formattedMessages,
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
if (tools.length > 0) {
|
|
85
|
+
requestBody.tools = tools.map((tool) => {
|
|
86
|
+
// Clean the schema for OpenAI - remove unsupported properties
|
|
87
|
+
const cleanSchema = { ...tool.inputSchema };
|
|
88
|
+
|
|
89
|
+
// Remove properties that OpenAI doesn't support
|
|
90
|
+
delete cleanSchema.$schema;
|
|
91
|
+
delete cleanSchema.additionalProperties;
|
|
92
|
+
|
|
93
|
+
// Ensure required properties for OpenAI
|
|
94
|
+
if (!cleanSchema.type) {
|
|
95
|
+
cleanSchema.type = 'object';
|
|
96
|
+
}
|
|
97
|
+
if (!cleanSchema.properties) {
|
|
98
|
+
cleanSchema.properties = {};
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
return {
|
|
102
|
+
type: 'function',
|
|
103
|
+
function: {
|
|
104
|
+
name: tool.name,
|
|
105
|
+
description: tool.description || 'No description provided',
|
|
106
|
+
parameters: cleanSchema,
|
|
107
|
+
},
|
|
108
|
+
};
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
console.log('Sending tools to OpenAI:', JSON.stringify(requestBody.tools, null, 2));
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
const response = await fetch('https://api.openai.com/v1/chat/completions', {
|
|
115
|
+
method: 'POST',
|
|
116
|
+
headers: {
|
|
117
|
+
'Content-Type': 'application/json',
|
|
118
|
+
Authorization: `Bearer ${apiKey}`,
|
|
119
|
+
},
|
|
120
|
+
body: JSON.stringify(requestBody),
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
if (!response.ok) {
|
|
124
|
+
const errorData = await response.json().catch(() => ({}));
|
|
125
|
+
console.error('OpenAI API error details:', errorData);
|
|
126
|
+
throw new Error(`OpenAI API error: ${response.statusText} - ${JSON.stringify(errorData)}`);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
const data = await response.json();
|
|
130
|
+
const choice = data.choices[0];
|
|
131
|
+
|
|
132
|
+
const result: ChatResponse = {
|
|
133
|
+
message: {
|
|
134
|
+
role: 'assistant',
|
|
135
|
+
content: choice.message.content || '',
|
|
136
|
+
},
|
|
137
|
+
finishReason: choice.finish_reason,
|
|
138
|
+
};
|
|
139
|
+
|
|
140
|
+
if (choice.message.tool_calls) {
|
|
141
|
+
result.toolCalls = choice.message.tool_calls.map((tc: any) => ({
|
|
142
|
+
id: tc.id,
|
|
143
|
+
name: tc.function?.name || '',
|
|
144
|
+
arguments: JSON.parse(tc.function?.arguments || '{}'),
|
|
145
|
+
}));
|
|
146
|
+
result.message.toolCalls = result.toolCalls;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
return result;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
private async chatGemini(
|
|
153
|
+
messages: ChatMessage[],
|
|
154
|
+
tools: any[],
|
|
155
|
+
apiKey: string
|
|
156
|
+
): Promise<ChatResponse> {
|
|
157
|
+
// Convert messages to Gemini format
|
|
158
|
+
const contents: any[] = [];
|
|
159
|
+
let systemInstruction = '';
|
|
160
|
+
|
|
161
|
+
// Group consecutive tool messages together for Gemini
|
|
162
|
+
let i = 0;
|
|
163
|
+
while (i < messages.length) {
|
|
164
|
+
const msg = messages[i];
|
|
165
|
+
|
|
166
|
+
if (msg.role === 'system') {
|
|
167
|
+
systemInstruction = msg.content;
|
|
168
|
+
i++;
|
|
169
|
+
continue;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
if (msg.role === 'tool') {
|
|
173
|
+
// Collect all consecutive tool messages and group them into ONE function response
|
|
174
|
+
const functionParts: any[] = [];
|
|
175
|
+
|
|
176
|
+
while (i < messages.length && messages[i].role === 'tool') {
|
|
177
|
+
const toolMsg = messages[i];
|
|
178
|
+
functionParts.push({
|
|
179
|
+
functionResponse: {
|
|
180
|
+
name: toolMsg.toolName || 'unknown', // Use toolName for Gemini!
|
|
181
|
+
response: {
|
|
182
|
+
content: toolMsg.content,
|
|
183
|
+
},
|
|
184
|
+
},
|
|
185
|
+
});
|
|
186
|
+
i++;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
// Add all function responses as ONE entry with multiple parts
|
|
190
|
+
contents.push({
|
|
191
|
+
role: 'function',
|
|
192
|
+
parts: functionParts,
|
|
193
|
+
});
|
|
194
|
+
} else if (msg.role === 'assistant' && msg.toolCalls && msg.toolCalls.length > 0) {
|
|
195
|
+
// Assistant message with tool calls
|
|
196
|
+
const parts: any[] = [];
|
|
197
|
+
|
|
198
|
+
if (msg.content) {
|
|
199
|
+
parts.push({ text: msg.content });
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
for (const tc of msg.toolCalls) {
|
|
203
|
+
parts.push({
|
|
204
|
+
functionCall: {
|
|
205
|
+
name: tc.name,
|
|
206
|
+
args: tc.arguments,
|
|
207
|
+
},
|
|
208
|
+
});
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
contents.push({
|
|
212
|
+
role: 'model',
|
|
213
|
+
parts,
|
|
214
|
+
});
|
|
215
|
+
i++;
|
|
216
|
+
} else {
|
|
217
|
+
// Regular user or assistant message
|
|
218
|
+
contents.push({
|
|
219
|
+
role: msg.role === 'assistant' ? 'model' : 'user',
|
|
220
|
+
parts: [{ text: msg.content }],
|
|
221
|
+
});
|
|
222
|
+
i++;
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
console.log('Formatted contents for Gemini:', JSON.stringify(contents, null, 2));
|
|
227
|
+
|
|
228
|
+
// Prepare request body
|
|
229
|
+
const requestBody: any = {
|
|
230
|
+
contents,
|
|
231
|
+
};
|
|
232
|
+
|
|
233
|
+
// Add system instruction if present
|
|
234
|
+
if (systemInstruction) {
|
|
235
|
+
requestBody.systemInstruction = {
|
|
236
|
+
parts: [{ text: systemInstruction }],
|
|
237
|
+
};
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
// Add tools if present
|
|
241
|
+
if (tools.length > 0) {
|
|
242
|
+
requestBody.tools = [{
|
|
243
|
+
functionDeclarations: tools.map((tool) => {
|
|
244
|
+
const cleanSchema = { ...tool.inputSchema };
|
|
245
|
+
|
|
246
|
+
// Remove unsupported properties
|
|
247
|
+
delete cleanSchema.$schema;
|
|
248
|
+
delete cleanSchema.additionalProperties;
|
|
249
|
+
|
|
250
|
+
// Convert to Gemini's expected format
|
|
251
|
+
const parameters: any = {
|
|
252
|
+
type: cleanSchema.type || 'OBJECT',
|
|
253
|
+
properties: {},
|
|
254
|
+
required: cleanSchema.required || [],
|
|
255
|
+
};
|
|
256
|
+
|
|
257
|
+
// Convert properties
|
|
258
|
+
if (cleanSchema.properties) {
|
|
259
|
+
for (const [key, value] of Object.entries(cleanSchema.properties)) {
|
|
260
|
+
const prop: any = value;
|
|
261
|
+
parameters.properties[key] = {
|
|
262
|
+
type: this.convertTypeToGemini(prop.type),
|
|
263
|
+
description: prop.description || '',
|
|
264
|
+
};
|
|
265
|
+
|
|
266
|
+
// Handle enums
|
|
267
|
+
if (prop.enum) {
|
|
268
|
+
parameters.properties[key].enum = prop.enum;
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
return {
|
|
274
|
+
name: tool.name,
|
|
275
|
+
description: tool.description || 'No description provided',
|
|
276
|
+
parameters,
|
|
277
|
+
};
|
|
278
|
+
}),
|
|
279
|
+
}];
|
|
280
|
+
|
|
281
|
+
console.log('Sending tools to Gemini:', JSON.stringify(requestBody.tools, null, 2));
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
// Use Gemini 2.0 Flash Experimental (latest model with function calling)
|
|
285
|
+
// The v1beta API uses 'gemini-2.0-flash-exp' for the newest features
|
|
286
|
+
const model = 'gemini-2.0-flash-exp';
|
|
287
|
+
const response = await fetch(
|
|
288
|
+
`https://generativelanguage.googleapis.com/v1beta/models/${model}:generateContent?key=${apiKey}`,
|
|
289
|
+
{
|
|
290
|
+
method: 'POST',
|
|
291
|
+
headers: { 'Content-Type': 'application/json' },
|
|
292
|
+
body: JSON.stringify(requestBody),
|
|
293
|
+
}
|
|
294
|
+
);
|
|
295
|
+
|
|
296
|
+
if (!response.ok) {
|
|
297
|
+
const errorData = await response.json().catch(() => ({}));
|
|
298
|
+
console.error('Gemini API error details:', errorData);
|
|
299
|
+
throw new Error(`Gemini API error: ${response.statusText} - ${JSON.stringify(errorData)}`);
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
const data = await response.json();
|
|
303
|
+
console.log('Gemini response:', JSON.stringify(data, null, 2));
|
|
304
|
+
|
|
305
|
+
const candidate = data.candidates?.[0];
|
|
306
|
+
if (!candidate) {
|
|
307
|
+
throw new Error('No response from Gemini');
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
const parts = candidate.content?.parts || [];
|
|
311
|
+
|
|
312
|
+
// Extract text content
|
|
313
|
+
let content = '';
|
|
314
|
+
const toolCalls: ToolCall[] = [];
|
|
315
|
+
|
|
316
|
+
for (const part of parts) {
|
|
317
|
+
if (part.text) {
|
|
318
|
+
content += part.text;
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
if (part.functionCall) {
|
|
322
|
+
toolCalls.push({
|
|
323
|
+
id: `call_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`,
|
|
324
|
+
name: part.functionCall.name,
|
|
325
|
+
arguments: part.functionCall.args || {},
|
|
326
|
+
});
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
const result: ChatResponse = {
|
|
331
|
+
message: {
|
|
332
|
+
role: 'assistant',
|
|
333
|
+
content: content || '',
|
|
334
|
+
},
|
|
335
|
+
finishReason: candidate.finishReason,
|
|
336
|
+
};
|
|
337
|
+
|
|
338
|
+
if (toolCalls.length > 0) {
|
|
339
|
+
result.toolCalls = toolCalls;
|
|
340
|
+
result.message.toolCalls = toolCalls;
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
return result;
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
private convertTypeToGemini(type?: string): string {
|
|
347
|
+
if (!type) return 'STRING';
|
|
348
|
+
|
|
349
|
+
const typeMap: Record<string, string> = {
|
|
350
|
+
'string': 'STRING',
|
|
351
|
+
'number': 'NUMBER',
|
|
352
|
+
'integer': 'INTEGER',
|
|
353
|
+
'boolean': 'BOOLEAN',
|
|
354
|
+
'array': 'ARRAY',
|
|
355
|
+
'object': 'OBJECT',
|
|
356
|
+
};
|
|
357
|
+
|
|
358
|
+
return typeMap[type.toLowerCase()] || 'STRING';
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
|