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