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.
Files changed (166) hide show
  1. package/.claude/commands/openspec/apply.md +23 -0
  2. package/.claude/commands/openspec/archive.md +27 -0
  3. package/.claude/commands/openspec/proposal.md +28 -0
  4. package/.claude/settings.local.json +12 -0
  5. package/.claude/skills/ui-ux-pro-max/SKILL.md +228 -0
  6. package/.claude/skills/ui-ux-pro-max/data/charts.csv +26 -0
  7. package/.claude/skills/ui-ux-pro-max/data/colors.csv +97 -0
  8. package/.claude/skills/ui-ux-pro-max/data/landing.csv +31 -0
  9. package/.claude/skills/ui-ux-pro-max/data/products.csv +97 -0
  10. package/.claude/skills/ui-ux-pro-max/data/prompts.csv +24 -0
  11. package/.claude/skills/ui-ux-pro-max/data/stacks/flutter.csv +53 -0
  12. package/.claude/skills/ui-ux-pro-max/data/stacks/html-tailwind.csv +56 -0
  13. package/.claude/skills/ui-ux-pro-max/data/stacks/nextjs.csv +53 -0
  14. package/.claude/skills/ui-ux-pro-max/data/stacks/nuxt-ui.csv +51 -0
  15. package/.claude/skills/ui-ux-pro-max/data/stacks/nuxtjs.csv +59 -0
  16. package/.claude/skills/ui-ux-pro-max/data/stacks/react-native.csv +52 -0
  17. package/.claude/skills/ui-ux-pro-max/data/stacks/react.csv +54 -0
  18. package/.claude/skills/ui-ux-pro-max/data/stacks/svelte.csv +54 -0
  19. package/.claude/skills/ui-ux-pro-max/data/stacks/swiftui.csv +51 -0
  20. package/.claude/skills/ui-ux-pro-max/data/stacks/vue.csv +50 -0
  21. package/.claude/skills/ui-ux-pro-max/data/styles.csv +59 -0
  22. package/.claude/skills/ui-ux-pro-max/data/typography.csv +58 -0
  23. package/.claude/skills/ui-ux-pro-max/data/ux-guidelines.csv +100 -0
  24. package/.claude/skills/ui-ux-pro-max/scripts/__pycache__/core.cpython-311.pyc +0 -0
  25. package/.claude/skills/ui-ux-pro-max/scripts/core.py +238 -0
  26. package/.claude/skills/ui-ux-pro-max/scripts/search.py +61 -0
  27. package/.cursor/commands/openspec-apply.md +23 -0
  28. package/.cursor/commands/openspec-archive.md +27 -0
  29. package/.cursor/commands/openspec-proposal.md +28 -0
  30. package/.cursor/commands/ui-ux-pro-max.md +226 -0
  31. package/.eslintrc.json +3 -0
  32. package/.shared/ui-ux-pro-max/data/charts.csv +26 -0
  33. package/.shared/ui-ux-pro-max/data/colors.csv +97 -0
  34. package/.shared/ui-ux-pro-max/data/landing.csv +31 -0
  35. package/.shared/ui-ux-pro-max/data/products.csv +97 -0
  36. package/.shared/ui-ux-pro-max/data/prompts.csv +24 -0
  37. package/.shared/ui-ux-pro-max/data/stacks/flutter.csv +53 -0
  38. package/.shared/ui-ux-pro-max/data/stacks/html-tailwind.csv +56 -0
  39. package/.shared/ui-ux-pro-max/data/stacks/nextjs.csv +53 -0
  40. package/.shared/ui-ux-pro-max/data/stacks/nuxt-ui.csv +51 -0
  41. package/.shared/ui-ux-pro-max/data/stacks/nuxtjs.csv +59 -0
  42. package/.shared/ui-ux-pro-max/data/stacks/react-native.csv +52 -0
  43. package/.shared/ui-ux-pro-max/data/stacks/react.csv +54 -0
  44. package/.shared/ui-ux-pro-max/data/stacks/svelte.csv +54 -0
  45. package/.shared/ui-ux-pro-max/data/stacks/swiftui.csv +51 -0
  46. package/.shared/ui-ux-pro-max/data/stacks/vue.csv +50 -0
  47. package/.shared/ui-ux-pro-max/data/styles.csv +59 -0
  48. package/.shared/ui-ux-pro-max/data/typography.csv +58 -0
  49. package/.shared/ui-ux-pro-max/data/ux-guidelines.csv +100 -0
  50. package/.shared/ui-ux-pro-max/scripts/core.py +238 -0
  51. package/.shared/ui-ux-pro-max/scripts/search.py +61 -0
  52. package/AGENTS.md +18 -0
  53. package/CLAUDE.md +18 -0
  54. package/IMPLEMENTATION.md +157 -0
  55. package/LICENSE +21 -0
  56. package/README.md +165 -0
  57. package/dist/.next/types/app/api/config/route.js +52 -0
  58. package/dist/.next/types/app/api/gateway/[...path]/route.js +52 -0
  59. package/dist/.next/types/app/api/gateway/route.js +52 -0
  60. package/dist/.next/types/app/api/logs/route.js +52 -0
  61. package/dist/.next/types/app/api/models/route.js +52 -0
  62. package/dist/.next/types/app/api/providers/route.js +52 -0
  63. package/dist/.next/types/app/api/providers/test/route.js +52 -0
  64. package/dist/.next/types/app/api/service/start/route.js +52 -0
  65. package/dist/.next/types/app/api/service/status/route.js +52 -0
  66. package/dist/.next/types/app/api/service/stop/route.js +52 -0
  67. package/dist/.next/types/app/layout.js +22 -0
  68. package/dist/.next/types/app/logs/page.js +22 -0
  69. package/dist/.next/types/app/models/page.js +22 -0
  70. package/dist/.next/types/app/page.js +22 -0
  71. package/dist/.next/types/app/providers/page.js +22 -0
  72. package/dist/src/app/api/config/route.js +43 -0
  73. package/dist/src/app/api/gateway/[...path]/route.js +83 -0
  74. package/dist/src/app/api/gateway/route.js +63 -0
  75. package/dist/src/app/api/logs/route.js +34 -0
  76. package/dist/src/app/api/models/route.js +152 -0
  77. package/dist/src/app/api/providers/route.js +118 -0
  78. package/dist/src/app/api/providers/test/route.js +154 -0
  79. package/dist/src/app/api/service/start/route.js +55 -0
  80. package/dist/src/app/api/service/status/route.js +17 -0
  81. package/dist/src/app/api/service/stop/route.js +20 -0
  82. package/dist/src/app/components/ConfirmDialog.jsx +31 -0
  83. package/dist/src/app/components/Nav.jsx +45 -0
  84. package/dist/src/app/components/Toast.jsx +37 -0
  85. package/dist/src/app/components/ToastProvider.jsx +21 -0
  86. package/dist/src/app/layout.jsx +13 -0
  87. package/dist/src/app/logs/page.jsx +210 -0
  88. package/dist/src/app/models/page.jsx +291 -0
  89. package/dist/src/app/page.jsx +236 -0
  90. package/dist/src/app/providers/page.jsx +402 -0
  91. package/dist/src/cli/index.js +90 -0
  92. package/dist/src/db/database.js +69 -0
  93. package/dist/src/db/queries.js +261 -0
  94. package/dist/src/db/schema.js +67 -0
  95. package/dist/src/server/crypto.js +22 -0
  96. package/dist/src/server/gateway-server.js +200 -0
  97. package/dist/src/server/gateway.js +76 -0
  98. package/dist/src/server/logger.js +72 -0
  99. package/dist/src/server/providers/anthropic.js +52 -0
  100. package/dist/src/server/providers/gemini.js +64 -0
  101. package/dist/src/server/providers/index.js +16 -0
  102. package/dist/src/server/providers/openai.js +86 -0
  103. package/dist/src/server/providers/types.js +1 -0
  104. package/dist/src/server/service-manager.js +286 -0
  105. package/docs/TODO.md +19 -0
  106. package/next.config.js +7 -0
  107. package/openspec/AGENTS.md +456 -0
  108. package/openspec/changes/add-logging/proposal.md +18 -0
  109. package/openspec/changes/add-logging/specs/core/spec.md +21 -0
  110. package/openspec/changes/add-logging/tasks.md +16 -0
  111. package/openspec/changes/add-provider-test-connection/proposal.md +22 -0
  112. package/openspec/changes/add-provider-test-connection/specs/model-provider/spec.md +68 -0
  113. package/openspec/changes/add-provider-test-connection/tasks.md +31 -0
  114. package/openspec/changes/improve-gateway-startup/design.md +137 -0
  115. package/openspec/changes/improve-gateway-startup/proposal.md +33 -0
  116. package/openspec/changes/improve-gateway-startup/specs/api-gateway/spec.md +94 -0
  117. package/openspec/changes/improve-gateway-startup/specs/web-ui/spec.md +67 -0
  118. package/openspec/changes/improve-gateway-startup/tasks.md +47 -0
  119. package/openspec/changes/init-api-gateway/design.md +185 -0
  120. package/openspec/changes/init-api-gateway/proposal.md +30 -0
  121. package/openspec/changes/init-api-gateway/specs/api-gateway/spec.md +42 -0
  122. package/openspec/changes/init-api-gateway/specs/cli-tool/spec.md +40 -0
  123. package/openspec/changes/init-api-gateway/specs/model-management/spec.md +47 -0
  124. package/openspec/changes/init-api-gateway/specs/model-provider/spec.md +33 -0
  125. package/openspec/changes/init-api-gateway/specs/request-logging/spec.md +54 -0
  126. package/openspec/changes/init-api-gateway/specs/web-ui/spec.md +49 -0
  127. package/openspec/changes/init-api-gateway/tasks.md +84 -0
  128. package/openspec/project.md +58 -0
  129. package/package.json +51 -0
  130. package/postcss.config.js +6 -0
  131. package/src/app/api/config/route.ts +62 -0
  132. package/src/app/api/gateway/[...path]/route.ts +118 -0
  133. package/src/app/api/gateway/route.ts +77 -0
  134. package/src/app/api/logs/route.ts +48 -0
  135. package/src/app/api/models/route.ts +210 -0
  136. package/src/app/api/providers/route.ts +162 -0
  137. package/src/app/api/providers/test/route.ts +182 -0
  138. package/src/app/api/service/start/route.ts +73 -0
  139. package/src/app/api/service/status/route.ts +22 -0
  140. package/src/app/api/service/stop/route.ts +27 -0
  141. package/src/app/components/ConfirmDialog.tsx +63 -0
  142. package/src/app/components/Nav.tsx +66 -0
  143. package/src/app/components/Toast.tsx +61 -0
  144. package/src/app/components/ToastProvider.tsx +43 -0
  145. package/src/app/globals.css +71 -0
  146. package/src/app/layout.tsx +22 -0
  147. package/src/app/logs/page.tsx +261 -0
  148. package/src/app/models/page.tsx +500 -0
  149. package/src/app/page.tsx +742 -0
  150. package/src/app/providers/page.tsx +558 -0
  151. package/src/cli/index.ts +95 -0
  152. package/src/db/database.ts +125 -0
  153. package/src/db/queries.ts +339 -0
  154. package/src/db/schema.ts +117 -0
  155. package/src/server/crypto.ts +48 -0
  156. package/src/server/gateway-server.ts +306 -0
  157. package/src/server/gateway.ts +163 -0
  158. package/src/server/logger.ts +96 -0
  159. package/src/server/providers/anthropic.ts +121 -0
  160. package/src/server/providers/gemini.ts +112 -0
  161. package/src/server/providers/index.ts +20 -0
  162. package/src/server/providers/openai.ts +235 -0
  163. package/src/server/providers/types.ts +20 -0
  164. package/src/server/service-manager.ts +321 -0
  165. package/tailwind.config.js +16 -0
  166. package/tsconfig.json +29 -0
@@ -0,0 +1,500 @@
1
+ 'use client';
2
+
3
+ import { useState, useEffect } from 'react';
4
+ import Nav from '../components/Nav';
5
+ import { useToast } from '../components/ToastProvider';
6
+ import ConfirmDialog from '../components/ConfirmDialog';
7
+
8
+ interface Model {
9
+ id: number;
10
+ provider_id: number;
11
+ name: string;
12
+ model_id: string;
13
+ enabled: boolean;
14
+ provider_name?: string;
15
+ provider_protocol?: string;
16
+ }
17
+
18
+ interface Provider {
19
+ id: number;
20
+ name: string;
21
+ protocol: string;
22
+ }
23
+
24
+ export default function ModelsPage() {
25
+ const [models, setModels] = useState<Model[]>([]);
26
+ const [filteredModels, setFilteredModels] = useState<Model[]>([]);
27
+ const [providers, setProviders] = useState<Provider[]>([]);
28
+ const [loading, setLoading] = useState(true);
29
+ const [showModal, setShowModal] = useState(false);
30
+ const [showDeleteConfirm, setShowDeleteConfirm] = useState(false);
31
+ const [showFetchConfirm, setShowFetchConfirm] = useState(false);
32
+ const [deleteId, setDeleteId] = useState<number | null>(null);
33
+ const [fetchProviderId, setFetchProviderId] = useState<number | null>(null);
34
+ const [selectedProviderId, setSelectedProviderId] = useState<number | null>(null);
35
+ const [searchQuery, setSearchQuery] = useState('');
36
+ const [formData, setFormData] = useState({
37
+ provider_id: '',
38
+ name: '',
39
+ model_id: '',
40
+ enabled: true,
41
+ });
42
+ const { showToast } = useToast();
43
+
44
+ useEffect(() => {
45
+ loadData();
46
+ }, []);
47
+
48
+ useEffect(() => {
49
+ filterModels();
50
+ }, [models, selectedProviderId, searchQuery]);
51
+
52
+ const filterModels = () => {
53
+ let filtered = [...models];
54
+
55
+ // 按供应商筛选
56
+ if (selectedProviderId !== null) {
57
+ filtered = filtered.filter(model => model.provider_id === selectedProviderId);
58
+ }
59
+
60
+ // 搜索筛选
61
+ if (searchQuery.trim()) {
62
+ const query = searchQuery.toLowerCase().trim();
63
+ filtered = filtered.filter(model =>
64
+ model.name.toLowerCase().includes(query) ||
65
+ model.model_id.toLowerCase().includes(query) ||
66
+ model.provider_name?.toLowerCase().includes(query) ||
67
+ model.provider_protocol?.toLowerCase().includes(query)
68
+ );
69
+ }
70
+
71
+ setFilteredModels(filtered);
72
+ };
73
+
74
+ const loadData = async () => {
75
+ try {
76
+ const [modelsRes, providersRes] = await Promise.all([
77
+ fetch('/api/models'),
78
+ fetch('/api/providers'),
79
+ ]);
80
+ const modelsData = await modelsRes.json();
81
+ const providersData = await providersRes.json();
82
+ setModels(modelsData);
83
+ setProviders(providersData);
84
+ } catch (error) {
85
+ console.error('Failed to load data:', error);
86
+ } finally {
87
+ setLoading(false);
88
+ }
89
+ };
90
+
91
+ const handleSubmit = async (e: React.FormEvent) => {
92
+ e.preventDefault();
93
+ try {
94
+ await fetch('/api/models', {
95
+ method: 'POST',
96
+ headers: { 'Content-Type': 'application/json' },
97
+ body: JSON.stringify({
98
+ ...formData,
99
+ provider_id: parseInt(formData.provider_id),
100
+ }),
101
+ });
102
+ setShowModal(false);
103
+ setFormData({ provider_id: '', name: '', model_id: '', enabled: true });
104
+ loadData();
105
+ showToast('模型添加成功', 'success');
106
+ } catch (error) {
107
+ console.error('Failed to save model:', error);
108
+ showToast('保存失败', 'error');
109
+ }
110
+ };
111
+
112
+ const handleToggleEnabled = async (id: number, enabled: boolean) => {
113
+ try {
114
+ await fetch('/api/models', {
115
+ method: 'PUT',
116
+ headers: { 'Content-Type': 'application/json' },
117
+ body: JSON.stringify({ id, enabled: !enabled }),
118
+ });
119
+ loadData();
120
+ } catch (error) {
121
+ console.error('Failed to update model:', error);
122
+ }
123
+ };
124
+
125
+ const handleDelete = (id: number) => {
126
+ setDeleteId(id);
127
+ setShowDeleteConfirm(true);
128
+ };
129
+
130
+ const confirmDelete = async () => {
131
+ if (deleteId) {
132
+ try {
133
+ await fetch(`/api/models?id=${deleteId}`, { method: 'DELETE' });
134
+ loadData();
135
+ showToast('模型已删除', 'success');
136
+ } catch (error) {
137
+ console.error('Failed to delete model:', error);
138
+ showToast('删除失败', 'error');
139
+ }
140
+ }
141
+ setShowDeleteConfirm(false);
142
+ setDeleteId(null);
143
+ };
144
+
145
+ const handleFetchModels = (providerId: number) => {
146
+ setFetchProviderId(providerId);
147
+ setShowFetchConfirm(true);
148
+ };
149
+
150
+ const confirmFetch = async () => {
151
+ if (fetchProviderId) {
152
+ try {
153
+ const res = await fetch('/api/models', {
154
+ method: 'PATCH',
155
+ headers: { 'Content-Type': 'application/json' },
156
+ body: JSON.stringify({ provider_id: fetchProviderId }),
157
+ });
158
+ const data = await res.json();
159
+ if (!res.ok) {
160
+ showToast(data.error || '拉取失败', 'error');
161
+ return;
162
+ }
163
+ const count = data.count ?? 0;
164
+ showToast(`成功拉取 ${count} 个模型`, 'success');
165
+ loadData();
166
+ } catch (error: any) {
167
+ console.error('Failed to fetch models:', error);
168
+ showToast('拉取失败: ' + error.message, 'error');
169
+ }
170
+ }
171
+ setShowFetchConfirm(false);
172
+ setFetchProviderId(null);
173
+ };
174
+
175
+ if (loading) {
176
+ return (
177
+ <>
178
+ <Nav />
179
+ <div className="min-h-screen flex items-center justify-center">
180
+ <div className="text-lg">加载中...</div>
181
+ </div>
182
+ </>
183
+ );
184
+ }
185
+
186
+ return (
187
+ <>
188
+ <Nav />
189
+ <main className="max-w-6xl mx-auto py-6 sm:px-6 lg:px-8">
190
+ <div className="px-4 py-4 sm:px-0">
191
+ <div className="flex justify-between items-center mb-5">
192
+ <div>
193
+ <h1 className="text-lg font-bold text-slate-800">模型管理</h1>
194
+ <p className="text-xs text-slate-500 mt-1">管理 AI 模型配置和状态</p>
195
+ </div>
196
+ <button
197
+ onClick={() => {
198
+ setFormData({ provider_id: '', name: '', model_id: '', enabled: true });
199
+ setShowModal(true);
200
+ }}
201
+ 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 transition-all duration-300"
202
+ >
203
+ <svg className="w-3.5 h-3.5 mr-1.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
204
+ <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 4v16m8-8H4" />
205
+ </svg>
206
+ 手动添加
207
+ </button>
208
+ </div>
209
+
210
+ {/* 一键拉取模型列表 - 移到列表上方 */}
211
+ <div className="mb-5 bg-white/70 backdrop-blur-sm shadow-md rounded-2xl border border-emerald-100/50 p-5">
212
+ <div className="flex items-center justify-between mb-4">
213
+ <div>
214
+ <h2 className="text-sm font-bold text-slate-800">快捷拉取</h2>
215
+ <p className="text-xs text-slate-500 mt-0.5">从供应商 API 自动拉取可用模型</p>
216
+ </div>
217
+ </div>
218
+ <div className="grid grid-cols-1 gap-2.5 sm:grid-cols-2 lg:grid-cols-3">
219
+ {providers.map((provider) => (
220
+ <div key={provider.id} className="flex items-center justify-between p-3 border border-slate-200 rounded-xl hover:border-emerald-300 hover:shadow-sm transition-all duration-300 cursor-pointer bg-white/80 backdrop-blur-sm">
221
+ <div className="flex-1">
222
+ <div className="font-semibold text-xs text-slate-800">{provider.name}</div>
223
+ <div className="text-[10px] text-slate-500 mt-0.5">{provider.protocol}</div>
224
+ </div>
225
+ <button
226
+ onClick={() => handleFetchModels(provider.id)}
227
+ className="ml-3 inline-flex items-center px-2.5 py-1 border border-transparent text-[10px] font-semibold rounded-lg text-emerald-700 bg-emerald-50 hover:bg-emerald-100 transition-all duration-300"
228
+ >
229
+ <svg className="w-3 h-3 mr-1" fill="none" stroke="currentColor" viewBox="0 0 24 24">
230
+ <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15" />
231
+ </svg>
232
+ 拉取
233
+ </button>
234
+ </div>
235
+ ))}
236
+ </div>
237
+ </div>
238
+
239
+ {/* 筛选和搜索栏 */}
240
+ <div className="mb-5 bg-white/70 backdrop-blur-sm shadow-md rounded-2xl border border-emerald-100/50 p-5">
241
+ <div className="flex flex-col sm:flex-row gap-4">
242
+ {/* 供应商筛选 */}
243
+ <div className="flex-1">
244
+ <label className="block text-xs font-semibold text-slate-700 mb-1.5">按供应商筛选</label>
245
+ <select
246
+ value={selectedProviderId ?? ''}
247
+ onChange={(e) => setSelectedProviderId(e.target.value ? parseInt(e.target.value) : null)}
248
+ 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"
249
+ >
250
+ <option value="">全部供应商</option>
251
+ {providers.map((provider) => (
252
+ <option key={provider.id} value={provider.id}>
253
+ {provider.name} ({provider.protocol})
254
+ </option>
255
+ ))}
256
+ </select>
257
+ </div>
258
+ {/* 搜索框 */}
259
+ <div className="flex-1">
260
+ <label className="block text-xs font-semibold text-slate-700 mb-1.5">搜索模型</label>
261
+ <div className="relative">
262
+ <input
263
+ type="text"
264
+ value={searchQuery}
265
+ onChange={(e) => setSearchQuery(e.target.value)}
266
+ placeholder="搜索模型名称、ID、供应商..."
267
+ className="block w-full rounded-lg border border-slate-200 bg-white/80 px-3 py-2 pl-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"
268
+ />
269
+ <svg className="absolute left-2.5 top-1/2 -translate-y-1/2 w-4 h-4 text-slate-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
270
+ <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z" />
271
+ </svg>
272
+ {searchQuery && (
273
+ <button
274
+ onClick={() => setSearchQuery('')}
275
+ className="absolute right-2 top-1/2 -translate-y-1/2 p-1 text-slate-400 hover:text-slate-600 transition-colors"
276
+ >
277
+ <svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
278
+ <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
279
+ </svg>
280
+ </button>
281
+ )}
282
+ </div>
283
+ </div>
284
+ </div>
285
+ {/* 结果统计 */}
286
+ {(selectedProviderId !== null || searchQuery.trim()) && (
287
+ <div className="mt-3 text-xs text-slate-500">
288
+ 显示 {filteredModels.length} / {models.length} 个模型
289
+ </div>
290
+ )}
291
+ </div>
292
+
293
+ <div className="bg-white/70 backdrop-blur-sm shadow-md rounded-2xl border border-emerald-100/50 overflow-hidden">
294
+ <table className="min-w-full divide-y divide-slate-100">
295
+ <thead className="bg-emerald-50/30">
296
+ <tr>
297
+ <th className="px-4 py-2.5 text-left text-xs font-semibold text-slate-600 uppercase tracking-wider">
298
+ 名称
299
+ </th>
300
+ <th className="px-6 py-3.5 text-left text-xs font-semibold text-gray-700 uppercase tracking-wider">
301
+ 模型 ID
302
+ </th>
303
+ <th className="px-6 py-3.5 text-left text-xs font-semibold text-gray-700 uppercase tracking-wider">
304
+ 供应商
305
+ </th>
306
+ <th className="px-6 py-3.5 text-left text-xs font-semibold text-gray-700 uppercase tracking-wider">
307
+ 状态
308
+ </th>
309
+ <th className="px-6 py-3.5 text-right text-xs font-semibold text-gray-700 uppercase tracking-wider">
310
+ 操作
311
+ </th>
312
+ </tr>
313
+ </thead>
314
+ <tbody className="bg-white divide-y divide-slate-100">
315
+ {filteredModels.length === 0 ? (
316
+ <tr>
317
+ <td colSpan={5} className="px-4 py-12 text-center">
318
+ <div className="text-slate-400">
319
+ {models.length === 0 ? (
320
+ <>
321
+ <svg className="mx-auto h-10 w-10 mb-3 text-slate-300" fill="none" stroke="currentColor" viewBox="0 0 24 24">
322
+ <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={1.5} d="M20 13V6a2 2 0 00-2-2H6a2 2 0 00-2 2v7m16 0v5a2 2 0 01-2 2H6a2 2 0 01-2-2v-5m16 0h-2.586a1 1 0 00-.707.293l-2.414 2.414a1 1 0 01-.707.293h-3.172a1 1 0 01-.707-.293l-2.414-2.414A1 1 0 006.586 13H4" />
323
+ </svg>
324
+ <p className="text-xs text-slate-500">暂无模型,请先添加或拉取模型</p>
325
+ </>
326
+ ) : (
327
+ <>
328
+ <svg className="mx-auto h-10 w-10 mb-3 text-slate-300" fill="none" stroke="currentColor" viewBox="0 0 24 24">
329
+ <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={1.5} d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z" />
330
+ </svg>
331
+ <p className="text-xs text-slate-500">没有找到匹配的模型</p>
332
+ {(selectedProviderId !== null || searchQuery.trim()) && (
333
+ <button
334
+ onClick={() => {
335
+ setSelectedProviderId(null);
336
+ setSearchQuery('');
337
+ }}
338
+ className="mt-2 text-xs text-emerald-600 hover:text-emerald-700 underline"
339
+ >
340
+ 清除筛选条件
341
+ </button>
342
+ )}
343
+ </>
344
+ )}
345
+ </div>
346
+ </td>
347
+ </tr>
348
+ ) : (
349
+ filteredModels.map((model) => (
350
+ <tr key={model.id} className="hover:bg-emerald-50/20 transition-colors duration-300">
351
+ <td className="px-4 py-3 whitespace-nowrap">
352
+ <div className="text-xs font-medium text-slate-800">{model.name}</div>
353
+ </td>
354
+ <td className="px-4 py-3 whitespace-nowrap">
355
+ <div className="text-xs text-slate-600 font-mono">{model.model_id}</div>
356
+ </td>
357
+ <td className="px-4 py-3 whitespace-nowrap">
358
+ <div className="text-xs text-slate-600">
359
+ <span className="font-medium">{model.provider_name}</span>
360
+ <span className="text-slate-400 ml-1">({model.provider_protocol})</span>
361
+ </div>
362
+ </td>
363
+ <td className="px-4 py-3 whitespace-nowrap">
364
+ <span className={`px-2 py-0.5 inline-flex text-xs leading-4 font-semibold rounded-full border ${
365
+ model.enabled
366
+ ? 'bg-emerald-100/80 text-emerald-700 border-emerald-200/50'
367
+ : 'bg-slate-100/80 text-slate-600 border-slate-200/50'
368
+ }`}>
369
+ {model.enabled ? '启用' : '禁用'}
370
+ </span>
371
+ </td>
372
+ <td className="px-4 py-3 whitespace-nowrap text-right text-xs font-medium">
373
+ <div className="flex items-center justify-end space-x-3">
374
+ <button
375
+ onClick={() => handleToggleEnabled(model.id, model.enabled)}
376
+ className="text-emerald-600 hover:text-emerald-700 font-medium transition-colors duration-300"
377
+ >
378
+ {model.enabled ? '禁用' : '启用'}
379
+ </button>
380
+ <button
381
+ onClick={() => handleDelete(model.id)}
382
+ className="text-rose-500 hover:text-rose-600 font-medium transition-colors duration-300"
383
+ >
384
+ 删除
385
+ </button>
386
+ </div>
387
+ </td>
388
+ </tr>
389
+ ))
390
+ )}
391
+ </tbody>
392
+ </table>
393
+ </div>
394
+ </div>
395
+ </main>
396
+
397
+ {showModal && (
398
+ <div className="fixed z-50 inset-0 overflow-y-auto">
399
+ <div className="flex items-center justify-center min-h-screen pt-4 px-4 pb-20 text-center sm:block sm:p-0">
400
+ <div className="fixed inset-0 bg-slate-900/40 backdrop-blur-sm transition-opacity" onClick={() => setShowModal(false)}></div>
401
+ <div className="inline-block align-bottom bg-white rounded-2xl text-left overflow-hidden shadow-xl transform transition-all sm:my-8 sm:align-middle sm:max-w-md sm:w-full border border-emerald-100/50">
402
+ <form onSubmit={handleSubmit} className="bg-white px-6 pt-5 pb-4 sm:p-6">
403
+ <div className="mb-5">
404
+ <h3 className="text-base font-bold text-slate-800 mb-1">添加模型</h3>
405
+ <p className="text-xs text-slate-500">手动添加新的 AI 模型</p>
406
+ </div>
407
+ <div className="space-y-4">
408
+ <div>
409
+ <label className="block text-xs font-semibold text-slate-700 mb-1.5">供应商</label>
410
+ <select
411
+ required
412
+ value={formData.provider_id}
413
+ onChange={(e) => setFormData({ ...formData, provider_id: e.target.value })}
414
+ autoComplete="off"
415
+ 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"
416
+ >
417
+ <option value="">选择供应商</option>
418
+ {providers.map((p) => (
419
+ <option key={p.id} value={p.id}>
420
+ {p.name}
421
+ </option>
422
+ ))}
423
+ </select>
424
+ </div>
425
+ <div>
426
+ <label className="block text-xs font-semibold text-slate-700 mb-1.5">名称</label>
427
+ <input
428
+ type="text"
429
+ required
430
+ value={formData.name}
431
+ onChange={(e) => setFormData({ ...formData, name: e.target.value })}
432
+ autoComplete="off"
433
+ 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"
434
+ placeholder="例如: GPT-4"
435
+ />
436
+ </div>
437
+ <div>
438
+ <label className="block text-xs font-semibold text-slate-700 mb-1.5">模型 ID</label>
439
+ <input
440
+ type="text"
441
+ required
442
+ value={formData.model_id}
443
+ onChange={(e) => setFormData({ ...formData, model_id: e.target.value })}
444
+ autoComplete="off"
445
+ 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"
446
+ placeholder="例如: gpt-4"
447
+ />
448
+ </div>
449
+ </div>
450
+ <div className="mt-5 flex items-center justify-end space-x-2">
451
+ <button
452
+ type="button"
453
+ onClick={() => setShowModal(false)}
454
+ className="px-3.5 py-1.5 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 transition-all duration-300"
455
+ >
456
+ 取消
457
+ </button>
458
+ <button
459
+ type="submit"
460
+ className="px-4 py-1.5 border border-transparent rounded-lg text-xs font-semibold text-white bg-emerald-600 hover:bg-emerald-700 focus:outline-none focus:ring-2 focus:ring-offset-1 focus:ring-emerald-500/50 transition-all duration-300"
461
+ >
462
+ 保存
463
+ </button>
464
+ </div>
465
+ </form>
466
+ </div>
467
+ </div>
468
+ </div>
469
+ )}
470
+
471
+ <ConfirmDialog
472
+ open={showDeleteConfirm}
473
+ title="确认删除"
474
+ message="确定要删除这个模型吗?此操作不可恢复。"
475
+ onConfirm={confirmDelete}
476
+ onCancel={() => {
477
+ setShowDeleteConfirm(false);
478
+ setDeleteId(null);
479
+ }}
480
+ confirmText="删除"
481
+ cancelText="取消"
482
+ type="danger"
483
+ />
484
+
485
+ <ConfirmDialog
486
+ open={showFetchConfirm}
487
+ title="拉取模型列表"
488
+ message="确定要从供应商拉取模型列表吗?"
489
+ onConfirm={confirmFetch}
490
+ onCancel={() => {
491
+ setShowFetchConfirm(false);
492
+ setFetchProviderId(null);
493
+ }}
494
+ confirmText="拉取"
495
+ cancelText="取消"
496
+ type="info"
497
+ />
498
+ </>
499
+ );
500
+ }