nitrostack 1.0.1 → 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 +15 -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
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
// MCP Client for Studio
|
|
2
|
+
// Communicates with MCP server via stdio
|
|
3
|
+
|
|
4
|
+
import { spawn, ChildProcess } from 'child_process';
|
|
5
|
+
import { Client } from '@modelcontextprotocol/sdk/client/index.js';
|
|
6
|
+
import { StdioClientTransport } from '@modelcontextprotocol/sdk/client/stdio.js';
|
|
7
|
+
|
|
8
|
+
export interface McpClientConfig {
|
|
9
|
+
command: string;
|
|
10
|
+
args: string[];
|
|
11
|
+
env?: Record<string, string>;
|
|
12
|
+
cwd?: string;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export class McpClient {
|
|
16
|
+
private client: Client | null = null;
|
|
17
|
+
private transport: StdioClientTransport | null = null;
|
|
18
|
+
|
|
19
|
+
async connect(config: McpClientConfig): Promise<void> {
|
|
20
|
+
if (this.client && this.transport) {
|
|
21
|
+
console.log('⚠️ Already connected, reusing existing connection');
|
|
22
|
+
return;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// Clean up any existing connections
|
|
26
|
+
if (this.client || this.transport) {
|
|
27
|
+
console.log('🧹 Cleaning up stale connection...');
|
|
28
|
+
try {
|
|
29
|
+
if (this.client) await this.client.close();
|
|
30
|
+
if (this.transport) await this.transport.close();
|
|
31
|
+
} catch (e) {
|
|
32
|
+
// Ignore cleanup errors
|
|
33
|
+
}
|
|
34
|
+
this.client = null;
|
|
35
|
+
this.transport = null;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
console.log('🚀 Connecting to MCP server:', config.command, config.args);
|
|
39
|
+
console.log('📝 Environment vars:', Object.keys(config.env || {}).join(', '));
|
|
40
|
+
|
|
41
|
+
// Create transport (it will spawn the process internally)
|
|
42
|
+
this.transport = new StdioClientTransport({
|
|
43
|
+
command: config.command,
|
|
44
|
+
args: config.args,
|
|
45
|
+
env: { ...process.env, ...config.env },
|
|
46
|
+
cwd: config.cwd, // Set working directory for the child process
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
// Create client
|
|
50
|
+
this.client = new Client(
|
|
51
|
+
{
|
|
52
|
+
name: 'nitrostack-studio',
|
|
53
|
+
version: '3.1.0',
|
|
54
|
+
},
|
|
55
|
+
{
|
|
56
|
+
capabilities: {
|
|
57
|
+
sampling: {},
|
|
58
|
+
roots: { listChanged: true },
|
|
59
|
+
},
|
|
60
|
+
}
|
|
61
|
+
);
|
|
62
|
+
|
|
63
|
+
// Handle connection errors
|
|
64
|
+
this.client.onerror = (error) => {
|
|
65
|
+
console.error('❌ MCP Client error:', error);
|
|
66
|
+
this.client = null;
|
|
67
|
+
this.transport = null;
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
this.client.onclose = () => {
|
|
71
|
+
console.log('🔌 MCP Client connection closed');
|
|
72
|
+
this.client = null;
|
|
73
|
+
this.transport = null;
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
// Connect
|
|
77
|
+
try {
|
|
78
|
+
await this.client.connect(this.transport);
|
|
79
|
+
console.log('✅ MCP Client connected and ready');
|
|
80
|
+
} catch (error) {
|
|
81
|
+
console.error('❌ Failed to connect MCP client:', error);
|
|
82
|
+
this.client = null;
|
|
83
|
+
this.transport = null;
|
|
84
|
+
throw error;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
async disconnect(): Promise<void> {
|
|
89
|
+
console.log('🛑 Disconnecting MCP client...');
|
|
90
|
+
|
|
91
|
+
if (this.client) {
|
|
92
|
+
await this.client.close();
|
|
93
|
+
this.client = null;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
if (this.transport) {
|
|
97
|
+
await this.transport.close();
|
|
98
|
+
this.transport = null;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
isConnected(): boolean {
|
|
103
|
+
return this.client !== null;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
async listTools() {
|
|
107
|
+
if (!this.client) throw new Error('Not connected');
|
|
108
|
+
return await this.client.listTools();
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
async callTool(name: string, args: any) {
|
|
112
|
+
if (!this.client) throw new Error('Not connected');
|
|
113
|
+
return await this.client.callTool({ name, arguments: args });
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
async listResources() {
|
|
117
|
+
if (!this.client) throw new Error('Not connected');
|
|
118
|
+
return await this.client.listResources();
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
async readResource(uri: string) {
|
|
122
|
+
if (!this.client) throw new Error('Not connected');
|
|
123
|
+
return await this.client.readResource({ uri });
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
async listPrompts() {
|
|
127
|
+
if (!this.client) throw new Error('Not connected');
|
|
128
|
+
return await this.client.listPrompts();
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
async getPrompt(name: string, args: any) {
|
|
132
|
+
if (!this.client) throw new Error('Not connected');
|
|
133
|
+
return await this.client.getPrompt({ name, arguments: args });
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
async ping() {
|
|
137
|
+
if (!this.client) throw new Error('Not connected');
|
|
138
|
+
return await this.client.ping();
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
async listRoots() {
|
|
142
|
+
if (!this.client) throw new Error('Not connected');
|
|
143
|
+
return await this.client.listRoots();
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
async executeTool(name: string, args: any) {
|
|
147
|
+
return await this.callTool(name, args);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
async createCompletion(params: any) {
|
|
151
|
+
if (!this.client) throw new Error('Not connected');
|
|
152
|
+
return await this.client.createCompletion(params);
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// Global singleton that persists across HMR
|
|
157
|
+
declare global {
|
|
158
|
+
var __mcpClient: McpClient | undefined;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
export function getMcpClient(): McpClient {
|
|
162
|
+
if (!global.__mcpClient) {
|
|
163
|
+
console.log('🆕 Creating new MCP client instance');
|
|
164
|
+
global.__mcpClient = new McpClient();
|
|
165
|
+
}
|
|
166
|
+
return global.__mcpClient;
|
|
167
|
+
}
|
|
168
|
+
|
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
import { create } from 'zustand';
|
|
2
|
+
import type {
|
|
3
|
+
Tool,
|
|
4
|
+
Resource,
|
|
5
|
+
Prompt,
|
|
6
|
+
ChatMessage,
|
|
7
|
+
ConnectionStatus,
|
|
8
|
+
TabType,
|
|
9
|
+
PingResult,
|
|
10
|
+
HealthCheck,
|
|
11
|
+
OAuthState,
|
|
12
|
+
} from './types';
|
|
13
|
+
|
|
14
|
+
interface StudioState {
|
|
15
|
+
// Connection
|
|
16
|
+
connection: ConnectionStatus;
|
|
17
|
+
setConnection: (status: ConnectionStatus) => void;
|
|
18
|
+
|
|
19
|
+
// Navigation
|
|
20
|
+
currentTab: TabType;
|
|
21
|
+
setCurrentTab: (tab: TabType) => void;
|
|
22
|
+
|
|
23
|
+
// Data
|
|
24
|
+
tools: Tool[];
|
|
25
|
+
setTools: (tools: Tool[]) => void;
|
|
26
|
+
|
|
27
|
+
resources: Resource[];
|
|
28
|
+
setResources: (resources: Resource[]) => void;
|
|
29
|
+
|
|
30
|
+
prompts: Prompt[];
|
|
31
|
+
setPrompts: (prompts: Prompt[]) => void;
|
|
32
|
+
|
|
33
|
+
// Chat
|
|
34
|
+
chatMessages: ChatMessage[];
|
|
35
|
+
addChatMessage: (message: ChatMessage) => void;
|
|
36
|
+
clearChat: () => void;
|
|
37
|
+
currentProvider: 'openai' | 'gemini';
|
|
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;
|
|
41
|
+
|
|
42
|
+
// Auth
|
|
43
|
+
jwtToken: string | null;
|
|
44
|
+
setJwtToken: (token: string | null) => void;
|
|
45
|
+
|
|
46
|
+
apiKey: string | null;
|
|
47
|
+
setApiKey: (key: string | null) => void;
|
|
48
|
+
|
|
49
|
+
oauthState: OAuthState;
|
|
50
|
+
setOAuthState: (state: Partial<OAuthState>) => void;
|
|
51
|
+
|
|
52
|
+
// Ping
|
|
53
|
+
pingHistory: PingResult[];
|
|
54
|
+
addPingResult: (result: PingResult) => void;
|
|
55
|
+
|
|
56
|
+
// Health
|
|
57
|
+
healthChecks: HealthCheck[];
|
|
58
|
+
setHealthChecks: (checks: HealthCheck[]) => void;
|
|
59
|
+
|
|
60
|
+
// Modal
|
|
61
|
+
enlargeModal: {
|
|
62
|
+
open: boolean;
|
|
63
|
+
type: 'tool' | 'resource' | null;
|
|
64
|
+
item: any;
|
|
65
|
+
};
|
|
66
|
+
openEnlargeModal: (type: 'tool' | 'resource', item: any) => void;
|
|
67
|
+
closeEnlargeModal: () => void;
|
|
68
|
+
|
|
69
|
+
// Loading states
|
|
70
|
+
loading: {
|
|
71
|
+
tools: boolean;
|
|
72
|
+
resources: boolean;
|
|
73
|
+
prompts: boolean;
|
|
74
|
+
chat: boolean;
|
|
75
|
+
};
|
|
76
|
+
setLoading: (key: keyof StudioState['loading'], value: boolean) => void;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export const useStudioStore = create<StudioState>((set) => ({
|
|
80
|
+
// Connection
|
|
81
|
+
connection: { connected: false, status: 'connecting' },
|
|
82
|
+
setConnection: (connection) => set({ connection }),
|
|
83
|
+
|
|
84
|
+
// Navigation
|
|
85
|
+
currentTab: 'tools',
|
|
86
|
+
setCurrentTab: (currentTab) => set({ currentTab }),
|
|
87
|
+
|
|
88
|
+
// Data
|
|
89
|
+
tools: [],
|
|
90
|
+
setTools: (tools) => set({ tools }),
|
|
91
|
+
|
|
92
|
+
resources: [],
|
|
93
|
+
setResources: (resources) => set({ resources }),
|
|
94
|
+
|
|
95
|
+
prompts: [],
|
|
96
|
+
setPrompts: (prompts) => set({ prompts }),
|
|
97
|
+
|
|
98
|
+
// Chat
|
|
99
|
+
chatMessages: [],
|
|
100
|
+
addChatMessage: (message) =>
|
|
101
|
+
set((state) => ({ chatMessages: [...state.chatMessages, message] })),
|
|
102
|
+
clearChat: () => set({ chatMessages: [] }),
|
|
103
|
+
currentProvider: 'gemini',
|
|
104
|
+
setCurrentProvider: (currentProvider) => set({ currentProvider }),
|
|
105
|
+
currentImage: null,
|
|
106
|
+
setCurrentImage: (currentImage) => set({ currentImage }),
|
|
107
|
+
|
|
108
|
+
// Auth
|
|
109
|
+
jwtToken: typeof window !== 'undefined' ? localStorage.getItem('mcp_jwt_token') : null,
|
|
110
|
+
setJwtToken: (jwtToken) => {
|
|
111
|
+
if (typeof window !== 'undefined') {
|
|
112
|
+
if (jwtToken) {
|
|
113
|
+
localStorage.setItem('mcp_jwt_token', jwtToken);
|
|
114
|
+
} else {
|
|
115
|
+
localStorage.removeItem('mcp_jwt_token');
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
set({ jwtToken });
|
|
119
|
+
},
|
|
120
|
+
|
|
121
|
+
apiKey: typeof window !== 'undefined' ? localStorage.getItem('mcp_api_key') : null,
|
|
122
|
+
setApiKey: (apiKey) => {
|
|
123
|
+
if (typeof window !== 'undefined') {
|
|
124
|
+
if (apiKey) {
|
|
125
|
+
localStorage.setItem('mcp_api_key', apiKey);
|
|
126
|
+
} else {
|
|
127
|
+
localStorage.removeItem('mcp_api_key');
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
set({ apiKey });
|
|
131
|
+
},
|
|
132
|
+
|
|
133
|
+
oauthState: typeof window !== 'undefined'
|
|
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
|
+
}
|
|
142
|
+
: {
|
|
143
|
+
authServerUrl: null,
|
|
144
|
+
resourceMetadata: null,
|
|
145
|
+
authServerMetadata: null,
|
|
146
|
+
clientRegistration: null,
|
|
147
|
+
selectedScopes: [],
|
|
148
|
+
currentToken: null,
|
|
149
|
+
},
|
|
150
|
+
setOAuthState: (newState) => {
|
|
151
|
+
const updatedState = (state: any) => {
|
|
152
|
+
const newOAuthState = { ...state.oauthState, ...newState };
|
|
153
|
+
|
|
154
|
+
// Persist to localStorage
|
|
155
|
+
if (typeof window !== 'undefined') {
|
|
156
|
+
localStorage.setItem('mcp_oauth_state', JSON.stringify(newOAuthState));
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
return { oauthState: newOAuthState };
|
|
160
|
+
};
|
|
161
|
+
set(updatedState);
|
|
162
|
+
},
|
|
163
|
+
|
|
164
|
+
// Ping
|
|
165
|
+
pingHistory: [],
|
|
166
|
+
addPingResult: (result) =>
|
|
167
|
+
set((state) => ({
|
|
168
|
+
pingHistory: [result, ...state.pingHistory].slice(0, 10),
|
|
169
|
+
})),
|
|
170
|
+
|
|
171
|
+
// Health
|
|
172
|
+
healthChecks: [],
|
|
173
|
+
setHealthChecks: (healthChecks) => set({ healthChecks }),
|
|
174
|
+
|
|
175
|
+
// Modal
|
|
176
|
+
enlargeModal: { open: false, type: null, item: null },
|
|
177
|
+
openEnlargeModal: (type, item) =>
|
|
178
|
+
set({ enlargeModal: { open: true, type, item } }),
|
|
179
|
+
closeEnlargeModal: () =>
|
|
180
|
+
set({ enlargeModal: { open: false, type: null, item: null } }),
|
|
181
|
+
|
|
182
|
+
// Loading
|
|
183
|
+
loading: {
|
|
184
|
+
tools: false,
|
|
185
|
+
resources: false,
|
|
186
|
+
prompts: false,
|
|
187
|
+
chat: false,
|
|
188
|
+
},
|
|
189
|
+
setLoading: (key, value) =>
|
|
190
|
+
set((state) => ({ loading: { ...state.loading, [key]: value } })),
|
|
191
|
+
}));
|
|
192
|
+
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { createContext, useContext, useEffect, useState } from 'react';
|
|
4
|
+
|
|
5
|
+
type Theme = 'dark' | 'light';
|
|
6
|
+
|
|
7
|
+
type ThemeContextType = {
|
|
8
|
+
theme: Theme;
|
|
9
|
+
toggleTheme: () => void;
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
const ThemeContext = createContext<ThemeContextType | undefined>(undefined);
|
|
13
|
+
|
|
14
|
+
export function ThemeProvider({ children }: { children: React.ReactNode }) {
|
|
15
|
+
const [theme, setTheme] = useState<Theme>('dark');
|
|
16
|
+
|
|
17
|
+
useEffect(() => {
|
|
18
|
+
// Load theme from localStorage
|
|
19
|
+
const stored = localStorage.getItem('theme') as Theme;
|
|
20
|
+
if (stored) {
|
|
21
|
+
setTheme(stored);
|
|
22
|
+
document.documentElement.classList.toggle('dark', stored === 'dark');
|
|
23
|
+
} else {
|
|
24
|
+
// Default to dark theme
|
|
25
|
+
document.documentElement.classList.add('dark');
|
|
26
|
+
}
|
|
27
|
+
}, []);
|
|
28
|
+
|
|
29
|
+
const toggleTheme = () => {
|
|
30
|
+
const newTheme = theme === 'dark' ? 'light' : 'dark';
|
|
31
|
+
setTheme(newTheme);
|
|
32
|
+
localStorage.setItem('theme', newTheme);
|
|
33
|
+
document.documentElement.classList.toggle('dark', newTheme === 'dark');
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
return (
|
|
37
|
+
<ThemeContext.Provider value={{ theme, toggleTheme }}>
|
|
38
|
+
{children}
|
|
39
|
+
</ThemeContext.Provider>
|
|
40
|
+
);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export function useTheme() {
|
|
44
|
+
const context = useContext(ThemeContext);
|
|
45
|
+
if (!context) {
|
|
46
|
+
throw new Error('useTheme must be used within ThemeProvider');
|
|
47
|
+
}
|
|
48
|
+
return context;
|
|
49
|
+
}
|
|
50
|
+
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
// NitroStack Studio Type Definitions
|
|
2
|
+
|
|
3
|
+
export interface Tool {
|
|
4
|
+
name: string;
|
|
5
|
+
description?: string;
|
|
6
|
+
inputSchema?: {
|
|
7
|
+
type: string;
|
|
8
|
+
properties?: Record<string, any>;
|
|
9
|
+
required?: string[];
|
|
10
|
+
};
|
|
11
|
+
examples?: {
|
|
12
|
+
request?: any;
|
|
13
|
+
response?: any;
|
|
14
|
+
};
|
|
15
|
+
widget?: {
|
|
16
|
+
route: string;
|
|
17
|
+
};
|
|
18
|
+
outputTemplate?: string;
|
|
19
|
+
_meta?: {
|
|
20
|
+
'openai/outputTemplate'?: string;
|
|
21
|
+
'ui/template'?: string;
|
|
22
|
+
[key: string]: any;
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export interface Resource {
|
|
27
|
+
name: string;
|
|
28
|
+
uri: string;
|
|
29
|
+
description?: string;
|
|
30
|
+
mimeType?: string;
|
|
31
|
+
examples?: {
|
|
32
|
+
response?: any;
|
|
33
|
+
};
|
|
34
|
+
_meta?: Record<string, any>;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export interface Prompt {
|
|
38
|
+
name: string;
|
|
39
|
+
description?: string;
|
|
40
|
+
arguments?: Array<{
|
|
41
|
+
name: string;
|
|
42
|
+
description?: string;
|
|
43
|
+
required?: boolean;
|
|
44
|
+
}>;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export interface Root {
|
|
48
|
+
uri: string;
|
|
49
|
+
name?: string;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export interface ChatMessage {
|
|
53
|
+
role: 'user' | 'assistant' | 'tool' | 'system';
|
|
54
|
+
content: string;
|
|
55
|
+
toolCalls?: ToolCall[];
|
|
56
|
+
toolCallId?: string;
|
|
57
|
+
image?: {
|
|
58
|
+
data: string;
|
|
59
|
+
type: string;
|
|
60
|
+
name: string;
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export interface ToolCall {
|
|
65
|
+
id: string;
|
|
66
|
+
name: string;
|
|
67
|
+
arguments: any;
|
|
68
|
+
result?: any;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export interface PingResult {
|
|
72
|
+
time: Date;
|
|
73
|
+
latency: number;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export interface ConnectionStatus {
|
|
77
|
+
connected: boolean;
|
|
78
|
+
status: 'connected' | 'connecting' | 'disconnected';
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export interface HealthCheck {
|
|
82
|
+
name: string;
|
|
83
|
+
status: 'up' | 'down' | 'degraded';
|
|
84
|
+
lastCheck: Date;
|
|
85
|
+
message?: string;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export interface OAuthState {
|
|
89
|
+
authServerUrl: string | null;
|
|
90
|
+
resourceMetadata: any;
|
|
91
|
+
authServerMetadata: any;
|
|
92
|
+
clientRegistration: any;
|
|
93
|
+
selectedScopes: string[];
|
|
94
|
+
currentToken: any;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
export type TabType =
|
|
98
|
+
| 'tools'
|
|
99
|
+
| 'chat'
|
|
100
|
+
| 'resources'
|
|
101
|
+
| 'prompts'
|
|
102
|
+
| 'auth'
|
|
103
|
+
| 'health'
|
|
104
|
+
| 'ping'
|
|
105
|
+
| 'sampling'
|
|
106
|
+
| 'roots';
|
|
107
|
+
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
// Widget Loader - Handles dev/prod widget loading
|
|
2
|
+
|
|
3
|
+
export function getWidgetUrl(uri: string): string {
|
|
4
|
+
// Check if we're in dev mode (widget dev server on 3001)
|
|
5
|
+
const isDevMode =
|
|
6
|
+
typeof window !== 'undefined' &&
|
|
7
|
+
(window.location.hostname === 'localhost' || window.location.hostname === '127.0.0.1');
|
|
8
|
+
|
|
9
|
+
if (isDevMode && uri.startsWith('/widgets/')) {
|
|
10
|
+
// Dev mode: load from Next.js dev server
|
|
11
|
+
const widgetPath = uri.replace('/widgets/', '');
|
|
12
|
+
return `http://localhost:3001/${widgetPath}`;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
// Production mode: load from same origin
|
|
16
|
+
return uri;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function isDevWidget(uri: string): boolean {
|
|
20
|
+
return uri.startsWith('/widgets/') || uri.includes('component://');
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export async function loadWidget(uri: string): Promise<{ html: string; isDevMode: boolean }> {
|
|
24
|
+
const isDevMode =
|
|
25
|
+
typeof window !== 'undefined' &&
|
|
26
|
+
(window.location.hostname === 'localhost' || window.location.hostname === '127.0.0.1');
|
|
27
|
+
|
|
28
|
+
if (isDevMode && uri.startsWith('/widgets/')) {
|
|
29
|
+
// Dev mode: return URL for iframe
|
|
30
|
+
return { html: getWidgetUrl(uri), isDevMode: true };
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// Production mode: fetch widget HTML
|
|
34
|
+
try {
|
|
35
|
+
const response = await fetch(`/api/resources/${encodeURIComponent(uri)}`);
|
|
36
|
+
const data = await response.json();
|
|
37
|
+
|
|
38
|
+
if (data.contents && data.contents.length > 0) {
|
|
39
|
+
return { html: data.contents[0].text || '', isDevMode: false };
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
return { html: '', isDevMode: false };
|
|
43
|
+
} catch (error) {
|
|
44
|
+
console.error('Failed to load widget:', error);
|
|
45
|
+
return { html: '', isDevMode: false };
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export function createWidgetHTML(html: string, data: any): string {
|
|
50
|
+
const htmlParts = [
|
|
51
|
+
'<!DOCTYPE html>',
|
|
52
|
+
'<html>',
|
|
53
|
+
'<head>',
|
|
54
|
+
'<meta charset="UTF-8">',
|
|
55
|
+
'<meta name="viewport" content="width=device-width, initial-scale=1.0">',
|
|
56
|
+
'<style>',
|
|
57
|
+
'html, body { margin: 0; padding: 0; background: transparent; }',
|
|
58
|
+
'body { font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif; }',
|
|
59
|
+
'</style>',
|
|
60
|
+
'</head>',
|
|
61
|
+
'<body>',
|
|
62
|
+
'<script>',
|
|
63
|
+
'window.openai = ' + JSON.stringify({ toolOutput: data }) + ';',
|
|
64
|
+
'</script>',
|
|
65
|
+
html,
|
|
66
|
+
'</body>',
|
|
67
|
+
'</html>',
|
|
68
|
+
];
|
|
69
|
+
|
|
70
|
+
return htmlParts.join('\n');
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export function postMessageToWidget(iframe: HTMLIFrameElement, data: any, delay: number = 500) {
|
|
74
|
+
iframe.onload = () => {
|
|
75
|
+
setTimeout(() => {
|
|
76
|
+
try {
|
|
77
|
+
iframe.contentWindow?.postMessage(
|
|
78
|
+
{
|
|
79
|
+
type: 'toolOutput',
|
|
80
|
+
data,
|
|
81
|
+
},
|
|
82
|
+
'*'
|
|
83
|
+
);
|
|
84
|
+
} catch (e) {
|
|
85
|
+
console.error('Failed to post message to widget:', e);
|
|
86
|
+
}
|
|
87
|
+
}, delay);
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
// Studio Middleware - Widget Proxying
|
|
2
|
+
import { NextResponse } from 'next/server';
|
|
3
|
+
import type { NextRequest } from 'next/server';
|
|
4
|
+
|
|
5
|
+
export function middleware(request: NextRequest) {
|
|
6
|
+
const { pathname } = request.nextUrl;
|
|
7
|
+
|
|
8
|
+
// In development, proxy widget requests to the widget dev server
|
|
9
|
+
if (process.env.NODE_ENV === 'development' && process.env.WIDGETS_DEV_MODE === 'true') {
|
|
10
|
+
const widgetsDevPort = process.env.WIDGETS_DEV_PORT || '3001';
|
|
11
|
+
|
|
12
|
+
// Proxy /widgets/* to widget dev server
|
|
13
|
+
if (pathname.startsWith('/widgets/')) {
|
|
14
|
+
const widgetPath = pathname.replace('/widgets/', '');
|
|
15
|
+
const targetUrl = `http://localhost:${widgetsDevPort}/${widgetPath}`;
|
|
16
|
+
|
|
17
|
+
return NextResponse.rewrite(new URL(targetUrl));
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
return NextResponse.next();
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export const config = {
|
|
25
|
+
matcher: ['/widgets/:path*'],
|
|
26
|
+
};
|
|
27
|
+
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/** @type {import('next').NextConfig} */
|
|
2
|
+
const nextConfig = {
|
|
3
|
+
// No static export in dev mode - we need API routes and middleware
|
|
4
|
+
images: {
|
|
5
|
+
unoptimized: true,
|
|
6
|
+
},
|
|
7
|
+
typescript: {
|
|
8
|
+
ignoreBuildErrors: true,
|
|
9
|
+
},
|
|
10
|
+
eslint: {
|
|
11
|
+
ignoreDuringBuilds: true,
|
|
12
|
+
},
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
export default nextConfig;
|
|
16
|
+
|