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.
@@ -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 || '获取 Provider 列表失败');
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(`创建成功 -> ${name}`);
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(`连通性正常 -> ${id} (${conn.latencyMs}ms)`);
239
+ setOpMsg(`Connectivity OK -> ${id} (${conn.latencyMs}ms)`);
222
240
  setErr(null);
223
241
  } else {
224
- setErr(`连通性异常: ${conn?.error || '未知错误'}`);
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 || `测试失败 -> ${id}`);
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(`已更新 Provider -> ${name.trim() || id}`);
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(`已删除 Provider -> ${id}`);
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
- <Text color="cyan">Provider 列表</Text>
276
- {!providers.length && !loading && <Text>暂无 Provider</Text>}
277
- {visibleProviders.map(p => {
278
- const isCur = currentProvider && (currentProvider.id === p.id);
279
- return (
280
- <Text key={p.id} color={isCur ? 'green' : undefined} bold={isCur}>
281
- {p.name || '-'}{isCur ? ' ← 当前' : ''}
282
- </Text>
283
- );
284
- })}
285
- {overflow > 0 && <Text dimColor> ...还有 {overflow} 个</Text>}
286
- {currentProvider && (
287
- <Text dimColor>
288
- 当前 Provider: {currentProvider.name || currentProvider.id}
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
- <Box marginTop={1} flexDirection="column">
310
+ {loading && <StateDisplay type="loading" message="Loading..." />}
311
+
312
+ <Box flexDirection="column" flexGrow={1}>
294
313
  <SelectInput
295
314
  items={[
296
- { label: '新增 Provider', value: 'add' },
297
- { label: '编辑 Provider(名称/API Key', value: 'edit' },
298
- { label: '配置槽位(main/haiku/sonnet/opus)', value: 'slots' },
299
- { label: '连通性测试', value: 'test' },
300
- { label: '删除 Provider', value: 'delete' },
301
- { label: '刷新', value: 'refresh' },
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
- <Box marginTop={1}>
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
- <Text color="yellow">未获取到预设,请先确保主服务已启动。</Text>
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: '← 返回', value: 'back' }]}
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
- <Text>选择预设:</Text>
379
- <SelectInput
380
- items={items}
381
- onSelect={it => {
382
- const preset = presets.find(p => p.id === (it.value as string));
383
- // preset.fields 获取字段列表;若为空则用默认最小集
384
- const fields: ProviderField[] =
385
- preset?.fields && preset.fields.length > 0
386
- ? preset.fields
387
- : [
388
- { key: 'name', label: 'Provider 昵称', required: true },
389
- { key: 'apiKey', label: 'API Key', required: true, secret: true },
390
- ];
391
- setSelectedPresetId(it.value as string);
392
- setAddFields(fields);
393
- setAddFieldValues({});
394
- setAddFieldIndex(0);
395
- setStage('add_input_fields');
396
- }}
397
- />
398
- <Text dimColor>ESC 返回</Text>
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
- <Text color="cyan">
441
- 新增 Provider — 字段 {addFieldIndex + 1}/{addFields.length}
442
- </Text>
443
- <Text>
444
- {field.label}{field.required ? <Text color="red"> *</Text> : ''}
445
- {field.placeholder ? <Text dimColor> ({field.placeholder})</Text> : ''}:
446
- </Text>
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 - ink-text-input supports mask prop
474
+ // @ts-ignore
451
475
  mask={field.secret ? '*' : undefined}
452
476
  onSubmit={handleSubmit}
453
477
  />
454
- {err && <Text color="red">{err}</Text>}
455
- <Text dimColor>回车继续,ESC 返回列表</Text>
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 <Text color="yellow">创建中...</Text>;
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
- <Text>选择要测试的 Provider:</Text>
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 && <Text color="red">{err}</Text>}
477
- <Text dimColor>ESC 返回</Text>
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 <Text color="yellow">测试中...</Text>;
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
- <Text color="red">选择要删除的 Provider:</Text>
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
- <Text dimColor>ESC 返回</Text>
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
- <Text color="red">确认删除 Provider:{selectedId} ?</Text>
533
+ <Box flexDirection="column" flexGrow={1}>
534
+ <Title color="red">Confirm Delete: {selectedId}?</Title>
511
535
  <SelectInput
512
536
  items={[
513
- { label: '是,删除', value: 'yes' },
514
- { label: '否,返回', value: 'no' }
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 && <Text color="red">{err}</Text>}
525
- <Text dimColor>ESC 返回</Text>
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 <Text color="yellow">删除中...</Text>;
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
- <Text color="yellow">暂无 Provider 可编辑。</Text>
543
- <SelectInput items={[{ label: '← 返回', value: 'back' }]} onSelect={() => setStage('list')} />
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
- <Text>选择要编辑的 Provider:</Text>
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
- <Text dimColor>ESC 返回</Text>
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
- <Text>编辑名称(当前:<Text color="cyan">{editName}</Text>,回车保留不变):</Text>
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
- <Text dimColor>回车继续,ESC 返回</Text>
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
- <Text>输入新 API Key(留空则不修改):</Text>
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 && <Text color="red">{err}</Text>}
594
- <Text dimColor>回车保存,ESC 返回</Text>
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 <Text color="yellow">保存中...</Text>;
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}] ${status} — ${SLOT_DESCS[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
- <Text color="cyan">配置槽位(选中槽位后选择模型)</Text>
624
- {err && <Text color="red">{err}</Text>}
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: '← 返回主菜单', value: 'back' }]}
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('所有 Provider 均未返回可用模型,请检查 API Key 和网络连接');
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
- <Text dimColor>ESC 返回</Text>
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
- <Text color="yellow">{slotLoadingMsg || '正在拉取模型列表...'}</Text>
670
- <Text dimColor>ESC 取消</Text>
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
- <Text color="red">没有可用的模型(所有 Provider 均未返回模型,请检查 API Key 和网络)</Text>
711
+ <Box flexDirection="column" flexGrow={1}>
712
+ <StateDisplay type="error" message="No available models found." />
688
713
  <SelectInput
689
- items={[{ label: '← 返回', value: 'back' }]}
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
- <Text color="cyan">配置槽位 [{currentSlotName}] — 选择模型</Text>
699
- {err && <Text color="red">{err}</Text>}
700
- <SelectInput
701
- items={items}
702
- onSelect={it => {
703
- const val = it.value as string;
704
- if (val.startsWith('__header__')) return;
705
- const sepIdx = val.indexOf('::');
706
- const providerId = val.slice(0, sepIdx);
707
- const modelId = val.slice(sepIdx + 2);
708
- setTempSlotProviderId(providerId);
709
- setTempSlotModelId(modelId);
710
- setSlotLabelInput(modelId); // 默认建议使用模型名作为 Label
711
- setStage('slot_input_label');
712
- }}
713
- />
714
- <Text dimColor>↑↓ 选择模型,回车确认,ESC 返回</Text>
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
- <Text color="cyan">配置槽位 [{currentSlotName}] — 设置显示名称</Text>
756
+ <Box flexDirection="column" flexGrow={1}>
757
+ <Title color="cyan">Configure Slot [{currentSlotName}] — Set Display Name</Title>
723
758
  <Text>
724
- 模型:{providers.find(p => p.id === tempSlotProviderId)?.name || tempSlotProviderId} / {tempSlotModelId}
759
+ Model: {providers.find(p => p.id === tempSlotProviderId)?.name || tempSlotProviderId} / {tempSlotModelId}
725
760
  </Text>
726
761
  <Box marginTop={1}>
727
- <Text>显示名称 (Label):</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(`已配置 [${currentSlotName}] -> ${label}`);
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
- <Text dimColor>回车保存组件名称(显示在 Claude UI),ESC 返回模型选择</Text>
790
+ <Hint>Enter: Save (Display name in UI) · ESC: Back to Models</Hint>
755
791
  </Box>
756
792
  );
757
793
  }