bingocode 1.1.65 → 1.1.66
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/package.json +1 -1
- package/src/cli/ProviderPanel.tsx +193 -174
- package/src/components/HelpV2/General.tsx +16 -5
- package/src/components/PromptInput/PromptInputHelpMenu.tsx +11 -2
- package/src/manager/CliMenuManager.tsx +230 -207
- package/src/manager/CliMenuUi.tsx +87 -27
- package/src/manager/TopToolbar.tsx +6 -6
- package/src/server/cli/providersMenu.tsx +46 -46
- package/src/server/config/providers.yaml +18 -18
- package/src/server/ensureSingletonLocalServer.ts +2 -2
- package/src/utils/config.ts +3 -0
|
@@ -3,6 +3,7 @@ import { Box, Text, useApp } from 'ink';
|
|
|
3
3
|
import SelectInput from 'ink-select-input';
|
|
4
4
|
import TextInput from 'ink-text-input';
|
|
5
5
|
import axios from 'axios';
|
|
6
|
+
import { Panel, Title, Chip, Hint, StateDisplay, ScrollBar, safePadEnd } from '../manager/CliMenuUi.tsx';
|
|
6
7
|
|
|
7
8
|
type ProviderField = {
|
|
8
9
|
key: string;
|
|
@@ -61,6 +62,9 @@ export const ProviderPanel: React.FC<{
|
|
|
61
62
|
const [loading, setLoading] = useState(false);
|
|
62
63
|
const [err, setErr] = useState<string | null>(null);
|
|
63
64
|
|
|
65
|
+
// Scrolling
|
|
66
|
+
const [listOffset, setListOffset] = useState(0);
|
|
67
|
+
|
|
64
68
|
const [providers, setProviders] = useState<Provider[]>([]);
|
|
65
69
|
const [currentId, setCurrentId] = useState<string | null>(null);
|
|
66
70
|
|
|
@@ -116,7 +120,7 @@ export const ProviderPanel: React.FC<{
|
|
|
116
120
|
setProviders(list || []);
|
|
117
121
|
setCurrentId(currentId);
|
|
118
122
|
} catch (e: any) {
|
|
119
|
-
setErr(e?.message || '
|
|
123
|
+
setErr(e?.message || 'Failed to fetch provider list');
|
|
120
124
|
} finally {
|
|
121
125
|
setLoading(false);
|
|
122
126
|
}
|
|
@@ -160,6 +164,7 @@ export const ProviderPanel: React.FC<{
|
|
|
160
164
|
setEditId(null);
|
|
161
165
|
setEditName('');
|
|
162
166
|
setEditKey('');
|
|
167
|
+
setListOffset(0);
|
|
163
168
|
} else {
|
|
164
169
|
onBack ? onBack() : exit();
|
|
165
170
|
}
|
|
@@ -199,12 +204,12 @@ export const ProviderPanel: React.FC<{
|
|
|
199
204
|
};
|
|
200
205
|
if (extra && Object.keys(extra).length > 0) body.extra = extra;
|
|
201
206
|
await axios.post(`${base}/api/providers`, body);
|
|
202
|
-
setOpMsg(
|
|
207
|
+
setOpMsg(`Success -> ${name}`);
|
|
203
208
|
await loadProviders();
|
|
204
209
|
setStage('list');
|
|
205
210
|
} catch (e: any) {
|
|
206
|
-
setErr(e?.response?.data?.message || e?.message || '
|
|
207
|
-
//
|
|
211
|
+
setErr(e?.response?.data?.message || e?.message || 'Create failed');
|
|
212
|
+
// Go back to the last field so user sees error instead of re-triggering submit
|
|
208
213
|
setAddFieldIndex(Math.max(0, addFields.length - 1));
|
|
209
214
|
setStage('add_input_fields');
|
|
210
215
|
}
|
|
@@ -212,20 +217,20 @@ export const ProviderPanel: React.FC<{
|
|
|
212
217
|
|
|
213
218
|
const doTest = async (id: string) => {
|
|
214
219
|
setStage('testing');
|
|
215
|
-
setErr(null); setOpMsg('
|
|
220
|
+
setErr(null); setOpMsg('Testing...');
|
|
216
221
|
try {
|
|
217
222
|
const res = await axios.post(`${base}/api/providers/${encodeURIComponent(id)}/test`);
|
|
218
223
|
const result = res?.data?.result;
|
|
219
224
|
const conn = result?.connectivity;
|
|
220
225
|
if (conn?.success) {
|
|
221
|
-
setOpMsg(
|
|
226
|
+
setOpMsg(`Connectivity OK -> ${id} (${conn.latencyMs}ms)`);
|
|
222
227
|
setErr(null);
|
|
223
228
|
} else {
|
|
224
|
-
setErr(
|
|
229
|
+
setErr(`Connectivity error: ${conn?.error || 'Unknown error'}`);
|
|
225
230
|
setOpMsg(null);
|
|
226
231
|
}
|
|
227
232
|
} catch (e: any) {
|
|
228
|
-
setErr(e?.response?.data?.message || e?.message ||
|
|
233
|
+
setErr(e?.response?.data?.message || e?.message || `Test failed -> ${id}`);
|
|
229
234
|
setOpMsg(null);
|
|
230
235
|
} finally {
|
|
231
236
|
if (stage !== 'list') setStage('list');
|
|
@@ -241,11 +246,11 @@ export const ProviderPanel: React.FC<{
|
|
|
241
246
|
if (name.trim()) updates.name = name.trim();
|
|
242
247
|
if (apiKey.trim()) updates.apiKey = apiKey.trim();
|
|
243
248
|
await axios.put(`${base}/api/providers/${encodeURIComponent(id)}`, updates);
|
|
244
|
-
setOpMsg(
|
|
249
|
+
setOpMsg(`Updated Provider -> ${name.trim() || id}`);
|
|
245
250
|
await loadProviders();
|
|
246
251
|
setStage('list');
|
|
247
252
|
} catch (e: any) {
|
|
248
|
-
setErr(e?.response?.data?.message || e?.message || '
|
|
253
|
+
setErr(e?.response?.data?.message || e?.message || 'Edit failed');
|
|
249
254
|
setStage('edit_input_key');
|
|
250
255
|
}
|
|
251
256
|
};
|
|
@@ -255,51 +260,51 @@ export const ProviderPanel: React.FC<{
|
|
|
255
260
|
setErr(null); setOpMsg(null);
|
|
256
261
|
try {
|
|
257
262
|
await axios.delete(`${base}/api/providers/${encodeURIComponent(id)}`);
|
|
258
|
-
setOpMsg(
|
|
263
|
+
setOpMsg(`Deleted Provider -> ${id}`);
|
|
259
264
|
await loadProviders();
|
|
260
265
|
setStage('list');
|
|
261
266
|
} catch (e: any) {
|
|
262
|
-
setErr(e?.response?.data?.message || e?.message || '
|
|
267
|
+
setErr(e?.response?.data?.message || e?.message || 'Delete failed');
|
|
263
268
|
setStage('list');
|
|
264
269
|
}
|
|
265
270
|
};
|
|
266
271
|
|
|
267
272
|
const MAX_LIST = 5;
|
|
268
273
|
|
|
269
|
-
//
|
|
274
|
+
// Render function for main list
|
|
270
275
|
const renderList = () => {
|
|
271
276
|
const visibleProviders = providers.slice(0, MAX_LIST);
|
|
272
277
|
const overflow = providers.length - MAX_LIST;
|
|
273
278
|
return (
|
|
274
|
-
<Box flexDirection="column">
|
|
275
|
-
<
|
|
276
|
-
{!providers.length && !loading && <
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
{p.
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
</Text>
|
|
290
|
-
|
|
291
|
-
|
|
279
|
+
<Box flexDirection="column" flexGrow={1}>
|
|
280
|
+
<Title color="cyan">Provider List</Title>
|
|
281
|
+
{!providers.length && !loading && <StateDisplay type="empty" message="No providers found" />}
|
|
282
|
+
<Box flexDirection="column" marginBottom={1}>
|
|
283
|
+
{visibleProviders.map(p => {
|
|
284
|
+
const isCur = currentProvider && (currentProvider.id === p.id);
|
|
285
|
+
return (
|
|
286
|
+
<Box key={p.id}>
|
|
287
|
+
<Text color={isCur ? 'green' : undefined} bold={isCur}>
|
|
288
|
+
{isCur ? '● ' : ' '}{p.name || '-'}
|
|
289
|
+
{isCur ? <Text dimColor> (Current)</Text> : ''}
|
|
290
|
+
</Text>
|
|
291
|
+
</Box>
|
|
292
|
+
);
|
|
293
|
+
})}
|
|
294
|
+
{overflow > 0 && <Text dimColor> ...and {overflow} more</Text>}
|
|
295
|
+
</Box>
|
|
296
|
+
|
|
297
|
+
{loading && <StateDisplay type="loading" message="Loading..." />}
|
|
292
298
|
|
|
293
|
-
<Box
|
|
299
|
+
<Box flexDirection="column" flexGrow={1}>
|
|
294
300
|
<SelectInput
|
|
295
301
|
items={[
|
|
296
|
-
{ label: '
|
|
297
|
-
{ label: '
|
|
298
|
-
{ label: '
|
|
299
|
-
{ label: '
|
|
300
|
-
{ label: '
|
|
301
|
-
{ label: '
|
|
302
|
-
{ label: '返回主菜单(ESC)', value: 'back' },
|
|
302
|
+
{ label: 'Add Provider', value: 'add' },
|
|
303
|
+
{ label: 'Edit Provider (Name/Key)', value: 'edit' },
|
|
304
|
+
{ label: 'Configure Slots', value: 'slots' },
|
|
305
|
+
{ label: 'Connectivity Test', value: 'test' },
|
|
306
|
+
{ label: 'Delete Provider', value: 'delete' },
|
|
307
|
+
{ label: 'Refresh', value: 'refresh' },
|
|
303
308
|
]}
|
|
304
309
|
onSelect={item => {
|
|
305
310
|
switch (item.value) {
|
|
@@ -308,12 +313,14 @@ export const ProviderPanel: React.FC<{
|
|
|
308
313
|
setAddFields([]);
|
|
309
314
|
setAddFieldValues({});
|
|
310
315
|
setAddFieldIndex(0);
|
|
316
|
+
setListOffset(0);
|
|
311
317
|
setStage('add_select_preset');
|
|
312
318
|
break;
|
|
313
319
|
case 'edit':
|
|
314
320
|
setEditId(null);
|
|
315
321
|
setEditName('');
|
|
316
322
|
setEditKey('');
|
|
323
|
+
setListOffset(0);
|
|
317
324
|
setStage('edit_select');
|
|
318
325
|
break;
|
|
319
326
|
case 'slots':
|
|
@@ -321,34 +328,26 @@ export const ProviderPanel: React.FC<{
|
|
|
321
328
|
.then(r => setSlotTable(r.data as Record<string, SlotEntry>))
|
|
322
329
|
.catch(() => {});
|
|
323
330
|
setStage('slot_config');
|
|
331
|
+
setListOffset(0);
|
|
324
332
|
break;
|
|
325
333
|
case 'test':
|
|
326
334
|
setStage('test_select');
|
|
335
|
+
setListOffset(0);
|
|
327
336
|
break;
|
|
328
337
|
case 'delete':
|
|
329
338
|
setStage('delete_select');
|
|
339
|
+
setListOffset(0);
|
|
330
340
|
break;
|
|
331
341
|
case 'refresh':
|
|
332
342
|
loadProviders();
|
|
333
343
|
break;
|
|
334
|
-
case 'back':
|
|
335
|
-
onBack ? onBack() : null;
|
|
336
|
-
break;
|
|
337
344
|
}
|
|
338
345
|
}}
|
|
339
346
|
/>
|
|
340
|
-
{err &&
|
|
341
|
-
|
|
342
|
-
<Text color="red">{err}</Text>
|
|
343
|
-
</Box>
|
|
344
|
-
)}
|
|
345
|
-
{opMsg && (
|
|
346
|
-
<Box marginTop={1}>
|
|
347
|
-
<Text color="green">{opMsg}</Text>
|
|
348
|
-
</Box>
|
|
349
|
-
)}
|
|
350
|
-
<Text dimColor>提示:ESC 返回。↑↓/回车 选择操作</Text>
|
|
347
|
+
{err && <StateDisplay type="error" message={err} />}
|
|
348
|
+
{opMsg && <Box marginTop={1}><Text color="green">{opMsg}</Text></Box>}
|
|
351
349
|
</Box>
|
|
350
|
+
<Hint>ESC: Back · ↑↓/Enter: Select Action</Hint>
|
|
352
351
|
</Box>
|
|
353
352
|
);
|
|
354
353
|
};
|
|
@@ -364,38 +363,48 @@ export const ProviderPanel: React.FC<{
|
|
|
364
363
|
}));
|
|
365
364
|
if (!items.length) {
|
|
366
365
|
return (
|
|
367
|
-
<Box flexDirection="column">
|
|
368
|
-
<
|
|
366
|
+
<Box flexDirection="column" flexGrow={1}>
|
|
367
|
+
<StateDisplay type="error" message="No presets available. Please ensure the backend is running." />
|
|
369
368
|
<SelectInput
|
|
370
|
-
items={[{ label: '←
|
|
369
|
+
items={[{ label: '← Back', value: 'back' }]}
|
|
371
370
|
onSelect={() => setStage('list')}
|
|
372
371
|
/>
|
|
373
372
|
</Box>
|
|
374
373
|
);
|
|
375
374
|
}
|
|
375
|
+
|
|
376
|
+
const MAX_VISIBLE = 8;
|
|
377
|
+
const start = Math.min(listOffset, Math.max(0, items.length - MAX_VISIBLE));
|
|
378
|
+
const sliced = items.slice(start, start + MAX_VISIBLE);
|
|
379
|
+
|
|
376
380
|
return (
|
|
377
|
-
<Box flexDirection="column">
|
|
378
|
-
<
|
|
379
|
-
<
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
381
|
+
<Box flexDirection="column" flexGrow={1}>
|
|
382
|
+
<Title color="cyan">Select Preset</Title>
|
|
383
|
+
<Box flexDirection="row" flexGrow={1}>
|
|
384
|
+
<Box flexDirection="column" flexGrow={1}>
|
|
385
|
+
<SelectInput
|
|
386
|
+
items={sliced}
|
|
387
|
+
onSelect={it => {
|
|
388
|
+
const preset = presets.find(p => p.id === (it.value as string));
|
|
389
|
+
const fields: ProviderField[] =
|
|
390
|
+
preset?.fields && preset.fields.length > 0
|
|
391
|
+
? preset.fields
|
|
392
|
+
: [
|
|
393
|
+
{ key: 'name', label: 'Provider Nickname', required: true },
|
|
394
|
+
{ key: 'apiKey', label: 'API Key', required: true, secret: true },
|
|
395
|
+
];
|
|
396
|
+
setSelectedPresetId(it.value as string);
|
|
397
|
+
setAddFields(fields);
|
|
398
|
+
setAddFieldValues({});
|
|
399
|
+
setAddFieldIndex(0);
|
|
400
|
+
setListOffset(0);
|
|
401
|
+
setStage('add_input_fields');
|
|
402
|
+
}}
|
|
403
|
+
/>
|
|
404
|
+
</Box>
|
|
405
|
+
<ScrollBar total={items.length} offset={start} height={MAX_VISIBLE} />
|
|
406
|
+
</Box>
|
|
407
|
+
<Hint>↑↓: Select · ESC: Back</Hint>
|
|
399
408
|
</Box>
|
|
400
409
|
);
|
|
401
410
|
}
|
|
@@ -403,18 +412,15 @@ export const ProviderPanel: React.FC<{
|
|
|
403
412
|
if (stage === 'add_input_fields') {
|
|
404
413
|
const field = addFields[addFieldIndex];
|
|
405
414
|
if (!field) {
|
|
406
|
-
|
|
407
|
-
return <Text color="yellow">创建中...</Text>;
|
|
415
|
+
return <StateDisplay type="loading" message="Creating..." />;
|
|
408
416
|
}
|
|
409
417
|
|
|
410
418
|
const currentVal = addFieldValues[field.key] ?? field.default ?? '';
|
|
411
419
|
|
|
412
420
|
const handleSubmit = (submittedVal: string) => {
|
|
413
|
-
// ink-text-input passes the current value to onSubmit
|
|
414
421
|
const val = submittedVal;
|
|
415
|
-
if (field.required && !val.trim()) return;
|
|
422
|
+
if (field.required && !val.trim()) return;
|
|
416
423
|
|
|
417
|
-
// 确保最新值已写入
|
|
418
424
|
const merged = { ...addFieldValues, [field.key]: val };
|
|
419
425
|
|
|
420
426
|
const nextIndex = addFieldIndex + 1;
|
|
@@ -422,7 +428,6 @@ export const ProviderPanel: React.FC<{
|
|
|
422
428
|
setAddFieldValues(merged);
|
|
423
429
|
setAddFieldIndex(nextIndex);
|
|
424
430
|
} else {
|
|
425
|
-
// 最后一个字段提交,触发创建
|
|
426
431
|
const name = merged['name'] || '';
|
|
427
432
|
const apiKey = merged['apiKey'] || '';
|
|
428
433
|
const baseUrl = merged['baseUrl'] || '';
|
|
@@ -436,29 +441,35 @@ export const ProviderPanel: React.FC<{
|
|
|
436
441
|
};
|
|
437
442
|
|
|
438
443
|
return (
|
|
439
|
-
<Box flexDirection="column">
|
|
440
|
-
<
|
|
441
|
-
|
|
442
|
-
</
|
|
443
|
-
<
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
444
|
+
<Box flexDirection="column" flexGrow={1}>
|
|
445
|
+
<Title color="cyan">
|
|
446
|
+
Add Provider — Field {addFieldIndex + 1}/{addFields.length}
|
|
447
|
+
</Title>
|
|
448
|
+
<Box marginBottom={1} flexDirection="row">
|
|
449
|
+
<Box width={20}>
|
|
450
|
+
<Text>
|
|
451
|
+
{field.label}{field.required ? <Text color="red"> *</Text> : ''}
|
|
452
|
+
</Text>
|
|
453
|
+
</Box>
|
|
454
|
+
<Box flexGrow={1}>
|
|
455
|
+
{field.placeholder ? <Text dimColor>({field.placeholder})</Text> : <Text />}
|
|
456
|
+
</Box>
|
|
457
|
+
</Box>
|
|
447
458
|
<TextInput
|
|
448
459
|
value={currentVal}
|
|
449
460
|
onChange={v => setAddFieldValues(prev => ({ ...prev, [field.key]: v }))}
|
|
450
|
-
// @ts-ignore
|
|
461
|
+
// @ts-ignore
|
|
451
462
|
mask={field.secret ? '*' : undefined}
|
|
452
463
|
onSubmit={handleSubmit}
|
|
453
464
|
/>
|
|
454
|
-
{err && <
|
|
455
|
-
<
|
|
465
|
+
{err && <StateDisplay type="error" message={err} />}
|
|
466
|
+
<Hint>Enter: Continue · ESC: Back to List</Hint>
|
|
456
467
|
</Box>
|
|
457
468
|
);
|
|
458
469
|
}
|
|
459
470
|
|
|
460
471
|
if (stage === 'creating') {
|
|
461
|
-
return <
|
|
472
|
+
return <StateDisplay type="loading" message="Creating..." />;
|
|
462
473
|
}
|
|
463
474
|
|
|
464
475
|
if (stage === 'test_select') {
|
|
@@ -467,20 +478,20 @@ export const ProviderPanel: React.FC<{
|
|
|
467
478
|
value: p.id
|
|
468
479
|
}));
|
|
469
480
|
return (
|
|
470
|
-
<Box flexDirection="column">
|
|
471
|
-
<
|
|
481
|
+
<Box flexDirection="column" flexGrow={1}>
|
|
482
|
+
<Title color="cyan">Select Provider to Test</Title>
|
|
472
483
|
<SelectInput
|
|
473
484
|
items={items}
|
|
474
485
|
onSelect={it => doTest(it.value as string)}
|
|
475
486
|
/>
|
|
476
|
-
{err && <
|
|
477
|
-
<
|
|
487
|
+
{err && <StateDisplay type="error" message={err} />}
|
|
488
|
+
<Hint>ESC: Back</Hint>
|
|
478
489
|
</Box>
|
|
479
490
|
);
|
|
480
491
|
}
|
|
481
492
|
|
|
482
493
|
if (stage === 'testing') {
|
|
483
|
-
return <
|
|
494
|
+
return <StateDisplay type="loading" message="Testing..." />;
|
|
484
495
|
}
|
|
485
496
|
|
|
486
497
|
if (stage === 'delete_select') {
|
|
@@ -489,8 +500,8 @@ export const ProviderPanel: React.FC<{
|
|
|
489
500
|
value: p.id
|
|
490
501
|
}));
|
|
491
502
|
return (
|
|
492
|
-
<Box flexDirection="column">
|
|
493
|
-
<
|
|
503
|
+
<Box flexDirection="column" flexGrow={1}>
|
|
504
|
+
<Title color="red">Select Provider to Delete</Title>
|
|
494
505
|
<SelectInput
|
|
495
506
|
items={items}
|
|
496
507
|
onSelect={it => {
|
|
@@ -498,7 +509,7 @@ export const ProviderPanel: React.FC<{
|
|
|
498
509
|
setStage('delete_confirm');
|
|
499
510
|
}}
|
|
500
511
|
/>
|
|
501
|
-
<
|
|
512
|
+
<Hint>ESC: Back</Hint>
|
|
502
513
|
</Box>
|
|
503
514
|
);
|
|
504
515
|
}
|
|
@@ -506,12 +517,12 @@ export const ProviderPanel: React.FC<{
|
|
|
506
517
|
if (stage === 'delete_confirm') {
|
|
507
518
|
if (!selectedId) { setStage('list'); return null; }
|
|
508
519
|
return (
|
|
509
|
-
<Box flexDirection="column">
|
|
510
|
-
<
|
|
520
|
+
<Box flexDirection="column" flexGrow={1}>
|
|
521
|
+
<Title color="red">Confirm Delete: {selectedId}?</Title>
|
|
511
522
|
<SelectInput
|
|
512
523
|
items={[
|
|
513
|
-
{ label: '
|
|
514
|
-
{ label: '
|
|
524
|
+
{ label: 'Yes, Delete', value: 'yes' },
|
|
525
|
+
{ label: 'No, Back', value: 'no' }
|
|
515
526
|
]}
|
|
516
527
|
onSelect={it => {
|
|
517
528
|
if (it.value === 'no') {
|
|
@@ -521,32 +532,32 @@ export const ProviderPanel: React.FC<{
|
|
|
521
532
|
}
|
|
522
533
|
}}
|
|
523
534
|
/>
|
|
524
|
-
{err && <
|
|
525
|
-
<
|
|
535
|
+
{err && <StateDisplay type="error" message={err} />}
|
|
536
|
+
<Hint>ESC: Back</Hint>
|
|
526
537
|
</Box>
|
|
527
538
|
);
|
|
528
539
|
}
|
|
529
540
|
|
|
530
541
|
if (stage === 'removing') {
|
|
531
|
-
return <
|
|
542
|
+
return <StateDisplay type="loading" message="Deleting..." />;
|
|
532
543
|
}
|
|
533
544
|
|
|
534
545
|
if (stage === 'edit_select') {
|
|
535
546
|
const items = providers.map(p => ({
|
|
536
|
-
label: `${p.name || p.id}${(currentId === p.id || p.isCurrent) ? ' ←
|
|
547
|
+
label: `${p.name || p.id}${(currentId === p.id || p.isCurrent) ? ' ← Current' : ''}`,
|
|
537
548
|
value: p.id,
|
|
538
549
|
}));
|
|
539
550
|
if (!items.length) {
|
|
540
551
|
return (
|
|
541
|
-
<Box flexDirection="column">
|
|
542
|
-
<
|
|
543
|
-
<SelectInput items={[{ label: '←
|
|
552
|
+
<Box flexDirection="column" flexGrow={1}>
|
|
553
|
+
<StateDisplay type="empty" message="No providers available to edit." />
|
|
554
|
+
<SelectInput items={[{ label: '← Back', value: 'back' }]} onSelect={() => setStage('list')} />
|
|
544
555
|
</Box>
|
|
545
556
|
);
|
|
546
557
|
}
|
|
547
558
|
return (
|
|
548
|
-
<Box flexDirection="column">
|
|
549
|
-
<
|
|
559
|
+
<Box flexDirection="column" flexGrow={1}>
|
|
560
|
+
<Title color="cyan">Select Provider to Edit</Title>
|
|
550
561
|
<SelectInput
|
|
551
562
|
items={items}
|
|
552
563
|
onSelect={it => {
|
|
@@ -557,29 +568,31 @@ export const ProviderPanel: React.FC<{
|
|
|
557
568
|
setStage('edit_input_name');
|
|
558
569
|
}}
|
|
559
570
|
/>
|
|
560
|
-
<
|
|
571
|
+
<Hint>ESC: Back</Hint>
|
|
561
572
|
</Box>
|
|
562
573
|
);
|
|
563
574
|
}
|
|
564
575
|
|
|
565
576
|
if (stage === 'edit_input_name') {
|
|
566
577
|
return (
|
|
567
|
-
<Box flexDirection="column">
|
|
568
|
-
<
|
|
578
|
+
<Box flexDirection="column" flexGrow={1}>
|
|
579
|
+
<Title color="cyan">Edit Name</Title>
|
|
580
|
+
<Text>Current: <Text color="cyan">{editName}</Text> (Enter to keep):</Text>
|
|
569
581
|
<TextInput
|
|
570
582
|
value={editName}
|
|
571
583
|
onChange={setEditName}
|
|
572
584
|
onSubmit={() => setStage('edit_input_key')}
|
|
573
585
|
/>
|
|
574
|
-
<
|
|
586
|
+
<Hint>Enter: Continue · ESC: Back</Hint>
|
|
575
587
|
</Box>
|
|
576
588
|
);
|
|
577
589
|
}
|
|
578
590
|
|
|
579
591
|
if (stage === 'edit_input_key') {
|
|
580
592
|
return (
|
|
581
|
-
<Box flexDirection="column">
|
|
582
|
-
<
|
|
593
|
+
<Box flexDirection="column" flexGrow={1}>
|
|
594
|
+
<Title color="cyan">Edit API Key</Title>
|
|
595
|
+
<Text>Enter new API Key (Leave empty to keep current):</Text>
|
|
583
596
|
<TextInput
|
|
584
597
|
value={editKey}
|
|
585
598
|
onChange={setEditKey}
|
|
@@ -590,49 +603,48 @@ export const ProviderPanel: React.FC<{
|
|
|
590
603
|
doEdit(editId, editName, editKey);
|
|
591
604
|
}}
|
|
592
605
|
/>
|
|
593
|
-
{err && <
|
|
594
|
-
<
|
|
606
|
+
{err && <StateDisplay type="error" message={err} />}
|
|
607
|
+
<Hint>Enter: Save · ESC: Back</Hint>
|
|
595
608
|
</Box>
|
|
596
609
|
);
|
|
597
610
|
}
|
|
598
611
|
|
|
599
612
|
if (stage === 'editing') {
|
|
600
|
-
return <
|
|
613
|
+
return <StateDisplay type="loading" message="Saving..." />;
|
|
601
614
|
}
|
|
602
615
|
|
|
603
616
|
if (stage === 'slot_config') {
|
|
604
617
|
const SLOTS = ['main', 'haiku', 'sonnet', 'opus'] as const;
|
|
605
618
|
const SLOT_DESCS: Record<string, string> = {
|
|
606
|
-
main: '
|
|
607
|
-
haiku: '
|
|
608
|
-
sonnet: '
|
|
609
|
-
opus: '
|
|
619
|
+
main: 'Main model for complex reasoning and long context.',
|
|
620
|
+
haiku: 'Fast & light for simple Q&A and low latency.',
|
|
621
|
+
sonnet: 'Balanced quality & speed for daily tasks.',
|
|
622
|
+
opus: 'Strongest reasoning for deep analysis.',
|
|
610
623
|
};
|
|
611
624
|
const items = SLOTS.map(s => {
|
|
612
625
|
const entry = slotTable[s];
|
|
613
626
|
const providerName = entry
|
|
614
627
|
? (providers.find(p => p.id === entry.providerId)?.name || entry.providerId)
|
|
615
628
|
: null;
|
|
616
|
-
const modelDisplayName = entry?.label || entry?.modelId || '
|
|
617
|
-
const status = entry ? `${providerName} / ${modelDisplayName}` : '
|
|
618
|
-
const label = `[${s}]
|
|
629
|
+
const modelDisplayName = entry?.label || entry?.modelId || 'Unconfigured';
|
|
630
|
+
const status = entry ? `${providerName} / ${modelDisplayName}` : 'Unconfigured';
|
|
631
|
+
const label = `[${s}] ${safePadEnd(status, 30)} — ${SLOT_DESCS[s]}`;
|
|
619
632
|
return { label, value: s };
|
|
620
633
|
});
|
|
621
634
|
return (
|
|
622
|
-
<Box flexDirection="column">
|
|
623
|
-
<
|
|
624
|
-
{err && <
|
|
625
|
-
{opMsg && <Text color="green">{opMsg}</Text>}
|
|
635
|
+
<Box flexDirection="column" flexGrow={1}>
|
|
636
|
+
<Title color="cyan">Configure Model Slots</Title>
|
|
637
|
+
{err && <StateDisplay type="error" message={err} />}
|
|
638
|
+
{opMsg && <Box marginBottom={1}><Text color="green">{opMsg}</Text></Box>}
|
|
626
639
|
<SelectInput
|
|
627
|
-
items={[...items, { label: '←
|
|
640
|
+
items={[...items, { label: '← Back to Menu', value: 'back' }]}
|
|
628
641
|
onSelect={it => {
|
|
629
642
|
if (it.value === 'back') { setStage('list'); setErr(null); return; }
|
|
630
643
|
const slotName = it.value as string;
|
|
631
644
|
setCurrentSlotName(slotName);
|
|
632
645
|
setErr(null);
|
|
633
|
-
setSlotLoadingMsg(
|
|
646
|
+
setSlotLoadingMsg(`Fetching model list...`);
|
|
634
647
|
setStage('slot_loading');
|
|
635
|
-
// Fetch model lists from each provider's API endpoint
|
|
636
648
|
Promise.all(
|
|
637
649
|
providers.map(p =>
|
|
638
650
|
axios.get(`${base}/api/providers/${encodeURIComponent(p.id)}/models`)
|
|
@@ -647,27 +659,27 @@ export const ProviderPanel: React.FC<{
|
|
|
647
659
|
const map: Record<string, string[]> = {};
|
|
648
660
|
results.forEach(r => { map[r.id] = r.models; });
|
|
649
661
|
setSlotProviderModels(map);
|
|
650
|
-
// Check if any models available
|
|
651
662
|
const hasAny = results.some(r => r.models.length > 0);
|
|
652
663
|
if (!hasAny) {
|
|
653
|
-
setErr('
|
|
664
|
+
setErr('No models returned from any provider. Check API keys.');
|
|
654
665
|
setStage('slot_config');
|
|
655
666
|
} else {
|
|
667
|
+
setListOffset(0);
|
|
656
668
|
setStage('slot_select_model');
|
|
657
669
|
}
|
|
658
670
|
});
|
|
659
671
|
}}
|
|
660
672
|
/>
|
|
661
|
-
<
|
|
673
|
+
<Hint>ESC: Back</Hint>
|
|
662
674
|
</Box>
|
|
663
675
|
);
|
|
664
676
|
}
|
|
665
677
|
|
|
666
678
|
if (stage === 'slot_loading') {
|
|
667
679
|
return (
|
|
668
|
-
<Box flexDirection="column">
|
|
669
|
-
<
|
|
670
|
-
<
|
|
680
|
+
<Box flexDirection="column" flexGrow={1}>
|
|
681
|
+
<StateDisplay type="loading" message={slotLoadingMsg || 'Fetching models...'} />
|
|
682
|
+
<Hint>ESC: Cancel</Hint>
|
|
671
683
|
</Box>
|
|
672
684
|
);
|
|
673
685
|
}
|
|
@@ -683,10 +695,10 @@ export const ProviderPanel: React.FC<{
|
|
|
683
695
|
|
|
684
696
|
if (items.length === 0) {
|
|
685
697
|
return (
|
|
686
|
-
<Box flexDirection="column">
|
|
687
|
-
<
|
|
698
|
+
<Box flexDirection="column" flexGrow={1}>
|
|
699
|
+
<StateDisplay type="error" message="No available models found." />
|
|
688
700
|
<SelectInput
|
|
689
|
-
items={[{ label: '←
|
|
701
|
+
items={[{ label: '← Back', value: 'back' }]}
|
|
690
702
|
onSelect={() => setStage('slot_config')}
|
|
691
703
|
/>
|
|
692
704
|
</Box>
|
|
@@ -694,37 +706,43 @@ export const ProviderPanel: React.FC<{
|
|
|
694
706
|
}
|
|
695
707
|
|
|
696
708
|
return (
|
|
697
|
-
<Box flexDirection="column">
|
|
698
|
-
<
|
|
699
|
-
{err && <
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
709
|
+
<Box flexDirection="column" flexGrow={1}>
|
|
710
|
+
<Title color="cyan">Configure Slot [{currentSlotName}] — Select Model</Title>
|
|
711
|
+
{err && <StateDisplay type="error" message={err} />}
|
|
712
|
+
|
|
713
|
+
<Box flexDirection="row" flexGrow={1}>
|
|
714
|
+
<Box flexDirection="column" flexGrow={1}>
|
|
715
|
+
<SelectInput
|
|
716
|
+
items={items}
|
|
717
|
+
onSelect={it => {
|
|
718
|
+
const val = it.value as string;
|
|
719
|
+
if (val.startsWith('__header__')) return;
|
|
720
|
+
const sepIdx = val.indexOf('::');
|
|
721
|
+
const providerId = val.slice(0, sepIdx);
|
|
722
|
+
const modelId = val.slice(sepIdx + 2);
|
|
723
|
+
setTempSlotProviderId(providerId);
|
|
724
|
+
setTempSlotModelId(modelId);
|
|
725
|
+
setSlotLabelInput(modelId);
|
|
726
|
+
setStage('slot_input_label');
|
|
727
|
+
}}
|
|
728
|
+
/>
|
|
729
|
+
</Box>
|
|
730
|
+
<ScrollBar total={items.length} offset={0} height={10} />
|
|
731
|
+
</Box>
|
|
732
|
+
<Hint>↑↓: Select Model · Enter: Confirm · ESC: Back</Hint>
|
|
715
733
|
</Box>
|
|
716
734
|
);
|
|
717
735
|
}
|
|
718
736
|
|
|
719
737
|
if (stage === 'slot_input_label') {
|
|
720
738
|
return (
|
|
721
|
-
<Box flexDirection="column">
|
|
722
|
-
<
|
|
739
|
+
<Box flexDirection="column" flexGrow={1}>
|
|
740
|
+
<Title color="cyan">Configure Slot [{currentSlotName}] — Set Display Name</Title>
|
|
723
741
|
<Text>
|
|
724
|
-
|
|
742
|
+
Model: {providers.find(p => p.id === tempSlotProviderId)?.name || tempSlotProviderId} / {tempSlotModelId}
|
|
725
743
|
</Text>
|
|
726
744
|
<Box marginTop={1}>
|
|
727
|
-
<Text
|
|
745
|
+
<Text>Display Name (Label): </Text>
|
|
728
746
|
<TextInput
|
|
729
747
|
value={slotLabelInput}
|
|
730
748
|
onChange={setSlotLabelInput}
|
|
@@ -740,18 +758,19 @@ export const ProviderPanel: React.FC<{
|
|
|
740
758
|
...prev,
|
|
741
759
|
[currentSlotName]: { providerId: tempSlotProviderId, modelId: tempSlotModelId, label }
|
|
742
760
|
}));
|
|
743
|
-
setOpMsg(
|
|
761
|
+
setOpMsg(`Configured [${currentSlotName}] -> ${label}`);
|
|
744
762
|
setErr(null);
|
|
763
|
+
setListOffset(0);
|
|
745
764
|
setStage('slot_config');
|
|
746
765
|
})
|
|
747
766
|
.catch(e => {
|
|
748
|
-
setErr((e as any)?.response?.data?.message || (e as any)?.message || '
|
|
767
|
+
setErr((e as any)?.response?.data?.message || (e as any)?.message || 'Save failed');
|
|
749
768
|
setStage('slot_config');
|
|
750
769
|
});
|
|
751
770
|
}}
|
|
752
771
|
/>
|
|
753
772
|
</Box>
|
|
754
|
-
<
|
|
773
|
+
<Hint>Enter: Save (Display name in UI) · ESC: Back to Models</Hint>
|
|
755
774
|
</Box>
|
|
756
775
|
);
|
|
757
776
|
}
|