ai-agent-router 0.1.0
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/.claude/commands/openspec/apply.md +23 -0
- package/.claude/commands/openspec/archive.md +27 -0
- package/.claude/commands/openspec/proposal.md +28 -0
- package/.claude/settings.local.json +12 -0
- package/.claude/skills/ui-ux-pro-max/SKILL.md +228 -0
- package/.claude/skills/ui-ux-pro-max/data/charts.csv +26 -0
- package/.claude/skills/ui-ux-pro-max/data/colors.csv +97 -0
- package/.claude/skills/ui-ux-pro-max/data/landing.csv +31 -0
- package/.claude/skills/ui-ux-pro-max/data/products.csv +97 -0
- package/.claude/skills/ui-ux-pro-max/data/prompts.csv +24 -0
- package/.claude/skills/ui-ux-pro-max/data/stacks/flutter.csv +53 -0
- package/.claude/skills/ui-ux-pro-max/data/stacks/html-tailwind.csv +56 -0
- package/.claude/skills/ui-ux-pro-max/data/stacks/nextjs.csv +53 -0
- package/.claude/skills/ui-ux-pro-max/data/stacks/nuxt-ui.csv +51 -0
- package/.claude/skills/ui-ux-pro-max/data/stacks/nuxtjs.csv +59 -0
- package/.claude/skills/ui-ux-pro-max/data/stacks/react-native.csv +52 -0
- package/.claude/skills/ui-ux-pro-max/data/stacks/react.csv +54 -0
- package/.claude/skills/ui-ux-pro-max/data/stacks/svelte.csv +54 -0
- package/.claude/skills/ui-ux-pro-max/data/stacks/swiftui.csv +51 -0
- package/.claude/skills/ui-ux-pro-max/data/stacks/vue.csv +50 -0
- package/.claude/skills/ui-ux-pro-max/data/styles.csv +59 -0
- package/.claude/skills/ui-ux-pro-max/data/typography.csv +58 -0
- package/.claude/skills/ui-ux-pro-max/data/ux-guidelines.csv +100 -0
- package/.claude/skills/ui-ux-pro-max/scripts/__pycache__/core.cpython-311.pyc +0 -0
- package/.claude/skills/ui-ux-pro-max/scripts/core.py +238 -0
- package/.claude/skills/ui-ux-pro-max/scripts/search.py +61 -0
- package/.cursor/commands/openspec-apply.md +23 -0
- package/.cursor/commands/openspec-archive.md +27 -0
- package/.cursor/commands/openspec-proposal.md +28 -0
- package/.cursor/commands/ui-ux-pro-max.md +226 -0
- package/.eslintrc.json +3 -0
- package/.shared/ui-ux-pro-max/data/charts.csv +26 -0
- package/.shared/ui-ux-pro-max/data/colors.csv +97 -0
- package/.shared/ui-ux-pro-max/data/landing.csv +31 -0
- package/.shared/ui-ux-pro-max/data/products.csv +97 -0
- package/.shared/ui-ux-pro-max/data/prompts.csv +24 -0
- package/.shared/ui-ux-pro-max/data/stacks/flutter.csv +53 -0
- package/.shared/ui-ux-pro-max/data/stacks/html-tailwind.csv +56 -0
- package/.shared/ui-ux-pro-max/data/stacks/nextjs.csv +53 -0
- package/.shared/ui-ux-pro-max/data/stacks/nuxt-ui.csv +51 -0
- package/.shared/ui-ux-pro-max/data/stacks/nuxtjs.csv +59 -0
- package/.shared/ui-ux-pro-max/data/stacks/react-native.csv +52 -0
- package/.shared/ui-ux-pro-max/data/stacks/react.csv +54 -0
- package/.shared/ui-ux-pro-max/data/stacks/svelte.csv +54 -0
- package/.shared/ui-ux-pro-max/data/stacks/swiftui.csv +51 -0
- package/.shared/ui-ux-pro-max/data/stacks/vue.csv +50 -0
- package/.shared/ui-ux-pro-max/data/styles.csv +59 -0
- package/.shared/ui-ux-pro-max/data/typography.csv +58 -0
- package/.shared/ui-ux-pro-max/data/ux-guidelines.csv +100 -0
- package/.shared/ui-ux-pro-max/scripts/core.py +238 -0
- package/.shared/ui-ux-pro-max/scripts/search.py +61 -0
- package/AGENTS.md +18 -0
- package/CLAUDE.md +18 -0
- package/IMPLEMENTATION.md +157 -0
- package/LICENSE +21 -0
- package/README.md +165 -0
- package/dist/.next/types/app/api/config/route.js +52 -0
- package/dist/.next/types/app/api/gateway/[...path]/route.js +52 -0
- package/dist/.next/types/app/api/gateway/route.js +52 -0
- package/dist/.next/types/app/api/logs/route.js +52 -0
- package/dist/.next/types/app/api/models/route.js +52 -0
- package/dist/.next/types/app/api/providers/route.js +52 -0
- package/dist/.next/types/app/api/providers/test/route.js +52 -0
- package/dist/.next/types/app/api/service/start/route.js +52 -0
- package/dist/.next/types/app/api/service/status/route.js +52 -0
- package/dist/.next/types/app/api/service/stop/route.js +52 -0
- package/dist/.next/types/app/layout.js +22 -0
- package/dist/.next/types/app/logs/page.js +22 -0
- package/dist/.next/types/app/models/page.js +22 -0
- package/dist/.next/types/app/page.js +22 -0
- package/dist/.next/types/app/providers/page.js +22 -0
- package/dist/src/app/api/config/route.js +43 -0
- package/dist/src/app/api/gateway/[...path]/route.js +83 -0
- package/dist/src/app/api/gateway/route.js +63 -0
- package/dist/src/app/api/logs/route.js +34 -0
- package/dist/src/app/api/models/route.js +152 -0
- package/dist/src/app/api/providers/route.js +118 -0
- package/dist/src/app/api/providers/test/route.js +154 -0
- package/dist/src/app/api/service/start/route.js +55 -0
- package/dist/src/app/api/service/status/route.js +17 -0
- package/dist/src/app/api/service/stop/route.js +20 -0
- package/dist/src/app/components/ConfirmDialog.jsx +31 -0
- package/dist/src/app/components/Nav.jsx +45 -0
- package/dist/src/app/components/Toast.jsx +37 -0
- package/dist/src/app/components/ToastProvider.jsx +21 -0
- package/dist/src/app/layout.jsx +13 -0
- package/dist/src/app/logs/page.jsx +210 -0
- package/dist/src/app/models/page.jsx +291 -0
- package/dist/src/app/page.jsx +236 -0
- package/dist/src/app/providers/page.jsx +402 -0
- package/dist/src/cli/index.js +90 -0
- package/dist/src/db/database.js +69 -0
- package/dist/src/db/queries.js +261 -0
- package/dist/src/db/schema.js +67 -0
- package/dist/src/server/crypto.js +22 -0
- package/dist/src/server/gateway-server.js +200 -0
- package/dist/src/server/gateway.js +76 -0
- package/dist/src/server/logger.js +72 -0
- package/dist/src/server/providers/anthropic.js +52 -0
- package/dist/src/server/providers/gemini.js +64 -0
- package/dist/src/server/providers/index.js +16 -0
- package/dist/src/server/providers/openai.js +86 -0
- package/dist/src/server/providers/types.js +1 -0
- package/dist/src/server/service-manager.js +286 -0
- package/docs/TODO.md +19 -0
- package/next.config.js +7 -0
- package/openspec/AGENTS.md +456 -0
- package/openspec/changes/add-logging/proposal.md +18 -0
- package/openspec/changes/add-logging/specs/core/spec.md +21 -0
- package/openspec/changes/add-logging/tasks.md +16 -0
- package/openspec/changes/add-provider-test-connection/proposal.md +22 -0
- package/openspec/changes/add-provider-test-connection/specs/model-provider/spec.md +68 -0
- package/openspec/changes/add-provider-test-connection/tasks.md +31 -0
- package/openspec/changes/improve-gateway-startup/design.md +137 -0
- package/openspec/changes/improve-gateway-startup/proposal.md +33 -0
- package/openspec/changes/improve-gateway-startup/specs/api-gateway/spec.md +94 -0
- package/openspec/changes/improve-gateway-startup/specs/web-ui/spec.md +67 -0
- package/openspec/changes/improve-gateway-startup/tasks.md +47 -0
- package/openspec/changes/init-api-gateway/design.md +185 -0
- package/openspec/changes/init-api-gateway/proposal.md +30 -0
- package/openspec/changes/init-api-gateway/specs/api-gateway/spec.md +42 -0
- package/openspec/changes/init-api-gateway/specs/cli-tool/spec.md +40 -0
- package/openspec/changes/init-api-gateway/specs/model-management/spec.md +47 -0
- package/openspec/changes/init-api-gateway/specs/model-provider/spec.md +33 -0
- package/openspec/changes/init-api-gateway/specs/request-logging/spec.md +54 -0
- package/openspec/changes/init-api-gateway/specs/web-ui/spec.md +49 -0
- package/openspec/changes/init-api-gateway/tasks.md +84 -0
- package/openspec/project.md +58 -0
- package/package.json +51 -0
- package/postcss.config.js +6 -0
- package/src/app/api/config/route.ts +62 -0
- package/src/app/api/gateway/[...path]/route.ts +118 -0
- package/src/app/api/gateway/route.ts +77 -0
- package/src/app/api/logs/route.ts +48 -0
- package/src/app/api/models/route.ts +210 -0
- package/src/app/api/providers/route.ts +162 -0
- package/src/app/api/providers/test/route.ts +182 -0
- package/src/app/api/service/start/route.ts +73 -0
- package/src/app/api/service/status/route.ts +22 -0
- package/src/app/api/service/stop/route.ts +27 -0
- package/src/app/components/ConfirmDialog.tsx +63 -0
- package/src/app/components/Nav.tsx +66 -0
- package/src/app/components/Toast.tsx +61 -0
- package/src/app/components/ToastProvider.tsx +43 -0
- package/src/app/globals.css +71 -0
- package/src/app/layout.tsx +22 -0
- package/src/app/logs/page.tsx +261 -0
- package/src/app/models/page.tsx +500 -0
- package/src/app/page.tsx +742 -0
- package/src/app/providers/page.tsx +558 -0
- package/src/cli/index.ts +95 -0
- package/src/db/database.ts +125 -0
- package/src/db/queries.ts +339 -0
- package/src/db/schema.ts +117 -0
- package/src/server/crypto.ts +48 -0
- package/src/server/gateway-server.ts +306 -0
- package/src/server/gateway.ts +163 -0
- package/src/server/logger.ts +96 -0
- package/src/server/providers/anthropic.ts +121 -0
- package/src/server/providers/gemini.ts +112 -0
- package/src/server/providers/index.ts +20 -0
- package/src/server/providers/openai.ts +235 -0
- package/src/server/providers/types.ts +20 -0
- package/src/server/service-manager.ts +321 -0
- package/tailwind.config.js +16 -0
- package/tsconfig.json +29 -0
package/src/app/page.tsx
ADDED
|
@@ -0,0 +1,742 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { useState, useEffect } from 'react';
|
|
4
|
+
import Nav from './components/Nav';
|
|
5
|
+
import { useToast } from './components/ToastProvider';
|
|
6
|
+
|
|
7
|
+
interface Config {
|
|
8
|
+
port?: string;
|
|
9
|
+
api_key?: string;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
interface ServiceStatus {
|
|
13
|
+
status: 'running' | 'stopped';
|
|
14
|
+
port?: number;
|
|
15
|
+
pid?: number | null;
|
|
16
|
+
started_at?: string | null;
|
|
17
|
+
error?: string;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
interface Model {
|
|
21
|
+
id: number;
|
|
22
|
+
provider_id: number;
|
|
23
|
+
name: string;
|
|
24
|
+
model_id: string;
|
|
25
|
+
enabled: boolean;
|
|
26
|
+
provider_name?: string;
|
|
27
|
+
provider_protocol?: string;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export default function Home() {
|
|
31
|
+
const [config, setConfig] = useState<Config>({});
|
|
32
|
+
const [loading, setLoading] = useState(true);
|
|
33
|
+
const [saving, setSaving] = useState(false);
|
|
34
|
+
const [serviceStatus, setServiceStatus] = useState<ServiceStatus>({ status: 'stopped' });
|
|
35
|
+
const [starting, setStarting] = useState(false);
|
|
36
|
+
const [stopping, setStopping] = useState(false);
|
|
37
|
+
const [showApiKey, setShowApiKey] = useState(false);
|
|
38
|
+
const [models, setModels] = useState<Model[]>([]);
|
|
39
|
+
const [selectedModel, setSelectedModel] = useState<Model | null>(null);
|
|
40
|
+
const [modelSearchQuery, setModelSearchQuery] = useState('');
|
|
41
|
+
const [showModelDropdown, setShowModelDropdown] = useState(false);
|
|
42
|
+
const [testing, setTesting] = useState(false);
|
|
43
|
+
const [testResult, setTestResult] = useState<any>(null);
|
|
44
|
+
const [showExampleConfig, setShowExampleConfig] = useState(false);
|
|
45
|
+
const { showToast } = useToast();
|
|
46
|
+
|
|
47
|
+
useEffect(() => {
|
|
48
|
+
loadConfig();
|
|
49
|
+
loadServiceStatus();
|
|
50
|
+
loadModels();
|
|
51
|
+
|
|
52
|
+
// Poll service status every 2 seconds
|
|
53
|
+
const interval = setInterval(loadServiceStatus, 2000);
|
|
54
|
+
|
|
55
|
+
return () => clearInterval(interval);
|
|
56
|
+
}, []);
|
|
57
|
+
|
|
58
|
+
// Close dropdown when clicking outside
|
|
59
|
+
useEffect(() => {
|
|
60
|
+
const handleClickOutside = (event: MouseEvent) => {
|
|
61
|
+
const target = event.target as HTMLElement;
|
|
62
|
+
if (!target.closest('[data-model-selector]')) {
|
|
63
|
+
setShowModelDropdown(false);
|
|
64
|
+
}
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
if (showModelDropdown) {
|
|
68
|
+
document.addEventListener('mousedown', handleClickOutside);
|
|
69
|
+
return () => document.removeEventListener('mousedown', handleClickOutside);
|
|
70
|
+
}
|
|
71
|
+
}, [showModelDropdown]);
|
|
72
|
+
|
|
73
|
+
const loadModels = async () => {
|
|
74
|
+
try {
|
|
75
|
+
const res = await fetch('/api/models');
|
|
76
|
+
const data = await res.json();
|
|
77
|
+
// Only show enabled models
|
|
78
|
+
const enabledModels = data.filter((m: Model) => m.enabled);
|
|
79
|
+
setModels(enabledModels);
|
|
80
|
+
|
|
81
|
+
// Clear selected model if it's no longer enabled
|
|
82
|
+
if (selectedModel && !enabledModels.find((m: Model) => m.id === selectedModel.id)) {
|
|
83
|
+
setSelectedModel(null);
|
|
84
|
+
setModelSearchQuery('');
|
|
85
|
+
}
|
|
86
|
+
} catch (error) {
|
|
87
|
+
console.error('Failed to load models:', error);
|
|
88
|
+
}
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
const loadConfig = async () => {
|
|
92
|
+
try {
|
|
93
|
+
const res = await fetch('/api/config');
|
|
94
|
+
const data = await res.json();
|
|
95
|
+
setConfig({
|
|
96
|
+
port: data.port || '3000',
|
|
97
|
+
api_key: data.api_key || '',
|
|
98
|
+
});
|
|
99
|
+
} catch (error) {
|
|
100
|
+
console.error('Failed to load config:', error);
|
|
101
|
+
} finally {
|
|
102
|
+
setLoading(false);
|
|
103
|
+
}
|
|
104
|
+
};
|
|
105
|
+
|
|
106
|
+
const loadServiceStatus = async () => {
|
|
107
|
+
try {
|
|
108
|
+
const res = await fetch('/api/service/status');
|
|
109
|
+
const data = await res.json();
|
|
110
|
+
const prevStatus = serviceStatus.status;
|
|
111
|
+
setServiceStatus(data);
|
|
112
|
+
|
|
113
|
+
// Clear test result and selected model when service stops
|
|
114
|
+
if (prevStatus === 'running' && data.status === 'stopped') {
|
|
115
|
+
setTestResult(null);
|
|
116
|
+
setSelectedModel(null);
|
|
117
|
+
setModelSearchQuery('');
|
|
118
|
+
setShowExampleConfig(false);
|
|
119
|
+
}
|
|
120
|
+
} catch (error) {
|
|
121
|
+
console.error('Failed to load service status:', error);
|
|
122
|
+
}
|
|
123
|
+
};
|
|
124
|
+
|
|
125
|
+
const handleStart = async () => {
|
|
126
|
+
if (starting || serviceStatus.status === 'running') return;
|
|
127
|
+
|
|
128
|
+
setStarting(true);
|
|
129
|
+
try {
|
|
130
|
+
const port = config.port ? parseInt(config.port, 10) : undefined;
|
|
131
|
+
const res = await fetch('/api/service/start', {
|
|
132
|
+
method: 'POST',
|
|
133
|
+
headers: { 'Content-Type': 'application/json' },
|
|
134
|
+
body: JSON.stringify({ port }),
|
|
135
|
+
});
|
|
136
|
+
const data = await res.json();
|
|
137
|
+
|
|
138
|
+
if (data.error) {
|
|
139
|
+
showToast(`启动失败: ${data.error}`, 'error');
|
|
140
|
+
} else {
|
|
141
|
+
showToast('服务已启动', 'success');
|
|
142
|
+
setServiceStatus(data);
|
|
143
|
+
}
|
|
144
|
+
} catch (error: any) {
|
|
145
|
+
console.error('Failed to start service:', error);
|
|
146
|
+
showToast('启动失败', 'error');
|
|
147
|
+
} finally {
|
|
148
|
+
setStarting(false);
|
|
149
|
+
// Reload status after a short delay
|
|
150
|
+
setTimeout(loadServiceStatus, 500);
|
|
151
|
+
}
|
|
152
|
+
};
|
|
153
|
+
|
|
154
|
+
const handleStop = async () => {
|
|
155
|
+
if (stopping || serviceStatus.status === 'stopped') return;
|
|
156
|
+
|
|
157
|
+
setStopping(true);
|
|
158
|
+
try {
|
|
159
|
+
const res = await fetch('/api/service/stop', {
|
|
160
|
+
method: 'POST',
|
|
161
|
+
headers: { 'Content-Type': 'application/json' },
|
|
162
|
+
});
|
|
163
|
+
const data = await res.json();
|
|
164
|
+
|
|
165
|
+
if (data.error) {
|
|
166
|
+
showToast(`停止失败: ${data.error}`, 'error');
|
|
167
|
+
} else {
|
|
168
|
+
showToast('服务已停止', 'success');
|
|
169
|
+
setServiceStatus(data);
|
|
170
|
+
}
|
|
171
|
+
} catch (error: any) {
|
|
172
|
+
console.error('Failed to stop service:', error);
|
|
173
|
+
showToast('停止失败', 'error');
|
|
174
|
+
} finally {
|
|
175
|
+
setStopping(false);
|
|
176
|
+
// Reload status after a short delay
|
|
177
|
+
setTimeout(loadServiceStatus, 500);
|
|
178
|
+
}
|
|
179
|
+
};
|
|
180
|
+
|
|
181
|
+
const saveConfig = async () => {
|
|
182
|
+
setSaving(true);
|
|
183
|
+
try {
|
|
184
|
+
await fetch('/api/config', {
|
|
185
|
+
method: 'POST',
|
|
186
|
+
headers: { 'Content-Type': 'application/json' },
|
|
187
|
+
body: JSON.stringify({
|
|
188
|
+
key: 'port',
|
|
189
|
+
value: config.port,
|
|
190
|
+
}),
|
|
191
|
+
});
|
|
192
|
+
if (config.api_key) {
|
|
193
|
+
await fetch('/api/config', {
|
|
194
|
+
method: 'POST',
|
|
195
|
+
headers: { 'Content-Type': 'application/json' },
|
|
196
|
+
body: JSON.stringify({
|
|
197
|
+
key: 'api_key',
|
|
198
|
+
value: config.api_key,
|
|
199
|
+
}),
|
|
200
|
+
});
|
|
201
|
+
}
|
|
202
|
+
showToast('配置已保存', 'success');
|
|
203
|
+
} catch (error) {
|
|
204
|
+
console.error('Failed to save config:', error);
|
|
205
|
+
showToast('保存失败', 'error');
|
|
206
|
+
} finally {
|
|
207
|
+
setSaving(false);
|
|
208
|
+
}
|
|
209
|
+
};
|
|
210
|
+
|
|
211
|
+
const filteredModels = models.filter((model) => {
|
|
212
|
+
if (!modelSearchQuery) return true;
|
|
213
|
+
const query = modelSearchQuery.toLowerCase();
|
|
214
|
+
return (
|
|
215
|
+
model.name.toLowerCase().includes(query) ||
|
|
216
|
+
model.model_id.toLowerCase().includes(query) ||
|
|
217
|
+
model.provider_name?.toLowerCase().includes(query)
|
|
218
|
+
);
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
const handleTestGateway = async () => {
|
|
222
|
+
if (!selectedModel) {
|
|
223
|
+
showToast('请先选择模型', 'error');
|
|
224
|
+
return;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
if (serviceStatus.status !== 'running' || !serviceStatus.port) {
|
|
228
|
+
showToast('网关服务未运行', 'error');
|
|
229
|
+
return;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
setTesting(true);
|
|
233
|
+
setTestResult(null);
|
|
234
|
+
|
|
235
|
+
try {
|
|
236
|
+
const gatewayUrl = `http://localhost:${serviceStatus.port}`;
|
|
237
|
+
const testPayload = {
|
|
238
|
+
model: selectedModel.model_id,
|
|
239
|
+
messages: [
|
|
240
|
+
{
|
|
241
|
+
role: 'user',
|
|
242
|
+
content: 'Hello, this is a test message. Please respond with "Test successful".',
|
|
243
|
+
},
|
|
244
|
+
],
|
|
245
|
+
max_tokens: 50,
|
|
246
|
+
};
|
|
247
|
+
|
|
248
|
+
const headers: Record<string, string> = {
|
|
249
|
+
'Content-Type': 'application/json',
|
|
250
|
+
};
|
|
251
|
+
|
|
252
|
+
if (config.api_key) {
|
|
253
|
+
headers['Authorization'] = `Bearer ${config.api_key}`;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
// Use root path with model and provider in query parameter
|
|
257
|
+
// Include provider to avoid conflicts if multiple providers have same model_id
|
|
258
|
+
const testUrl = `${gatewayUrl}/?model=${encodeURIComponent(selectedModel.model_id)}&provider=${encodeURIComponent(selectedModel.provider_name || '')}`;
|
|
259
|
+
|
|
260
|
+
console.log('[Test] Sending request to:', testUrl);
|
|
261
|
+
console.log('[Test] Headers:', headers);
|
|
262
|
+
console.log('[Test] Payload:', testPayload);
|
|
263
|
+
|
|
264
|
+
const response = await fetch(testUrl, {
|
|
265
|
+
method: 'POST',
|
|
266
|
+
headers,
|
|
267
|
+
body: JSON.stringify(testPayload),
|
|
268
|
+
});
|
|
269
|
+
|
|
270
|
+
console.log('[Test] Response status:', response.status);
|
|
271
|
+
console.log('[Test] Response headers:', Object.fromEntries(response.headers.entries()));
|
|
272
|
+
|
|
273
|
+
// Read response as text first to handle potential JSON parsing errors
|
|
274
|
+
const responseText = await response.text();
|
|
275
|
+
let data: any;
|
|
276
|
+
|
|
277
|
+
try {
|
|
278
|
+
data = responseText ? JSON.parse(responseText) : null;
|
|
279
|
+
} catch (parseError: any) {
|
|
280
|
+
// If JSON parsing fails, return the raw text
|
|
281
|
+
setTestResult({
|
|
282
|
+
success: false,
|
|
283
|
+
status: response.status,
|
|
284
|
+
error: {
|
|
285
|
+
message: 'Invalid JSON response from gateway',
|
|
286
|
+
rawResponse: responseText.substring(0, 500), // Limit to first 500 chars
|
|
287
|
+
parseError: parseError.message,
|
|
288
|
+
},
|
|
289
|
+
});
|
|
290
|
+
showToast('测试失败: 响应格式错误', 'error');
|
|
291
|
+
setTesting(false);
|
|
292
|
+
return;
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
if (response.ok) {
|
|
296
|
+
setTestResult({
|
|
297
|
+
success: true,
|
|
298
|
+
status: response.status,
|
|
299
|
+
data,
|
|
300
|
+
});
|
|
301
|
+
showToast('测试成功', 'success');
|
|
302
|
+
} else {
|
|
303
|
+
setTestResult({
|
|
304
|
+
success: false,
|
|
305
|
+
status: response.status,
|
|
306
|
+
error: data,
|
|
307
|
+
});
|
|
308
|
+
showToast('测试失败', 'error');
|
|
309
|
+
}
|
|
310
|
+
} catch (error: any) {
|
|
311
|
+
setTestResult({
|
|
312
|
+
success: false,
|
|
313
|
+
error: {
|
|
314
|
+
message: error.message || '网络错误',
|
|
315
|
+
type: 'network_error',
|
|
316
|
+
},
|
|
317
|
+
});
|
|
318
|
+
showToast('测试失败: ' + (error.message || '未知错误'), 'error');
|
|
319
|
+
} finally {
|
|
320
|
+
setTesting(false);
|
|
321
|
+
}
|
|
322
|
+
};
|
|
323
|
+
|
|
324
|
+
const getExampleConfig = () => {
|
|
325
|
+
if (!selectedModel || serviceStatus.status !== 'running' || !serviceStatus.port) {
|
|
326
|
+
return null;
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
const gatewayUrl = `http://localhost:${serviceStatus.port}`;
|
|
330
|
+
const exampleConfig = {
|
|
331
|
+
endpoint: `${gatewayUrl}?model=${selectedModel.model_id}&provider=${selectedModel.provider_name || ''}`,
|
|
332
|
+
method: 'POST',
|
|
333
|
+
headers: {
|
|
334
|
+
'Content-Type': 'application/json',
|
|
335
|
+
...(config.api_key ? { Authorization: `Bearer ${config.api_key}` } : {}),
|
|
336
|
+
},
|
|
337
|
+
body: {
|
|
338
|
+
model: selectedModel.model_id,
|
|
339
|
+
messages: [
|
|
340
|
+
{
|
|
341
|
+
role: 'user',
|
|
342
|
+
content: 'Hello!',
|
|
343
|
+
},
|
|
344
|
+
],
|
|
345
|
+
max_tokens: 100,
|
|
346
|
+
},
|
|
347
|
+
note: 'The provider parameter is optional but recommended to avoid conflicts when multiple providers have the same model_id.',
|
|
348
|
+
};
|
|
349
|
+
|
|
350
|
+
return exampleConfig;
|
|
351
|
+
};
|
|
352
|
+
|
|
353
|
+
if (loading) {
|
|
354
|
+
return (
|
|
355
|
+
<div className="min-h-screen flex items-center justify-center">
|
|
356
|
+
<div className="text-lg">加载中...</div>
|
|
357
|
+
</div>
|
|
358
|
+
);
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
return (
|
|
362
|
+
<div className="min-h-screen bg-gradient-to-br from-emerald-50/30 via-white to-slate-50/50">
|
|
363
|
+
<Nav />
|
|
364
|
+
|
|
365
|
+
<main className="max-w-3xl mx-auto py-6 sm:px-6 lg:px-8">
|
|
366
|
+
<div className="px-4 py-4 sm:px-0">
|
|
367
|
+
<div className="bg-white/70 backdrop-blur-sm shadow-md rounded-2xl border border-emerald-100/50">
|
|
368
|
+
<div className="px-6 py-5 sm:p-6">
|
|
369
|
+
<div className="mb-5">
|
|
370
|
+
<h2 className="text-lg font-bold text-slate-800 mb-1">网关配置</h2>
|
|
371
|
+
<p className="text-xs text-slate-500">配置 API 网关的运行参数</p>
|
|
372
|
+
</div>
|
|
373
|
+
|
|
374
|
+
<div className="space-y-3.5">
|
|
375
|
+
<div>
|
|
376
|
+
<label className="block text-xs font-semibold text-slate-700 mb-1.5">
|
|
377
|
+
端口
|
|
378
|
+
</label>
|
|
379
|
+
<input
|
|
380
|
+
type="number"
|
|
381
|
+
value={config.port}
|
|
382
|
+
onChange={(e) => setConfig({ ...config, port: e.target.value })}
|
|
383
|
+
autoComplete="off"
|
|
384
|
+
className="block w-full rounded-lg border border-slate-200 bg-white/80 px-3 py-2 text-xs shadow-sm transition-all duration-300 focus:border-emerald-400 focus:ring-2 focus:ring-emerald-400/20 focus:ring-offset-1"
|
|
385
|
+
placeholder="3000"
|
|
386
|
+
/>
|
|
387
|
+
<p className="mt-1 text-xs text-slate-400">
|
|
388
|
+
提示:如果 Web UI 运行在 3000 端口,建议将服务端口设置为 3001 或其他端口以避免冲突
|
|
389
|
+
</p>
|
|
390
|
+
</div>
|
|
391
|
+
|
|
392
|
+
<div>
|
|
393
|
+
<label className="block text-xs font-semibold text-slate-700 mb-1.5">
|
|
394
|
+
API Key (可选)
|
|
395
|
+
</label>
|
|
396
|
+
<div className="relative">
|
|
397
|
+
<input
|
|
398
|
+
type={showApiKey ? 'text' : 'password'}
|
|
399
|
+
value={config.api_key}
|
|
400
|
+
onChange={(e) => setConfig({ ...config, api_key: e.target.value })}
|
|
401
|
+
autoComplete="new-password"
|
|
402
|
+
className="block w-full rounded-lg border border-slate-200 bg-white/80 px-3 py-2 pr-9 text-xs shadow-sm transition-all duration-300 focus:border-emerald-400 focus:ring-2 focus:ring-emerald-400/20 focus:ring-offset-1"
|
|
403
|
+
placeholder="留空则不验证"
|
|
404
|
+
/>
|
|
405
|
+
<button
|
|
406
|
+
type="button"
|
|
407
|
+
onClick={() => setShowApiKey(!showApiKey)}
|
|
408
|
+
className="absolute right-2 top-1/2 -translate-y-1/2 p-1 text-slate-400 hover:text-slate-600 transition-colors"
|
|
409
|
+
tabIndex={-1}
|
|
410
|
+
>
|
|
411
|
+
{showApiKey ? (
|
|
412
|
+
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
413
|
+
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M13.875 18.825A10.05 10.05 0 0112 19c-4.478 0-8.268-2.943-9.543-7a9.97 9.97 0 011.563-3.029m5.858.908a3 3 0 114.243 4.243M9.878 9.878l4.242 4.242M9.88 9.88l-3.29-3.29m7.532 7.532l3.29 3.29M3 3l3.59 3.59m0 0A9.953 9.953 0 0112 5c4.478 0 8.268 2.943 9.543 7a10.025 10.025 0 01-4.132 5.411m0 0L21 21" />
|
|
414
|
+
</svg>
|
|
415
|
+
) : (
|
|
416
|
+
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
417
|
+
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M15 12a3 3 0 11-6 0 3 3 0 016 0z" />
|
|
418
|
+
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M2.458 12C3.732 7.943 7.523 5 12 5c4.478 0 8.268 2.943 9.542 7-1.274 4.057-5.064 7-9.542 7-4.477 0-8.268-2.943-9.542-7z" />
|
|
419
|
+
</svg>
|
|
420
|
+
)}
|
|
421
|
+
</button>
|
|
422
|
+
</div>
|
|
423
|
+
</div>
|
|
424
|
+
|
|
425
|
+
<div className="flex items-center justify-between pt-3">
|
|
426
|
+
<div>
|
|
427
|
+
<span className="text-xs text-slate-500">状态: </span>
|
|
428
|
+
<span className={`text-xs font-medium ${serviceStatus.status === 'running' ? 'text-emerald-600' : 'text-slate-500'}`}>
|
|
429
|
+
{serviceStatus.status === 'running' ? '运行中' : '已停止'}
|
|
430
|
+
</span>
|
|
431
|
+
{serviceStatus.status === 'running' && serviceStatus.port && (
|
|
432
|
+
<span className="text-xs text-slate-400 ml-2">(端口: {serviceStatus.port})</span>
|
|
433
|
+
)}
|
|
434
|
+
</div>
|
|
435
|
+
<div className="flex space-x-2">
|
|
436
|
+
<button
|
|
437
|
+
onClick={saveConfig}
|
|
438
|
+
disabled={saving || serviceStatus.status === 'running'}
|
|
439
|
+
className="inline-flex items-center px-3.5 py-1.5 border border-transparent text-xs font-semibold rounded-lg shadow-sm text-white bg-emerald-600 hover:bg-emerald-700 focus:outline-none focus:ring-2 focus:ring-offset-1 focus:ring-emerald-500/50 disabled:opacity-50 disabled:cursor-not-allowed transition-all duration-300"
|
|
440
|
+
>
|
|
441
|
+
{saving ? (
|
|
442
|
+
<>
|
|
443
|
+
<svg className="animate-spin -ml-1 mr-1.5 h-3 w-3 text-white" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
|
|
444
|
+
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4"></circle>
|
|
445
|
+
<path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
|
|
446
|
+
</svg>
|
|
447
|
+
保存中
|
|
448
|
+
</>
|
|
449
|
+
) : (
|
|
450
|
+
'保存配置'
|
|
451
|
+
)}
|
|
452
|
+
</button>
|
|
453
|
+
<button
|
|
454
|
+
onClick={serviceStatus.status === 'running' ? handleStop : handleStart}
|
|
455
|
+
disabled={starting || stopping}
|
|
456
|
+
className={`inline-flex items-center px-3.5 py-1.5 border border-transparent text-xs font-semibold rounded-lg shadow-sm text-white focus:outline-none focus:ring-2 focus:ring-offset-1 transition-all duration-300 disabled:opacity-50 disabled:cursor-not-allowed ${
|
|
457
|
+
serviceStatus.status === 'running'
|
|
458
|
+
? 'bg-rose-500 hover:bg-rose-600 focus:ring-rose-500/50'
|
|
459
|
+
: 'bg-emerald-600 hover:bg-emerald-700 focus:ring-emerald-500/50'
|
|
460
|
+
}`}
|
|
461
|
+
>
|
|
462
|
+
{starting ? (
|
|
463
|
+
<>
|
|
464
|
+
<svg className="animate-spin -ml-1 mr-1.5 h-3 w-3 text-white" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
|
|
465
|
+
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4"></circle>
|
|
466
|
+
<path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
|
|
467
|
+
</svg>
|
|
468
|
+
启动中
|
|
469
|
+
</>
|
|
470
|
+
) : stopping ? (
|
|
471
|
+
<>
|
|
472
|
+
<svg className="animate-spin -ml-1 mr-1.5 h-3 w-3 text-white" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
|
|
473
|
+
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4"></circle>
|
|
474
|
+
<path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
|
|
475
|
+
</svg>
|
|
476
|
+
停止中
|
|
477
|
+
</>
|
|
478
|
+
) : serviceStatus.status === 'running' ? (
|
|
479
|
+
<>
|
|
480
|
+
<svg className="w-3 h-3 mr-1.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
481
|
+
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
|
|
482
|
+
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 10a1 1 0 011-1h4a1 1 0 011 1v4a1 1 0 01-1 1h-4a1 1 0 01-1-1v-4z" />
|
|
483
|
+
</svg>
|
|
484
|
+
停止
|
|
485
|
+
</>
|
|
486
|
+
) : (
|
|
487
|
+
<>
|
|
488
|
+
<svg className="w-3 h-3 mr-1.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
489
|
+
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M14.752 11.168l-3.197-2.132A1 1 0 0010 9.87v4.263a1 1 0 001.555.832l3.197-2.132a1 1 0 000-1.664z" />
|
|
490
|
+
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
|
|
491
|
+
</svg>
|
|
492
|
+
启动
|
|
493
|
+
</>
|
|
494
|
+
)}
|
|
495
|
+
</button>
|
|
496
|
+
</div>
|
|
497
|
+
</div>
|
|
498
|
+
</div>
|
|
499
|
+
</div>
|
|
500
|
+
</div>
|
|
501
|
+
|
|
502
|
+
{/* Gateway Test Section - Only show when service is running */}
|
|
503
|
+
{serviceStatus.status === 'running' && serviceStatus.port && (
|
|
504
|
+
<div className="mt-6 bg-white/70 backdrop-blur-sm shadow-md rounded-2xl border border-emerald-100/50">
|
|
505
|
+
<div className="px-6 py-5 sm:p-6">
|
|
506
|
+
<div className="mb-5">
|
|
507
|
+
<h2 className="text-lg font-bold text-slate-800 mb-1">网关测试</h2>
|
|
508
|
+
<p className="text-xs text-slate-500">测试网关服务连接和模型可用性</p>
|
|
509
|
+
</div>
|
|
510
|
+
|
|
511
|
+
<div className="space-y-4">
|
|
512
|
+
{/* Model Search and Selection */}
|
|
513
|
+
<div data-model-selector>
|
|
514
|
+
<label className="block text-xs font-semibold text-slate-700 mb-1.5">
|
|
515
|
+
选择模型
|
|
516
|
+
</label>
|
|
517
|
+
<div className="relative">
|
|
518
|
+
<input
|
|
519
|
+
type="text"
|
|
520
|
+
value={selectedModel ? selectedModel.name : modelSearchQuery}
|
|
521
|
+
onChange={(e) => {
|
|
522
|
+
setModelSearchQuery(e.target.value);
|
|
523
|
+
setShowModelDropdown(true);
|
|
524
|
+
if (selectedModel) {
|
|
525
|
+
setSelectedModel(null);
|
|
526
|
+
}
|
|
527
|
+
}}
|
|
528
|
+
onFocus={() => setShowModelDropdown(true)}
|
|
529
|
+
placeholder="搜索模型名称、ID 或供应商..."
|
|
530
|
+
className="block w-full rounded-lg border border-slate-200 bg-white/80 px-3 py-2 pr-9 text-xs shadow-sm transition-all duration-300 focus:border-emerald-400 focus:ring-2 focus:ring-emerald-400/20 focus:ring-offset-1"
|
|
531
|
+
/>
|
|
532
|
+
<svg
|
|
533
|
+
className="absolute right-2.5 top-1/2 -translate-y-1/2 w-4 h-4 text-slate-400"
|
|
534
|
+
fill="none"
|
|
535
|
+
stroke="currentColor"
|
|
536
|
+
viewBox="0 0 24 24"
|
|
537
|
+
>
|
|
538
|
+
<path
|
|
539
|
+
strokeLinecap="round"
|
|
540
|
+
strokeLinejoin="round"
|
|
541
|
+
strokeWidth={2}
|
|
542
|
+
d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"
|
|
543
|
+
/>
|
|
544
|
+
</svg>
|
|
545
|
+
</div>
|
|
546
|
+
|
|
547
|
+
{/* Model Dropdown */}
|
|
548
|
+
{showModelDropdown && filteredModels.length > 0 && (
|
|
549
|
+
<div className="mt-2 max-h-48 overflow-y-auto rounded-lg border border-slate-200 bg-white shadow-lg z-10">
|
|
550
|
+
{filteredModels.map((model) => (
|
|
551
|
+
<button
|
|
552
|
+
key={model.id}
|
|
553
|
+
type="button"
|
|
554
|
+
onClick={() => {
|
|
555
|
+
setSelectedModel(model);
|
|
556
|
+
setModelSearchQuery('');
|
|
557
|
+
setShowModelDropdown(false);
|
|
558
|
+
}}
|
|
559
|
+
className={`w-full text-left px-3 py-2 text-xs hover:bg-emerald-50 transition-colors ${
|
|
560
|
+
selectedModel?.id === model.id ? 'bg-emerald-50' : ''
|
|
561
|
+
}`}
|
|
562
|
+
>
|
|
563
|
+
<div className="font-medium text-slate-800">{model.name}</div>
|
|
564
|
+
<div className="text-slate-500 text-[10px] mt-0.5">
|
|
565
|
+
{model.model_id} · {model.provider_name}
|
|
566
|
+
</div>
|
|
567
|
+
</button>
|
|
568
|
+
))}
|
|
569
|
+
</div>
|
|
570
|
+
)}
|
|
571
|
+
|
|
572
|
+
{/* Selected Model Display */}
|
|
573
|
+
{selectedModel && (
|
|
574
|
+
<div className="mt-2 p-3 rounded-lg bg-emerald-50/50 border border-emerald-200/50">
|
|
575
|
+
<div className="flex items-center justify-between">
|
|
576
|
+
<div>
|
|
577
|
+
<div className="text-xs font-semibold text-slate-800">
|
|
578
|
+
{selectedModel.name}
|
|
579
|
+
</div>
|
|
580
|
+
<div className="text-[10px] text-slate-500 mt-0.5">
|
|
581
|
+
{selectedModel.model_id} · {selectedModel.provider_name}
|
|
582
|
+
</div>
|
|
583
|
+
</div>
|
|
584
|
+
<button
|
|
585
|
+
type="button"
|
|
586
|
+
onClick={() => {
|
|
587
|
+
setSelectedModel(null);
|
|
588
|
+
setModelSearchQuery('');
|
|
589
|
+
setShowModelDropdown(true);
|
|
590
|
+
}}
|
|
591
|
+
className="text-slate-400 hover:text-slate-600 transition-colors"
|
|
592
|
+
>
|
|
593
|
+
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
594
|
+
<path
|
|
595
|
+
strokeLinecap="round"
|
|
596
|
+
strokeLinejoin="round"
|
|
597
|
+
strokeWidth={2}
|
|
598
|
+
d="M6 18L18 6M6 6l12 12"
|
|
599
|
+
/>
|
|
600
|
+
</svg>
|
|
601
|
+
</button>
|
|
602
|
+
</div>
|
|
603
|
+
</div>
|
|
604
|
+
)}
|
|
605
|
+
|
|
606
|
+
{models.length === 0 && (
|
|
607
|
+
<p className="mt-2 text-xs text-slate-400">
|
|
608
|
+
暂无启用的模型,请先在模型管理页面启用模型
|
|
609
|
+
</p>
|
|
610
|
+
)}
|
|
611
|
+
|
|
612
|
+
{showModelDropdown && filteredModels.length === 0 && modelSearchQuery && (
|
|
613
|
+
<div className="mt-2 p-3 rounded-lg border border-slate-200 bg-slate-50">
|
|
614
|
+
<p className="text-xs text-slate-500">未找到匹配的模型</p>
|
|
615
|
+
</div>
|
|
616
|
+
)}
|
|
617
|
+
</div>
|
|
618
|
+
|
|
619
|
+
{/* Test Button */}
|
|
620
|
+
<div className="flex items-center justify-between pt-2">
|
|
621
|
+
<div className="flex items-center space-x-2">
|
|
622
|
+
<button
|
|
623
|
+
onClick={handleTestGateway}
|
|
624
|
+
disabled={testing || !selectedModel}
|
|
625
|
+
className="inline-flex items-center px-4 py-2 border border-transparent text-xs font-semibold rounded-lg shadow-sm text-white bg-blue-600 hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-1 focus:ring-blue-500/50 disabled:opacity-50 disabled:cursor-not-allowed transition-all duration-300"
|
|
626
|
+
>
|
|
627
|
+
{testing ? (
|
|
628
|
+
<>
|
|
629
|
+
<svg
|
|
630
|
+
className="animate-spin -ml-1 mr-2 h-3 w-3 text-white"
|
|
631
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
632
|
+
fill="none"
|
|
633
|
+
viewBox="0 0 24 24"
|
|
634
|
+
>
|
|
635
|
+
<circle
|
|
636
|
+
className="opacity-25"
|
|
637
|
+
cx="12"
|
|
638
|
+
cy="12"
|
|
639
|
+
r="10"
|
|
640
|
+
stroke="currentColor"
|
|
641
|
+
strokeWidth="4"
|
|
642
|
+
></circle>
|
|
643
|
+
<path
|
|
644
|
+
className="opacity-75"
|
|
645
|
+
fill="currentColor"
|
|
646
|
+
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
|
|
647
|
+
></path>
|
|
648
|
+
</svg>
|
|
649
|
+
测试中...
|
|
650
|
+
</>
|
|
651
|
+
) : (
|
|
652
|
+
<>
|
|
653
|
+
<svg
|
|
654
|
+
className="w-3 h-3 mr-1.5"
|
|
655
|
+
fill="none"
|
|
656
|
+
stroke="currentColor"
|
|
657
|
+
viewBox="0 0 24 24"
|
|
658
|
+
>
|
|
659
|
+
<path
|
|
660
|
+
strokeLinecap="round"
|
|
661
|
+
strokeLinejoin="round"
|
|
662
|
+
strokeWidth={2}
|
|
663
|
+
d="M14.752 11.168l-3.197-2.132A1 1 0 0010 9.87v4.263a1 1 0 001.555.832l3.197-2.132a1 1 0 000-1.664z"
|
|
664
|
+
/>
|
|
665
|
+
<path
|
|
666
|
+
strokeLinecap="round"
|
|
667
|
+
strokeLinejoin="round"
|
|
668
|
+
strokeWidth={2}
|
|
669
|
+
d="M21 12a9 9 0 11-18 0 9 9 0 0118 0z"
|
|
670
|
+
/>
|
|
671
|
+
</svg>
|
|
672
|
+
测试连接
|
|
673
|
+
</>
|
|
674
|
+
)}
|
|
675
|
+
</button>
|
|
676
|
+
|
|
677
|
+
<button
|
|
678
|
+
onClick={() => setShowExampleConfig(!showExampleConfig)}
|
|
679
|
+
disabled={!selectedModel}
|
|
680
|
+
className="inline-flex items-center px-3.5 py-2 border border-slate-200 rounded-lg text-xs font-semibold text-slate-600 bg-white hover:bg-slate-50 focus:outline-none focus:ring-2 focus:ring-offset-1 focus:ring-slate-400/30 disabled:opacity-50 disabled:cursor-not-allowed transition-all duration-300"
|
|
681
|
+
>
|
|
682
|
+
{showExampleConfig ? '隐藏' : '显示'}示例配置
|
|
683
|
+
</button>
|
|
684
|
+
</div>
|
|
685
|
+
</div>
|
|
686
|
+
|
|
687
|
+
{/* Example Config */}
|
|
688
|
+
{showExampleConfig && selectedModel && getExampleConfig() && (
|
|
689
|
+
<div className="mt-4 p-4 rounded-lg bg-slate-50 border border-slate-200">
|
|
690
|
+
<div className="flex items-center justify-between mb-2">
|
|
691
|
+
<label className="block text-xs font-semibold text-slate-700">
|
|
692
|
+
示例配置 (JSON)
|
|
693
|
+
</label>
|
|
694
|
+
<button
|
|
695
|
+
type="button"
|
|
696
|
+
onClick={() => {
|
|
697
|
+
const config = getExampleConfig();
|
|
698
|
+
if (config) {
|
|
699
|
+
navigator.clipboard.writeText(JSON.stringify(config, null, 2));
|
|
700
|
+
showToast('配置已复制到剪贴板', 'success');
|
|
701
|
+
}
|
|
702
|
+
}}
|
|
703
|
+
className="text-xs text-blue-600 hover:text-blue-700 font-medium transition-colors"
|
|
704
|
+
>
|
|
705
|
+
复制
|
|
706
|
+
</button>
|
|
707
|
+
</div>
|
|
708
|
+
<pre className="text-[10px] text-slate-700 overflow-x-auto bg-white p-3 rounded border border-slate-200">
|
|
709
|
+
{JSON.stringify(getExampleConfig(), null, 2)}
|
|
710
|
+
</pre>
|
|
711
|
+
</div>
|
|
712
|
+
)}
|
|
713
|
+
|
|
714
|
+
{/* Test Result */}
|
|
715
|
+
{testResult && (
|
|
716
|
+
<div className="mt-4 p-4 rounded-lg border border-slate-200 bg-slate-50">
|
|
717
|
+
<div className="flex items-center justify-between mb-2">
|
|
718
|
+
<label className="block text-xs font-semibold text-slate-700">
|
|
719
|
+
测试结果
|
|
720
|
+
</label>
|
|
721
|
+
<span
|
|
722
|
+
className={`text-xs font-semibold ${
|
|
723
|
+
testResult.success ? 'text-emerald-600' : 'text-rose-600'
|
|
724
|
+
}`}
|
|
725
|
+
>
|
|
726
|
+
{testResult.success ? '成功' : '失败'}
|
|
727
|
+
</span>
|
|
728
|
+
</div>
|
|
729
|
+
<pre className="text-[10px] text-slate-700 overflow-x-auto bg-white p-3 rounded border border-slate-200 max-h-64 overflow-y-auto">
|
|
730
|
+
{JSON.stringify(testResult, null, 2)}
|
|
731
|
+
</pre>
|
|
732
|
+
</div>
|
|
733
|
+
)}
|
|
734
|
+
</div>
|
|
735
|
+
</div>
|
|
736
|
+
</div>
|
|
737
|
+
)}
|
|
738
|
+
</div>
|
|
739
|
+
</main>
|
|
740
|
+
</div>
|
|
741
|
+
);
|
|
742
|
+
}
|