portos-ai-toolkit 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/LICENSE +21 -0
- package/README.md +100 -0
- package/package.json +76 -0
- package/src/client/api.js +102 -0
- package/src/client/components/ProviderDropdown.jsx +39 -0
- package/src/client/components/index.js +5 -0
- package/src/client/hooks/index.js +6 -0
- package/src/client/hooks/useProviders.js +96 -0
- package/src/client/hooks/useRuns.js +94 -0
- package/src/client/index.js +11 -0
- package/src/client/pages/AIProviders.jsx +665 -0
- package/src/index.js +8 -0
- package/src/server/index.d.ts +87 -0
- package/src/server/index.js +95 -0
- package/src/server/prompts.js +234 -0
- package/src/server/providers.js +253 -0
- package/src/server/providers.test.js +120 -0
- package/src/server/routes/prompts.js +96 -0
- package/src/server/routes/providers.js +105 -0
- package/src/server/routes/runs.js +157 -0
- package/src/server/runner.js +475 -0
- package/src/server/validation.js +51 -0
- package/src/shared/constants.js +26 -0
- package/src/shared/index.js +5 -0
|
@@ -0,0 +1,665 @@
|
|
|
1
|
+
import { useState, useEffect } from 'react';
|
|
2
|
+
import { createApiClient } from '../api.js';
|
|
3
|
+
import { io } from 'socket.io-client';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* AIProviders - Full-featured AI provider management page
|
|
7
|
+
*
|
|
8
|
+
* @param {Object} props
|
|
9
|
+
* @param {Function} props.onError - Error handler (e.g., toast.error)
|
|
10
|
+
* @param {string} props.colorPrefix - CSS color prefix (default: 'app')
|
|
11
|
+
*/
|
|
12
|
+
export default function AIProviders({ onError = console.error, colorPrefix = 'app' }) {
|
|
13
|
+
const api = createApiClient({ onError });
|
|
14
|
+
|
|
15
|
+
const [providers, setProviders] = useState([]);
|
|
16
|
+
const [activeProviderId, setActiveProviderId] = useState(null);
|
|
17
|
+
const [loading, setLoading] = useState(true);
|
|
18
|
+
const [showForm, setShowForm] = useState(false);
|
|
19
|
+
const [editingProvider, setEditingProvider] = useState(null);
|
|
20
|
+
const [testResults, setTestResults] = useState({});
|
|
21
|
+
const [runs, setRuns] = useState([]);
|
|
22
|
+
const [showRunPanel, setShowRunPanel] = useState(false);
|
|
23
|
+
const [runPrompt, setRunPrompt] = useState('');
|
|
24
|
+
const [activeRun, setActiveRun] = useState(null);
|
|
25
|
+
const [runOutput, setRunOutput] = useState('');
|
|
26
|
+
const [socket, setSocket] = useState(null);
|
|
27
|
+
|
|
28
|
+
// Color classes using the prefix
|
|
29
|
+
const colors = {
|
|
30
|
+
bg: `bg-${colorPrefix}-bg`,
|
|
31
|
+
card: `bg-${colorPrefix}-card`,
|
|
32
|
+
border: `border-${colorPrefix}-border`,
|
|
33
|
+
accent: `bg-${colorPrefix}-accent`,
|
|
34
|
+
accentHover: `hover:bg-${colorPrefix}-accent/80`,
|
|
35
|
+
accentText: `text-${colorPrefix}-accent`,
|
|
36
|
+
accentBg: `bg-${colorPrefix}-accent/20`,
|
|
37
|
+
success: `bg-${colorPrefix}-success`,
|
|
38
|
+
successText: `text-${colorPrefix}-success`,
|
|
39
|
+
successBg: `bg-${colorPrefix}-success/20`,
|
|
40
|
+
warning: `bg-${colorPrefix}-warning`,
|
|
41
|
+
warningText: `text-${colorPrefix}-warning`,
|
|
42
|
+
warningBg: `bg-${colorPrefix}-warning/20`,
|
|
43
|
+
error: `bg-${colorPrefix}-error`,
|
|
44
|
+
errorText: `text-${colorPrefix}-error`,
|
|
45
|
+
errorBg: `bg-${colorPrefix}-error/20`,
|
|
46
|
+
borderColor: `bg-${colorPrefix}-border`,
|
|
47
|
+
borderHover: `hover:bg-${colorPrefix}-border/80`,
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
useEffect(() => {
|
|
51
|
+
loadData();
|
|
52
|
+
const newSocket = io({ path: '/socket.io' });
|
|
53
|
+
setSocket(newSocket);
|
|
54
|
+
return () => newSocket?.disconnect();
|
|
55
|
+
}, []);
|
|
56
|
+
|
|
57
|
+
useEffect(() => {
|
|
58
|
+
if (!activeRun || !socket) return;
|
|
59
|
+
|
|
60
|
+
const handleData = (data) => {
|
|
61
|
+
setRunOutput(prev => prev + data);
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
const handleComplete = () => {
|
|
65
|
+
setActiveRun(null);
|
|
66
|
+
loadRuns();
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
socket.on(`run:${activeRun}:data`, handleData);
|
|
70
|
+
socket.on(`run:${activeRun}:complete`, handleComplete);
|
|
71
|
+
|
|
72
|
+
return () => {
|
|
73
|
+
socket.off(`run:${activeRun}:data`, handleData);
|
|
74
|
+
socket.off(`run:${activeRun}:complete`, handleComplete);
|
|
75
|
+
};
|
|
76
|
+
}, [activeRun, socket]);
|
|
77
|
+
|
|
78
|
+
const loadData = async () => {
|
|
79
|
+
setLoading(true);
|
|
80
|
+
const [providersData, runsData] = await Promise.all([
|
|
81
|
+
api.providers.getAll().catch(() => ({ providers: [], activeProvider: null })),
|
|
82
|
+
api.runs.list(20).catch(() => ({ runs: [] }))
|
|
83
|
+
]);
|
|
84
|
+
setProviders(providersData.providers || []);
|
|
85
|
+
setActiveProviderId(providersData.activeProvider);
|
|
86
|
+
setRuns(runsData.runs || []);
|
|
87
|
+
setLoading(false);
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
const loadRuns = async () => {
|
|
91
|
+
const runsData = await api.runs.list(20).catch(() => ({ runs: [] }));
|
|
92
|
+
setRuns(runsData.runs || []);
|
|
93
|
+
};
|
|
94
|
+
|
|
95
|
+
const handleSetActive = async (id) => {
|
|
96
|
+
await api.providers.setActive(id);
|
|
97
|
+
setActiveProviderId(id);
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
const handleTest = async (id) => {
|
|
101
|
+
setTestResults(prev => ({ ...prev, [id]: { testing: true } }));
|
|
102
|
+
const result = await api.providers.test(id).catch(err => ({ success: false, error: err.message }));
|
|
103
|
+
setTestResults(prev => ({ ...prev, [id]: result }));
|
|
104
|
+
};
|
|
105
|
+
|
|
106
|
+
const handleDelete = async (id) => {
|
|
107
|
+
await api.providers.delete(id);
|
|
108
|
+
loadData();
|
|
109
|
+
};
|
|
110
|
+
|
|
111
|
+
const handleToggleEnabled = async (provider) => {
|
|
112
|
+
await api.providers.update(provider.id, { enabled: !provider.enabled });
|
|
113
|
+
loadData();
|
|
114
|
+
};
|
|
115
|
+
|
|
116
|
+
const handleRefreshModels = async (id) => {
|
|
117
|
+
await api.providers.refreshModels(id);
|
|
118
|
+
loadData();
|
|
119
|
+
};
|
|
120
|
+
|
|
121
|
+
const handleExecuteRun = async () => {
|
|
122
|
+
if (!runPrompt.trim() || !activeProviderId) return;
|
|
123
|
+
|
|
124
|
+
setRunOutput('');
|
|
125
|
+
const result = await api.runs.create({
|
|
126
|
+
providerId: activeProviderId,
|
|
127
|
+
prompt: runPrompt
|
|
128
|
+
}).catch(err => ({ error: err.message }));
|
|
129
|
+
|
|
130
|
+
if (result.error) {
|
|
131
|
+
setRunOutput(`Error: ${result.error}`);
|
|
132
|
+
return;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
setActiveRun(result.runId);
|
|
136
|
+
};
|
|
137
|
+
|
|
138
|
+
const handleStopRun = async () => {
|
|
139
|
+
if (activeRun) {
|
|
140
|
+
await api.runs.stop(activeRun);
|
|
141
|
+
setActiveRun(null);
|
|
142
|
+
}
|
|
143
|
+
};
|
|
144
|
+
|
|
145
|
+
if (loading) {
|
|
146
|
+
return (
|
|
147
|
+
<div className="flex items-center justify-center h-64">
|
|
148
|
+
<div className="text-gray-400">Loading providers...</div>
|
|
149
|
+
</div>
|
|
150
|
+
);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
return (
|
|
154
|
+
<div className="p-6 space-y-6">
|
|
155
|
+
<div className="flex flex-col sm:flex-row sm:items-center justify-between gap-4">
|
|
156
|
+
<h1 className="text-2xl font-bold text-white">AI Providers</h1>
|
|
157
|
+
<div className="flex flex-wrap gap-2">
|
|
158
|
+
<button
|
|
159
|
+
onClick={() => setShowRunPanel(!showRunPanel)}
|
|
160
|
+
className={`px-4 py-2 ${colors.accent} ${colors.accentHover} text-white rounded-lg transition-colors`}
|
|
161
|
+
>
|
|
162
|
+
{showRunPanel ? 'Hide Runner' : 'Run Prompt'}
|
|
163
|
+
</button>
|
|
164
|
+
<button
|
|
165
|
+
onClick={() => { setEditingProvider(null); setShowForm(true); }}
|
|
166
|
+
className={`px-4 py-2 ${colors.borderColor} ${colors.borderHover} text-white rounded-lg transition-colors`}
|
|
167
|
+
>
|
|
168
|
+
Add Provider
|
|
169
|
+
</button>
|
|
170
|
+
</div>
|
|
171
|
+
</div>
|
|
172
|
+
|
|
173
|
+
{/* Run Panel */}
|
|
174
|
+
{showRunPanel && (
|
|
175
|
+
<div className={`${colors.card} border ${colors.border} rounded-xl p-4 space-y-4`}>
|
|
176
|
+
<div className="flex flex-col sm:flex-row gap-2 sm:gap-4">
|
|
177
|
+
<select
|
|
178
|
+
value={activeProviderId || ''}
|
|
179
|
+
onChange={(e) => handleSetActive(e.target.value)}
|
|
180
|
+
className={`px-3 py-2 ${colors.bg} border ${colors.border} rounded-lg text-white w-full sm:w-auto`}
|
|
181
|
+
>
|
|
182
|
+
<option value="">Select Provider</option>
|
|
183
|
+
{providers.filter(p => p.enabled).map(p => (
|
|
184
|
+
<option key={p.id} value={p.id}>{p.name}</option>
|
|
185
|
+
))}
|
|
186
|
+
</select>
|
|
187
|
+
</div>
|
|
188
|
+
|
|
189
|
+
<textarea
|
|
190
|
+
value={runPrompt}
|
|
191
|
+
onChange={(e) => setRunPrompt(e.target.value)}
|
|
192
|
+
placeholder="Enter your prompt..."
|
|
193
|
+
rows={3}
|
|
194
|
+
className={`w-full px-3 py-2 ${colors.bg} border ${colors.border} rounded-lg text-white resize-none focus:border-${colorPrefix}-accent focus:outline-none`}
|
|
195
|
+
/>
|
|
196
|
+
|
|
197
|
+
<div className="flex justify-between items-center">
|
|
198
|
+
<button
|
|
199
|
+
onClick={handleExecuteRun}
|
|
200
|
+
disabled={!runPrompt.trim() || !activeProviderId || activeRun}
|
|
201
|
+
className={`px-6 py-2 ${colors.success} hover:opacity-80 text-white rounded-lg transition-colors disabled:opacity-50`}
|
|
202
|
+
>
|
|
203
|
+
{activeRun ? 'Running...' : 'Execute'}
|
|
204
|
+
</button>
|
|
205
|
+
|
|
206
|
+
{activeRun && (
|
|
207
|
+
<button
|
|
208
|
+
onClick={handleStopRun}
|
|
209
|
+
className={`px-4 py-2 ${colors.error} hover:opacity-80 text-white rounded-lg transition-colors`}
|
|
210
|
+
>
|
|
211
|
+
Stop
|
|
212
|
+
</button>
|
|
213
|
+
)}
|
|
214
|
+
</div>
|
|
215
|
+
|
|
216
|
+
{runOutput && (
|
|
217
|
+
<div className={`${colors.bg} border ${colors.border} rounded-lg p-3 max-h-64 overflow-auto`}>
|
|
218
|
+
<pre className="text-sm text-gray-300 font-mono whitespace-pre-wrap">{runOutput}</pre>
|
|
219
|
+
</div>
|
|
220
|
+
)}
|
|
221
|
+
</div>
|
|
222
|
+
)}
|
|
223
|
+
|
|
224
|
+
{/* Provider List */}
|
|
225
|
+
<div className="grid gap-4">
|
|
226
|
+
{providers.map(provider => (
|
|
227
|
+
<div
|
|
228
|
+
key={provider.id}
|
|
229
|
+
className={`${colors.card} border rounded-xl p-4 ${
|
|
230
|
+
provider.id === activeProviderId ? `border-${colorPrefix}-accent` : colors.border
|
|
231
|
+
}`}
|
|
232
|
+
>
|
|
233
|
+
<div className="flex flex-col lg:flex-row lg:items-start justify-between gap-4">
|
|
234
|
+
<div className="flex-1 min-w-0">
|
|
235
|
+
<div className="flex flex-wrap items-center gap-2">
|
|
236
|
+
<h3 className="text-lg font-semibold text-white">{provider.name}</h3>
|
|
237
|
+
<span className={`text-xs px-2 py-0.5 rounded ${
|
|
238
|
+
provider.type === 'cli' ? 'bg-blue-500/20 text-blue-400' : 'bg-purple-500/20 text-purple-400'
|
|
239
|
+
}`}>
|
|
240
|
+
{provider.type.toUpperCase()}
|
|
241
|
+
</span>
|
|
242
|
+
{provider.id === activeProviderId && (
|
|
243
|
+
<span className={`text-xs px-2 py-0.5 rounded ${colors.accentBg} ${colors.accentText}`}>
|
|
244
|
+
DEFAULT
|
|
245
|
+
</span>
|
|
246
|
+
)}
|
|
247
|
+
{!provider.enabled && (
|
|
248
|
+
<span className="text-xs px-2 py-0.5 rounded bg-gray-500/20 text-gray-400">
|
|
249
|
+
DISABLED
|
|
250
|
+
</span>
|
|
251
|
+
)}
|
|
252
|
+
</div>
|
|
253
|
+
|
|
254
|
+
<div className="mt-2 text-sm text-gray-400 space-y-1">
|
|
255
|
+
{provider.type === 'cli' && (
|
|
256
|
+
<p className="break-words">Command: <code className="text-gray-300 break-all">{provider.command} {provider.args?.join(' ')}</code></p>
|
|
257
|
+
)}
|
|
258
|
+
{provider.type === 'api' && (
|
|
259
|
+
<p className="break-words">Endpoint: <code className="text-gray-300 break-all">{provider.endpoint}</code></p>
|
|
260
|
+
)}
|
|
261
|
+
{provider.models?.length > 0 && (
|
|
262
|
+
<p>Models: {provider.models.slice(0, 3).join(', ')}{provider.models.length > 3 ? ` +${provider.models.length - 3}` : ''}</p>
|
|
263
|
+
)}
|
|
264
|
+
{provider.defaultModel && (
|
|
265
|
+
<p className="break-words">Default: <code className="text-gray-300 break-all">{provider.defaultModel}</code></p>
|
|
266
|
+
)}
|
|
267
|
+
{(provider.lightModel || provider.mediumModel || provider.heavyModel) && (
|
|
268
|
+
<p className="text-xs">
|
|
269
|
+
Tiers:
|
|
270
|
+
{provider.lightModel && <span className="ml-1 text-green-400">{provider.lightModel}</span>}
|
|
271
|
+
{provider.mediumModel && <span className="ml-1 text-yellow-400">{provider.mediumModel}</span>}
|
|
272
|
+
{provider.heavyModel && <span className="ml-1 text-red-400">{provider.heavyModel}</span>}
|
|
273
|
+
</p>
|
|
274
|
+
)}
|
|
275
|
+
</div>
|
|
276
|
+
|
|
277
|
+
{testResults[provider.id] && !testResults[provider.id].testing && (
|
|
278
|
+
<div className={`mt-2 text-sm ${testResults[provider.id].success ? colors.successText : colors.errorText}`}>
|
|
279
|
+
{testResults[provider.id].success
|
|
280
|
+
? `✓ Available${testResults[provider.id].version ? ` (${testResults[provider.id].version})` : ''}`
|
|
281
|
+
: `✗ ${testResults[provider.id].error}`
|
|
282
|
+
}
|
|
283
|
+
</div>
|
|
284
|
+
)}
|
|
285
|
+
</div>
|
|
286
|
+
|
|
287
|
+
<div className="flex flex-wrap items-center gap-2">
|
|
288
|
+
<button
|
|
289
|
+
onClick={() => handleTest(provider.id)}
|
|
290
|
+
disabled={testResults[provider.id]?.testing}
|
|
291
|
+
className={`px-3 py-1.5 text-sm ${colors.borderColor} ${colors.borderHover} text-white rounded transition-colors disabled:opacity-50`}
|
|
292
|
+
>
|
|
293
|
+
{testResults[provider.id]?.testing ? 'Testing...' : 'Test'}
|
|
294
|
+
</button>
|
|
295
|
+
|
|
296
|
+
{provider.type === 'api' && (
|
|
297
|
+
<button
|
|
298
|
+
onClick={() => handleRefreshModels(provider.id)}
|
|
299
|
+
className={`px-3 py-1.5 text-sm ${colors.borderColor} ${colors.borderHover} text-white rounded transition-colors`}
|
|
300
|
+
>
|
|
301
|
+
Refresh
|
|
302
|
+
</button>
|
|
303
|
+
)}
|
|
304
|
+
|
|
305
|
+
<button
|
|
306
|
+
onClick={() => handleToggleEnabled(provider)}
|
|
307
|
+
className={`px-3 py-1.5 text-sm rounded transition-colors ${
|
|
308
|
+
provider.enabled
|
|
309
|
+
? `${colors.warningBg} ${colors.warningText} hover:bg-${colorPrefix}-warning/30`
|
|
310
|
+
: `${colors.successBg} ${colors.successText} hover:bg-${colorPrefix}-success/30`
|
|
311
|
+
}`}
|
|
312
|
+
>
|
|
313
|
+
{provider.enabled ? 'Disable' : 'Enable'}
|
|
314
|
+
</button>
|
|
315
|
+
|
|
316
|
+
{provider.id !== activeProviderId && provider.enabled && (
|
|
317
|
+
<button
|
|
318
|
+
onClick={() => handleSetActive(provider.id)}
|
|
319
|
+
className={`px-3 py-1.5 text-sm ${colors.accentBg} ${colors.accentText} hover:bg-${colorPrefix}-accent/30 rounded transition-colors`}
|
|
320
|
+
>
|
|
321
|
+
Set Default
|
|
322
|
+
</button>
|
|
323
|
+
)}
|
|
324
|
+
|
|
325
|
+
<button
|
|
326
|
+
onClick={() => { setEditingProvider(provider); setShowForm(true); }}
|
|
327
|
+
className={`px-3 py-1.5 text-sm ${colors.borderColor} ${colors.borderHover} text-white rounded transition-colors`}
|
|
328
|
+
>
|
|
329
|
+
Edit
|
|
330
|
+
</button>
|
|
331
|
+
|
|
332
|
+
<button
|
|
333
|
+
onClick={() => handleDelete(provider.id)}
|
|
334
|
+
className={`px-3 py-1.5 text-sm ${colors.errorBg} ${colors.errorText} hover:bg-${colorPrefix}-error/30 rounded transition-colors`}
|
|
335
|
+
>
|
|
336
|
+
Delete
|
|
337
|
+
</button>
|
|
338
|
+
</div>
|
|
339
|
+
</div>
|
|
340
|
+
</div>
|
|
341
|
+
))}
|
|
342
|
+
|
|
343
|
+
{providers.length === 0 && (
|
|
344
|
+
<div className="text-center py-12 text-gray-500">
|
|
345
|
+
No providers configured. Add a provider to get started.
|
|
346
|
+
</div>
|
|
347
|
+
)}
|
|
348
|
+
</div>
|
|
349
|
+
|
|
350
|
+
{/* Recent Runs */}
|
|
351
|
+
{runs.length > 0 && (
|
|
352
|
+
<div className="mt-8">
|
|
353
|
+
<h2 className="text-xl font-bold text-white mb-4">Recent Runs</h2>
|
|
354
|
+
<div className="space-y-2">
|
|
355
|
+
{runs.map(run => (
|
|
356
|
+
<div
|
|
357
|
+
key={run.id}
|
|
358
|
+
className={`${colors.card} border ${colors.border} rounded-lg p-3 flex flex-col sm:flex-row sm:items-center justify-between gap-2`}
|
|
359
|
+
>
|
|
360
|
+
<div className="flex items-start sm:items-center gap-3 min-w-0">
|
|
361
|
+
<span className={`w-2 h-2 rounded-full flex-shrink-0 mt-1.5 sm:mt-0 ${
|
|
362
|
+
run.success === true ? colors.success :
|
|
363
|
+
run.success === false ? colors.error :
|
|
364
|
+
`${colors.warning} animate-pulse`
|
|
365
|
+
}`} />
|
|
366
|
+
<div className="min-w-0">
|
|
367
|
+
<p className="text-sm text-white truncate">{run.prompt}</p>
|
|
368
|
+
<p className="text-xs text-gray-500">
|
|
369
|
+
{run.providerName} • {run.workspaceName || 'No workspace'} • {new Date(run.startTime).toLocaleString()}
|
|
370
|
+
</p>
|
|
371
|
+
</div>
|
|
372
|
+
</div>
|
|
373
|
+
<div className="text-sm text-gray-400 flex-shrink-0 pl-5 sm:pl-0">
|
|
374
|
+
{run.duration ? `${(run.duration / 1000).toFixed(1)}s` : 'Running...'}
|
|
375
|
+
</div>
|
|
376
|
+
</div>
|
|
377
|
+
))}
|
|
378
|
+
</div>
|
|
379
|
+
</div>
|
|
380
|
+
)}
|
|
381
|
+
|
|
382
|
+
{/* Provider Form Modal */}
|
|
383
|
+
{showForm && (
|
|
384
|
+
<ProviderForm
|
|
385
|
+
provider={editingProvider}
|
|
386
|
+
onClose={() => { setShowForm(false); setEditingProvider(null); }}
|
|
387
|
+
onSave={() => { setShowForm(false); setEditingProvider(null); loadData(); }}
|
|
388
|
+
api={api}
|
|
389
|
+
colorPrefix={colorPrefix}
|
|
390
|
+
/>
|
|
391
|
+
)}
|
|
392
|
+
</div>
|
|
393
|
+
);
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
function ProviderForm({ provider, onClose, onSave, api, colorPrefix = 'app' }) {
|
|
397
|
+
const [formData, setFormData] = useState({
|
|
398
|
+
name: provider?.name || '',
|
|
399
|
+
type: provider?.type || 'cli',
|
|
400
|
+
command: provider?.command || '',
|
|
401
|
+
args: provider?.args?.join(' ') || '',
|
|
402
|
+
endpoint: provider?.endpoint || '',
|
|
403
|
+
apiKey: provider?.apiKey || '',
|
|
404
|
+
models: provider?.models || [],
|
|
405
|
+
defaultModel: provider?.defaultModel || '',
|
|
406
|
+
lightModel: provider?.lightModel || '',
|
|
407
|
+
mediumModel: provider?.mediumModel || '',
|
|
408
|
+
heavyModel: provider?.heavyModel || '',
|
|
409
|
+
timeout: provider?.timeout || 300000,
|
|
410
|
+
enabled: provider?.enabled !== false
|
|
411
|
+
});
|
|
412
|
+
|
|
413
|
+
const availableModels = formData.models || [];
|
|
414
|
+
|
|
415
|
+
const handleSubmit = async (e) => {
|
|
416
|
+
e.preventDefault();
|
|
417
|
+
const data = {
|
|
418
|
+
...formData,
|
|
419
|
+
args: formData.args ? formData.args.split(' ').filter(Boolean) : [],
|
|
420
|
+
timeout: parseInt(formData.timeout)
|
|
421
|
+
};
|
|
422
|
+
|
|
423
|
+
if (provider) {
|
|
424
|
+
await api.providers.update(provider.id, data);
|
|
425
|
+
} else {
|
|
426
|
+
await api.providers.create(data);
|
|
427
|
+
}
|
|
428
|
+
onSave();
|
|
429
|
+
};
|
|
430
|
+
|
|
431
|
+
return (
|
|
432
|
+
<div className="fixed inset-0 bg-black/50 flex items-center justify-center z-50 p-4">
|
|
433
|
+
<div className={`bg-${colorPrefix}-card border border-${colorPrefix}-border rounded-xl p-4 sm:p-6 w-full max-w-lg max-h-[90vh] overflow-y-auto`}>
|
|
434
|
+
<h2 className="text-xl font-bold text-white mb-4">
|
|
435
|
+
{provider ? 'Edit Provider' : 'Add Provider'}
|
|
436
|
+
</h2>
|
|
437
|
+
<form onSubmit={handleSubmit} className="space-y-4">
|
|
438
|
+
<div>
|
|
439
|
+
<label className="block text-sm text-gray-400 mb-1">Name *</label>
|
|
440
|
+
<input
|
|
441
|
+
type="text"
|
|
442
|
+
value={formData.name}
|
|
443
|
+
onChange={(e) => setFormData(prev => ({ ...prev, name: e.target.value }))}
|
|
444
|
+
required
|
|
445
|
+
className={`w-full px-3 py-2 bg-${colorPrefix}-bg border border-${colorPrefix}-border rounded-lg text-white focus:border-${colorPrefix}-accent focus:outline-none`}
|
|
446
|
+
/>
|
|
447
|
+
</div>
|
|
448
|
+
<div>
|
|
449
|
+
<label className="block text-sm text-gray-400 mb-1">Type *</label>
|
|
450
|
+
<select
|
|
451
|
+
value={formData.type}
|
|
452
|
+
onChange={(e) => setFormData(prev => ({ ...prev, type: e.target.value }))}
|
|
453
|
+
className={`w-full px-3 py-2 bg-${colorPrefix}-bg border border-${colorPrefix}-border rounded-lg text-white focus:border-${colorPrefix}-accent focus:outline-none`}
|
|
454
|
+
>
|
|
455
|
+
<option value="cli">CLI</option>
|
|
456
|
+
<option value="api">API</option>
|
|
457
|
+
</select>
|
|
458
|
+
</div>
|
|
459
|
+
{formData.type === 'cli' && (
|
|
460
|
+
<>
|
|
461
|
+
<div>
|
|
462
|
+
<label className="block text-sm text-gray-400 mb-1">Command *</label>
|
|
463
|
+
<input
|
|
464
|
+
type="text"
|
|
465
|
+
value={formData.command}
|
|
466
|
+
onChange={(e) => setFormData(prev => ({ ...prev, command: e.target.value }))}
|
|
467
|
+
placeholder="claude"
|
|
468
|
+
required={formData.type === 'cli'}
|
|
469
|
+
className={`w-full px-3 py-2 bg-${colorPrefix}-bg border border-${colorPrefix}-border rounded-lg text-white focus:border-${colorPrefix}-accent focus:outline-none`}
|
|
470
|
+
/>
|
|
471
|
+
</div>
|
|
472
|
+
<div>
|
|
473
|
+
<label className="block text-sm text-gray-400 mb-1">Arguments (space-separated)</label>
|
|
474
|
+
<input
|
|
475
|
+
type="text"
|
|
476
|
+
value={formData.args}
|
|
477
|
+
onChange={(e) => setFormData(prev => ({ ...prev, args: e.target.value }))}
|
|
478
|
+
placeholder="--print -p"
|
|
479
|
+
className={`w-full px-3 py-2 bg-${colorPrefix}-bg border border-${colorPrefix}-border rounded-lg text-white focus:border-${colorPrefix}-accent focus:outline-none`}
|
|
480
|
+
/>
|
|
481
|
+
</div>
|
|
482
|
+
</>
|
|
483
|
+
)}
|
|
484
|
+
{formData.type === 'api' && (
|
|
485
|
+
<>
|
|
486
|
+
<div>
|
|
487
|
+
<label className="block text-sm text-gray-400 mb-1">Endpoint *</label>
|
|
488
|
+
<input
|
|
489
|
+
type="url"
|
|
490
|
+
value={formData.endpoint}
|
|
491
|
+
onChange={(e) => setFormData(prev => ({ ...prev, endpoint: e.target.value }))}
|
|
492
|
+
placeholder="http://localhost:1234/v1"
|
|
493
|
+
required={formData.type === 'api'}
|
|
494
|
+
className={`w-full px-3 py-2 bg-${colorPrefix}-bg border border-${colorPrefix}-border rounded-lg text-white focus:border-${colorPrefix}-accent focus:outline-none`}
|
|
495
|
+
/>
|
|
496
|
+
</div>
|
|
497
|
+
<div>
|
|
498
|
+
<label className="block text-sm text-gray-400 mb-1">API Key</label>
|
|
499
|
+
<input
|
|
500
|
+
type="password"
|
|
501
|
+
value={formData.apiKey}
|
|
502
|
+
onChange={(e) => setFormData(prev => ({ ...prev, apiKey: e.target.value }))}
|
|
503
|
+
className={`w-full px-3 py-2 bg-${colorPrefix}-bg border border-${colorPrefix}-border rounded-lg text-white focus:border-${colorPrefix}-accent focus:outline-none`}
|
|
504
|
+
/>
|
|
505
|
+
</div>
|
|
506
|
+
</>
|
|
507
|
+
)}
|
|
508
|
+
|
|
509
|
+
<div>
|
|
510
|
+
<label className="block text-sm text-gray-400 mb-1">
|
|
511
|
+
Available Models
|
|
512
|
+
{formData.type === 'api' && <span className="text-xs text-gray-500 ml-2">(Use Refresh after saving)</span>}
|
|
513
|
+
</label>
|
|
514
|
+
<textarea
|
|
515
|
+
value={(formData.models || []).join(', ')}
|
|
516
|
+
onChange={(e) => {
|
|
517
|
+
const models = e.target.value.split(',').map(m => m.trim()).filter(Boolean);
|
|
518
|
+
setFormData(prev => ({ ...prev, models }));
|
|
519
|
+
}}
|
|
520
|
+
placeholder="model-1, model-2, model-3"
|
|
521
|
+
rows={2}
|
|
522
|
+
className={`w-full px-3 py-2 bg-${colorPrefix}-bg border border-${colorPrefix}-border rounded-lg text-white resize-none focus:border-${colorPrefix}-accent focus:outline-none`}
|
|
523
|
+
/>
|
|
524
|
+
</div>
|
|
525
|
+
|
|
526
|
+
<div>
|
|
527
|
+
<label className="block text-sm text-gray-400 mb-1">Default Model</label>
|
|
528
|
+
{availableModels.length > 0 ? (
|
|
529
|
+
<select
|
|
530
|
+
value={formData.defaultModel}
|
|
531
|
+
onChange={(e) => setFormData(prev => ({ ...prev, defaultModel: e.target.value }))}
|
|
532
|
+
className={`w-full px-3 py-2 bg-${colorPrefix}-bg border border-${colorPrefix}-border rounded-lg text-white focus:border-${colorPrefix}-accent focus:outline-none`}
|
|
533
|
+
>
|
|
534
|
+
<option value="">None</option>
|
|
535
|
+
{availableModels.map(model => (
|
|
536
|
+
<option key={model} value={model}>{model}</option>
|
|
537
|
+
))}
|
|
538
|
+
</select>
|
|
539
|
+
) : (
|
|
540
|
+
<input
|
|
541
|
+
type="text"
|
|
542
|
+
value={formData.defaultModel}
|
|
543
|
+
onChange={(e) => setFormData(prev => ({ ...prev, defaultModel: e.target.value }))}
|
|
544
|
+
placeholder="claude-sonnet-4-20250514"
|
|
545
|
+
className={`w-full px-3 py-2 bg-${colorPrefix}-bg border border-${colorPrefix}-border rounded-lg text-white focus:border-${colorPrefix}-accent focus:outline-none`}
|
|
546
|
+
/>
|
|
547
|
+
)}
|
|
548
|
+
</div>
|
|
549
|
+
|
|
550
|
+
{/* Model Tiers */}
|
|
551
|
+
<div className={`border-t border-${colorPrefix}-border pt-4 mt-4`}>
|
|
552
|
+
<h4 className="text-sm font-medium text-gray-300 mb-3">Model Tiers</h4>
|
|
553
|
+
<div className="grid grid-cols-1 sm:grid-cols-3 gap-3">
|
|
554
|
+
<div>
|
|
555
|
+
<label className="block text-xs text-gray-400 mb-1">
|
|
556
|
+
<span className="inline-block w-2 h-2 rounded-full bg-green-500 mr-1"></span>
|
|
557
|
+
Light (fast)
|
|
558
|
+
</label>
|
|
559
|
+
{availableModels.length > 0 ? (
|
|
560
|
+
<select
|
|
561
|
+
value={formData.lightModel}
|
|
562
|
+
onChange={(e) => setFormData(prev => ({ ...prev, lightModel: e.target.value }))}
|
|
563
|
+
className={`w-full px-2 py-1.5 bg-${colorPrefix}-bg border border-${colorPrefix}-border rounded-lg text-white text-sm focus:border-${colorPrefix}-accent focus:outline-none`}
|
|
564
|
+
>
|
|
565
|
+
<option value="">None</option>
|
|
566
|
+
{availableModels.map(model => (
|
|
567
|
+
<option key={model} value={model}>{model}</option>
|
|
568
|
+
))}
|
|
569
|
+
</select>
|
|
570
|
+
) : (
|
|
571
|
+
<input
|
|
572
|
+
type="text"
|
|
573
|
+
value={formData.lightModel}
|
|
574
|
+
onChange={(e) => setFormData(prev => ({ ...prev, lightModel: e.target.value }))}
|
|
575
|
+
placeholder="haiku"
|
|
576
|
+
className={`w-full px-2 py-1.5 bg-${colorPrefix}-bg border border-${colorPrefix}-border rounded-lg text-white text-sm focus:border-${colorPrefix}-accent focus:outline-none`}
|
|
577
|
+
/>
|
|
578
|
+
)}
|
|
579
|
+
</div>
|
|
580
|
+
<div>
|
|
581
|
+
<label className="block text-xs text-gray-400 mb-1">
|
|
582
|
+
<span className="inline-block w-2 h-2 rounded-full bg-yellow-500 mr-1"></span>
|
|
583
|
+
Medium (balanced)
|
|
584
|
+
</label>
|
|
585
|
+
{availableModels.length > 0 ? (
|
|
586
|
+
<select
|
|
587
|
+
value={formData.mediumModel}
|
|
588
|
+
onChange={(e) => setFormData(prev => ({ ...prev, mediumModel: e.target.value }))}
|
|
589
|
+
className={`w-full px-2 py-1.5 bg-${colorPrefix}-bg border border-${colorPrefix}-border rounded-lg text-white text-sm focus:border-${colorPrefix}-accent focus:outline-none`}
|
|
590
|
+
>
|
|
591
|
+
<option value="">None</option>
|
|
592
|
+
{availableModels.map(model => (
|
|
593
|
+
<option key={model} value={model}>{model}</option>
|
|
594
|
+
))}
|
|
595
|
+
</select>
|
|
596
|
+
) : (
|
|
597
|
+
<input
|
|
598
|
+
type="text"
|
|
599
|
+
value={formData.mediumModel}
|
|
600
|
+
onChange={(e) => setFormData(prev => ({ ...prev, mediumModel: e.target.value }))}
|
|
601
|
+
placeholder="sonnet"
|
|
602
|
+
className={`w-full px-2 py-1.5 bg-${colorPrefix}-bg border border-${colorPrefix}-border rounded-lg text-white text-sm focus:border-${colorPrefix}-accent focus:outline-none`}
|
|
603
|
+
/>
|
|
604
|
+
)}
|
|
605
|
+
</div>
|
|
606
|
+
<div>
|
|
607
|
+
<label className="block text-xs text-gray-400 mb-1">
|
|
608
|
+
<span className="inline-block w-2 h-2 rounded-full bg-red-500 mr-1"></span>
|
|
609
|
+
Heavy (powerful)
|
|
610
|
+
</label>
|
|
611
|
+
{availableModels.length > 0 ? (
|
|
612
|
+
<select
|
|
613
|
+
value={formData.heavyModel}
|
|
614
|
+
onChange={(e) => setFormData(prev => ({ ...prev, heavyModel: e.target.value }))}
|
|
615
|
+
className={`w-full px-2 py-1.5 bg-${colorPrefix}-bg border border-${colorPrefix}-border rounded-lg text-white text-sm focus:border-${colorPrefix}-accent focus:outline-none`}
|
|
616
|
+
>
|
|
617
|
+
<option value="">None</option>
|
|
618
|
+
{availableModels.map(model => (
|
|
619
|
+
<option key={model} value={model}>{model}</option>
|
|
620
|
+
))}
|
|
621
|
+
</select>
|
|
622
|
+
) : (
|
|
623
|
+
<input
|
|
624
|
+
type="text"
|
|
625
|
+
value={formData.heavyModel}
|
|
626
|
+
onChange={(e) => setFormData(prev => ({ ...prev, heavyModel: e.target.value }))}
|
|
627
|
+
placeholder="opus"
|
|
628
|
+
className={`w-full px-2 py-1.5 bg-${colorPrefix}-bg border border-${colorPrefix}-border rounded-lg text-white text-sm focus:border-${colorPrefix}-accent focus:outline-none`}
|
|
629
|
+
/>
|
|
630
|
+
)}
|
|
631
|
+
</div>
|
|
632
|
+
</div>
|
|
633
|
+
</div>
|
|
634
|
+
|
|
635
|
+
<div>
|
|
636
|
+
<label className="block text-sm text-gray-400 mb-1">Timeout (ms)</label>
|
|
637
|
+
<input
|
|
638
|
+
type="number"
|
|
639
|
+
value={formData.timeout}
|
|
640
|
+
onChange={(e) => setFormData(prev => ({ ...prev, timeout: e.target.value }))}
|
|
641
|
+
className={`w-full px-3 py-2 bg-${colorPrefix}-bg border border-${colorPrefix}-border rounded-lg text-white focus:border-${colorPrefix}-accent focus:outline-none`}
|
|
642
|
+
/>
|
|
643
|
+
</div>
|
|
644
|
+
<label className="flex items-center gap-2">
|
|
645
|
+
<input
|
|
646
|
+
type="checkbox"
|
|
647
|
+
checked={formData.enabled}
|
|
648
|
+
onChange={(e) => setFormData(prev => ({ ...prev, enabled: e.target.checked }))}
|
|
649
|
+
className={`w-4 h-4 rounded border-${colorPrefix}-border bg-${colorPrefix}-bg`}
|
|
650
|
+
/>
|
|
651
|
+
<span className="text-sm text-gray-400">Enabled</span>
|
|
652
|
+
</label>
|
|
653
|
+
<div className="flex justify-end gap-3 pt-4">
|
|
654
|
+
<button type="button" onClick={onClose} className="px-4 py-2 text-gray-400 hover:text-white">
|
|
655
|
+
Cancel
|
|
656
|
+
</button>
|
|
657
|
+
<button type="submit" className={`px-6 py-2 bg-${colorPrefix}-accent hover:bg-${colorPrefix}-accent/80 text-white rounded-lg transition-colors`}>
|
|
658
|
+
{provider ? 'Save' : 'Create'}
|
|
659
|
+
</button>
|
|
660
|
+
</div>
|
|
661
|
+
</form>
|
|
662
|
+
</div>
|
|
663
|
+
</div>
|
|
664
|
+
);
|
|
665
|
+
}
|
package/src/index.js
ADDED