@nextclaw/ui 0.5.41 → 0.5.42

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (31) hide show
  1. package/CHANGELOG.md +8 -0
  2. package/dist/assets/{ChannelsList-C2Qd0t9b.js → ChannelsList-D1TDjZD6.js} +1 -1
  3. package/dist/assets/{ChatPage-BqlsPWdy.js → ChatPage-DpqxbP_M.js} +1 -1
  4. package/dist/assets/{CronConfig-CdvpKuSy.js → CronConfig-bxrnSSbp.js} +1 -1
  5. package/dist/assets/{DocBrowser-Bgmeukiz.js → DocBrowser-YLb5sMWS.js} +1 -1
  6. package/dist/assets/{MarketplacePage-t3Sib5LO.js → MarketplacePage-CTrpdj82.js} +1 -1
  7. package/dist/assets/{ModelConfig-DZUg-4N3.js → ModelConfig-tc0NiAb4.js} +1 -1
  8. package/dist/assets/ProvidersList-CALeNCLF.js +1 -0
  9. package/dist/assets/{RuntimeConfig-Dv-06ffH.js → RuntimeConfig-1hfJQMOn.js} +1 -1
  10. package/dist/assets/{SecretsConfig-Djc-_j3o.js → SecretsConfig-B5czvrZv.js} +1 -1
  11. package/dist/assets/{SessionsConfig-BY8rxQ7m.js → SessionsConfig-BXSaq5p7.js} +1 -1
  12. package/dist/assets/{card-ITZ1xl10.js → card-CAvcgkiT.js} +1 -1
  13. package/dist/assets/index-CL28Z9YZ.js +2 -0
  14. package/dist/assets/index-c0vY8dDm.css +1 -0
  15. package/dist/assets/{label-BowGMNYa.js → label-BlmIivGH.js} +1 -1
  16. package/dist/assets/{logos-BlD3kGKz.js → logos-BoshQeun.js} +1 -1
  17. package/dist/assets/{page-layout-C62p_dlG.js → page-layout-Cu4Yj2Q_.js} +1 -1
  18. package/dist/assets/{switch-CpKwqv79.js → switch-C5wm6PbF.js} +1 -1
  19. package/dist/assets/{tabs-custom-D1fYWpL_.js → tabs-custom-B4CXzsVy.js} +1 -1
  20. package/dist/assets/{useConfig-0trWsjx9.js → useConfig-Uy1hlToJ.js} +1 -1
  21. package/dist/assets/{useConfirmDialog-B_7sVz-j.js → useConfirmDialog-BUK2K_01.js} +1 -1
  22. package/dist/assets/{vendor-DN_iJQc4.js → vendor-CHxMoJ93.js} +26 -26
  23. package/dist/index.html +3 -3
  24. package/package.json +1 -1
  25. package/src/components/common/MaskedInput.tsx +34 -9
  26. package/src/components/config/ProviderForm.tsx +132 -98
  27. package/src/components/ui/input.tsx +1 -1
  28. package/src/lib/i18n.ts +6 -0
  29. package/dist/assets/ProvidersList-BvtEBIdN.js +0 -1
  30. package/dist/assets/index-C8UPX5Q7.js +0 -2
  31. package/dist/assets/index-DMEuanmd.css +0 -1
package/dist/index.html CHANGED
@@ -6,9 +6,9 @@
6
6
  <link rel="icon" type="image/svg+xml" href="/logo.svg" />
7
7
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
8
8
  <title>NextClaw - 系统配置</title>
9
- <script type="module" crossorigin src="/assets/index-C8UPX5Q7.js"></script>
10
- <link rel="modulepreload" crossorigin href="/assets/vendor-DN_iJQc4.js">
11
- <link rel="stylesheet" crossorigin href="/assets/index-DMEuanmd.css">
9
+ <script type="module" crossorigin src="/assets/index-CL28Z9YZ.js"></script>
10
+ <link rel="modulepreload" crossorigin href="/assets/vendor-CHxMoJ93.js">
11
+ <link rel="stylesheet" crossorigin href="/assets/index-c0vY8dDm.css">
12
12
  </head>
13
13
 
14
14
  <body>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nextclaw/ui",
3
- "version": "0.5.41",
3
+ "version": "0.5.42",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -3,26 +3,51 @@ import { Input } from '@/components/ui/input';
3
3
  import { Button } from '@/components/ui/button';
4
4
  import { Eye, EyeOff } from 'lucide-react';
5
5
  import { cn } from '@/lib/utils';
6
- import { t } from '@/lib/i18n';
7
6
 
8
7
  interface MaskedInputProps extends React.InputHTMLAttributes<HTMLInputElement> {
9
8
  maskedValue?: string;
10
9
  isSet?: boolean;
11
10
  }
12
11
 
13
- export function MaskedInput({ maskedValue, isSet, className, ...props }: MaskedInputProps) {
12
+ export function MaskedInput({ maskedValue, isSet, className, value, onChange, placeholder, ...props }: MaskedInputProps) {
14
13
  const [showKey, setShowKey] = useState(false);
14
+ const [isEditing, setIsEditing] = useState(false);
15
+ const hasUserInput = typeof value === 'string' && value.length > 0;
16
+
17
+ // Determine what to display
18
+ const showMaskedDots = isSet && !hasUserInput && !isEditing;
15
19
 
16
20
  return (
17
21
  <div className="relative">
18
- <Input
19
- type={showKey ? 'text' : 'password'}
20
- className={cn('pr-20', className)}
21
- placeholder={isSet ? `${t('apiKeySet')} (${t('unchanged')})` : ''}
22
- {...props}
23
- />
22
+ {showMaskedDots ? (
23
+ // Show masked state - clickable to edit
24
+ <div
25
+ onClick={() => setIsEditing(true)}
26
+ className={cn(
27
+ 'flex h-9 w-full rounded-xl border border-gray-200/80 bg-white px-3.5 py-2 text-sm text-gray-500 cursor-text items-center pr-12',
28
+ className
29
+ )}
30
+ >
31
+ ••••••••••••
32
+ </div>
33
+ ) : (
34
+ <Input
35
+ type={showKey ? 'text' : 'password'}
36
+ className={cn('pr-12', className)}
37
+ value={value}
38
+ onChange={onChange}
39
+ onBlur={() => {
40
+ if (!hasUserInput) {
41
+ setIsEditing(false);
42
+ }
43
+ }}
44
+ placeholder={placeholder}
45
+ autoFocus={isEditing}
46
+ {...props}
47
+ />
48
+ )}
24
49
  <div className="absolute right-2 top-1/2 -translate-y-1/2 flex gap-1">
25
- {(isSet || maskedValue) && (
50
+ {(isSet || hasUserInput) && (
26
51
  <Button
27
52
  type="button"
28
53
  variant="ghost"
@@ -17,7 +17,7 @@ import { StatusDot } from '@/components/ui/status-dot';
17
17
  import { t } from '@/lib/i18n';
18
18
  import { hintForPath } from '@/lib/config-hints';
19
19
  import type { ProviderConfigUpdate, ProviderConnectionTestRequest } from '@/api/types';
20
- import { KeyRound, Globe, Hash, RotateCcw, CircleDotDashed, Sparkles, Plus, X, Trash2 } from 'lucide-react';
20
+ import { CircleDotDashed, Plus, X, Trash2, ChevronDown, Settings2 } from 'lucide-react';
21
21
  import { toast } from 'sonner';
22
22
  import { CONFIG_DETAIL_CARD_CLASS, CONFIG_EMPTY_DETAIL_CARD_CLASS } from './config-layout';
23
23
 
@@ -152,6 +152,8 @@ export function ProviderForm({ providerName, onProviderDeleted }: ProviderFormPr
152
152
  const [models, setModels] = useState<string[]>([]);
153
153
  const [modelDraft, setModelDraft] = useState('');
154
154
  const [providerDisplayName, setProviderDisplayName] = useState('');
155
+ const [showAdvanced, setShowAdvanced] = useState(false);
156
+ const [showModelInput, setShowModelInput] = useState(false);
155
157
 
156
158
  const providerSpec = meta?.providers.find((p) => p.name === providerName);
157
159
  const providerConfig = providerName ? config?.providers[providerName] : null;
@@ -382,22 +384,31 @@ export function ProviderForm({ providerName, onProviderDeleted }: ProviderFormPr
382
384
 
383
385
  return (
384
386
  <div className={CONFIG_DETAIL_CARD_CLASS}>
385
- <div className="border-b border-gray-100 px-6 py-5">
386
- <div className="flex flex-wrap items-center justify-between gap-3">
387
- <div className="min-w-0">
388
- <h3 className="truncate text-lg font-semibold text-gray-900">{providerTitle}</h3>
389
- <p className="mt-1 text-sm text-gray-500">{t('providerFormDescription')}</p>
387
+ <div className="border-b border-gray-100 px-6 py-4">
388
+ <div className="flex items-center justify-between">
389
+ <h3 className="truncate text-lg font-semibold text-gray-900">{providerTitle}</h3>
390
+ <div className="flex items-center gap-3">
391
+ {isCustomProvider && (
392
+ <button
393
+ type="button"
394
+ onClick={handleDeleteProvider}
395
+ disabled={deleteProvider.isPending}
396
+ className="text-gray-400 hover:text-red-500 transition-colors"
397
+ title={t('providerDelete')}
398
+ >
399
+ <Trash2 className="h-4 w-4" />
400
+ </button>
401
+ )}
402
+ <StatusDot status={providerConfig.apiKeySet ? 'ready' : 'setup'} label={statusLabel} />
390
403
  </div>
391
- <StatusDot status={providerConfig.apiKeySet ? 'ready' : 'setup'} label={statusLabel} />
392
404
  </div>
393
405
  </div>
394
406
 
395
407
  <form onSubmit={handleSubmit} className="flex min-h-0 flex-1 flex-col">
396
- <div className="min-h-0 flex-1 space-y-6 overflow-y-auto px-6 py-5">
408
+ <div className="min-h-0 flex-1 space-y-5 overflow-y-auto px-6 py-5">
397
409
  {isCustomProvider && (
398
- <div className="space-y-2.5">
399
- <Label htmlFor="providerDisplayName" className="flex items-center gap-2 text-sm font-medium text-gray-900">
400
- <Hash className="h-3.5 w-3.5 text-gray-500" />
410
+ <div className="space-y-2">
411
+ <Label htmlFor="providerDisplayName" className="text-sm font-medium text-gray-900">
401
412
  {t('providerDisplayName')}
402
413
  </Label>
403
414
  <Input
@@ -408,13 +419,12 @@ export function ProviderForm({ providerName, onProviderDeleted }: ProviderFormPr
408
419
  placeholder={defaultDisplayName || t('providerDisplayNamePlaceholder')}
409
420
  className="rounded-xl"
410
421
  />
411
- <p className="text-xs text-gray-500">{t('providerDisplayNameHelp')}</p>
422
+ <p className="text-xs text-gray-500">{t('providerDisplayNameHelpShort')}</p>
412
423
  </div>
413
424
  )}
414
425
 
415
- <div className="space-y-2.5">
416
- <Label htmlFor="apiKey" className="flex items-center gap-2 text-sm font-medium text-gray-900">
417
- <KeyRound className="h-3.5 w-3.5 text-gray-500" />
426
+ <div className="space-y-2">
427
+ <Label htmlFor="apiKey" className="text-sm font-medium text-gray-900">
418
428
  {apiKeyHint?.label ?? t('apiKey')}
419
429
  </Label>
420
430
  <MaskedInput
@@ -422,15 +432,14 @@ export function ProviderForm({ providerName, onProviderDeleted }: ProviderFormPr
422
432
  value={apiKey}
423
433
  isSet={providerConfig.apiKeySet}
424
434
  onChange={(e) => setApiKey(e.target.value)}
425
- placeholder={providerConfig.apiKeySet ? t('apiKeySet') : apiKeyHint?.placeholder ?? t('enterApiKey')}
435
+ placeholder={apiKeyHint?.placeholder ?? t('enterApiKey')}
426
436
  className="rounded-xl"
427
437
  />
428
438
  <p className="text-xs text-gray-500">{t('leaveBlankToKeepUnchanged')}</p>
429
439
  </div>
430
440
 
431
- <div className="space-y-2.5">
432
- <Label htmlFor="apiBase" className="flex items-center gap-2 text-sm font-medium text-gray-900">
433
- <Globe className="h-3.5 w-3.5 text-gray-500" />
441
+ <div className="space-y-2">
442
+ <Label htmlFor="apiBase" className="text-sm font-medium text-gray-900">
434
443
  {apiBaseHint?.label ?? t('apiBase')}
435
444
  </Label>
436
445
  <Input
@@ -441,70 +450,66 @@ export function ProviderForm({ providerName, onProviderDeleted }: ProviderFormPr
441
450
  placeholder={defaultApiBase || apiBaseHint?.placeholder || 'https://api.example.com'}
442
451
  className="rounded-xl"
443
452
  />
444
- <p className="text-xs text-gray-500">{apiBaseHelpText}</p>
445
- {isCustomProvider && <p className="text-xs text-gray-500">{t('providerOpenAICompatHint')}</p>}
453
+ <p className="text-xs text-gray-500">{t('providerApiBaseHelpShort')}</p>
446
454
  </div>
447
455
 
448
- {providerSpec.supportsWireApi && (
449
- <div className="space-y-2.5">
450
- <Label htmlFor="wireApi" className="flex items-center gap-2 text-sm font-medium text-gray-900">
451
- <Hash className="h-3.5 w-3.5 text-gray-500" />
452
- {wireApiHint?.label ?? t('wireApi')}
456
+ <div className="space-y-2">
457
+ <div className="flex items-center justify-between">
458
+ <Label className="text-sm font-medium text-gray-900">
459
+ {t('providerModelsTitle')}
453
460
  </Label>
454
- <Select value={wireApi} onValueChange={(v) => setWireApi(v as WireApiType)}>
455
- <SelectTrigger className="rounded-xl">
456
- <SelectValue />
457
- </SelectTrigger>
458
- <SelectContent>
459
- {(providerSpec.wireApiOptions || ['auto', 'chat', 'responses']).map((option) => (
460
- <SelectItem key={option} value={option}>
461
- {option === 'chat' ? t('wireApiChat') : option === 'responses' ? t('wireApiResponses') : t('wireApiAuto')}
462
- </SelectItem>
463
- ))}
464
- </SelectContent>
465
- </Select>
466
- {wireApiHint?.help && <p className="text-xs text-gray-500">{wireApiHint.help}</p>}
461
+ {!showModelInput && (
462
+ <button
463
+ type="button"
464
+ onClick={() => setShowModelInput(true)}
465
+ className="text-xs text-primary hover:text-primary/80 font-medium flex items-center gap-1"
466
+ >
467
+ <Plus className="h-3 w-3" />
468
+ {t('providerAddModel')}
469
+ </button>
470
+ )}
467
471
  </div>
468
- )}
469
472
 
470
- <div className="space-y-2.5">
471
- <Label className="flex items-center gap-2 text-sm font-medium text-gray-900">
472
- <Hash className="h-3.5 w-3.5 text-gray-500" />
473
- {extraHeadersHint?.label ?? t('extraHeaders')}
474
- </Label>
475
- <KeyValueEditor value={extraHeaders} onChange={setExtraHeaders} />
476
- <p className="text-xs text-gray-500">{extraHeadersHint?.help || t('providerExtraHeadersHelp')}</p>
477
- </div>
478
-
479
- <div className="space-y-2.5">
480
- <Label className="flex items-center gap-2 text-sm font-medium text-gray-900">
481
- <Sparkles className="h-3.5 w-3.5 text-gray-500" />
482
- {t('providerModelsTitle')}
483
- </Label>
484
-
485
- <div className="flex items-center gap-2">
486
- <Input
487
- value={modelDraft}
488
- onChange={(event) => setModelDraft(event.target.value)}
489
- onKeyDown={(event) => {
490
- if (event.key === 'Enter') {
491
- event.preventDefault();
492
- handleAddModel();
493
- }
494
- }}
495
- placeholder={t('providerModelInputPlaceholder')}
496
- className="flex-1"
497
- />
498
- <Button type="button" variant="outline" onClick={handleAddModel} disabled={modelDraft.trim().length === 0}>
499
- <Plus className="mr-1.5 h-4 w-4" />
500
- {t('providerAddModel')}
501
- </Button>
502
- </div>
503
- <p className="text-xs text-gray-500">{t('providerModelInputHint')}</p>
473
+ {showModelInput && (
474
+ <div className="flex items-center gap-2">
475
+ <Input
476
+ value={modelDraft}
477
+ onChange={(event) => setModelDraft(event.target.value)}
478
+ onKeyDown={(event) => {
479
+ if (event.key === 'Enter') {
480
+ event.preventDefault();
481
+ handleAddModel();
482
+ }
483
+ if (event.key === 'Escape') {
484
+ setShowModelInput(false);
485
+ setModelDraft('');
486
+ }
487
+ }}
488
+ placeholder={t('providerModelInputPlaceholder')}
489
+ className="flex-1 rounded-xl"
490
+ autoFocus
491
+ />
492
+ <Button type="button" size="sm" onClick={handleAddModel} disabled={modelDraft.trim().length === 0}>
493
+ {t('add')}
494
+ </Button>
495
+ <Button type="button" size="sm" variant="ghost" onClick={() => { setShowModelInput(false); setModelDraft(''); }}>
496
+ <X className="h-4 w-4" />
497
+ </Button>
498
+ </div>
499
+ )}
504
500
 
505
501
  {models.length === 0 ? (
506
- <div className="rounded-xl border border-dashed border-gray-200 bg-gray-50 px-3 py-2 text-xs text-gray-500">
507
- {t('providerModelsEmpty')}
502
+ <div className="rounded-xl border border-dashed border-gray-200 bg-gray-50 px-4 py-6 text-center">
503
+ <p className="text-sm text-gray-500">{t('providerModelsEmptyShort')}</p>
504
+ {!showModelInput && (
505
+ <button
506
+ type="button"
507
+ onClick={() => setShowModelInput(true)}
508
+ className="mt-2 text-sm text-primary hover:text-primary/80 font-medium"
509
+ >
510
+ {t('providerAddFirstModel')}
511
+ </button>
512
+ )}
508
513
  </div>
509
514
  ) : (
510
515
  <div className="flex flex-wrap gap-2">
@@ -526,32 +531,61 @@ export function ProviderForm({ providerName, onProviderDeleted }: ProviderFormPr
526
531
  ))}
527
532
  </div>
528
533
  )}
529
- <p className="text-xs text-gray-500">{t('providerModelsHelp')}</p>
530
534
  </div>
531
- </div>
532
535
 
533
- <div className="flex flex-wrap items-center justify-between gap-3 border-t border-gray-100 px-6 py-4">
534
- <div className="flex flex-wrap items-center gap-2">
535
- <Button type="button" variant="outline" onClick={resetToDefault}>
536
- <RotateCcw className="mr-2 h-4 w-4" />
537
- {t('resetToDefault')}
538
- </Button>
539
- {isCustomProvider && (
540
- <Button
541
- type="button"
542
- variant="outline"
543
- onClick={handleDeleteProvider}
544
- disabled={deleteProvider.isPending}
545
- >
546
- <Trash2 className="mr-2 h-4 w-4" />
547
- {deleteProvider.isPending ? t('saving') : t('providerDelete')}
548
- </Button>
536
+ {/* Advanced Settings - Collapsible */}
537
+ <div className="border-t border-gray-100 pt-4">
538
+ <button
539
+ type="button"
540
+ onClick={() => setShowAdvanced(!showAdvanced)}
541
+ className="flex w-full items-center justify-between text-sm text-gray-600 hover:text-gray-900 transition-colors"
542
+ >
543
+ <span className="flex items-center gap-1.5">
544
+ <Settings2 className="h-3.5 w-3.5" />
545
+ {t('providerAdvancedSettings')}
546
+ </span>
547
+ <ChevronDown className={`h-4 w-4 transition-transform ${showAdvanced ? 'rotate-180' : ''}`} />
548
+ </button>
549
+
550
+ {showAdvanced && (
551
+ <div className="mt-4 space-y-5">
552
+ {providerSpec.supportsWireApi && (
553
+ <div className="space-y-2">
554
+ <Label htmlFor="wireApi" className="text-sm font-medium text-gray-900">
555
+ {wireApiHint?.label ?? t('wireApi')}
556
+ </Label>
557
+ <Select value={wireApi} onValueChange={(v) => setWireApi(v as WireApiType)}>
558
+ <SelectTrigger className="rounded-xl">
559
+ <SelectValue />
560
+ </SelectTrigger>
561
+ <SelectContent>
562
+ {(providerSpec.wireApiOptions || ['auto', 'chat', 'responses']).map((option) => (
563
+ <SelectItem key={option} value={option}>
564
+ {option === 'chat' ? t('wireApiChat') : option === 'responses' ? t('wireApiResponses') : t('wireApiAuto')}
565
+ </SelectItem>
566
+ ))}
567
+ </SelectContent>
568
+ </Select>
569
+ </div>
570
+ )}
571
+
572
+ <div className="space-y-2">
573
+ <Label className="text-sm font-medium text-gray-900">
574
+ {extraHeadersHint?.label ?? t('extraHeaders')}
575
+ </Label>
576
+ <KeyValueEditor value={extraHeaders} onChange={setExtraHeaders} />
577
+ <p className="text-xs text-gray-500">{t('providerExtraHeadersHelpShort')}</p>
578
+ </div>
579
+ </div>
549
580
  )}
550
- <Button type="button" variant="outline" onClick={handleTestConnection} disabled={testProviderConnection.isPending}>
551
- <CircleDotDashed className="mr-2 h-4 w-4" />
552
- {testProviderConnection.isPending ? t('providerTestingConnection') : t('providerTestConnection')}
553
- </Button>
554
581
  </div>
582
+ </div>
583
+
584
+ <div className="flex items-center justify-between border-t border-gray-100 px-6 py-4">
585
+ <Button type="button" variant="outline" size="sm" onClick={handleTestConnection} disabled={testProviderConnection.isPending}>
586
+ <CircleDotDashed className="mr-1.5 h-4 w-4" />
587
+ {testProviderConnection.isPending ? t('providerTestingConnection') : t('providerTestConnection')}
588
+ </Button>
555
589
  <Button type="submit" disabled={updateProvider.isPending || !hasChanges}>
556
590
  {updateProvider.isPending ? t('saving') : hasChanges ? t('save') : t('unchanged')}
557
591
  </Button>
@@ -9,7 +9,7 @@ const Input = React.forwardRef<HTMLInputElement, InputProps>(
9
9
  <input
10
10
  type={type}
11
11
  className={cn(
12
- 'flex h-9 w-full rounded-xl border border-gray-200/80 bg-white px-3.5 py-2 text-sm file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-gray-400 focus:outline-none focus:ring-1 focus:ring-primary/40 focus:border-primary/40 transition-colors disabled:cursor-not-allowed disabled:opacity-50',
12
+ 'flex h-9 w-full rounded-xl border border-gray-200/80 bg-white px-3.5 py-2 text-sm text-gray-900 file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-gray-300 placeholder:font-normal focus:outline-none focus:ring-1 focus:ring-primary/40 focus:border-primary/40 transition-colors disabled:cursor-not-allowed disabled:opacity-50',
13
13
  className
14
14
  )}
15
15
  ref={ref}
package/src/lib/i18n.ts CHANGED
@@ -243,6 +243,12 @@ export const LABELS: Record<string, { zh: string; en: string }> = {
243
243
  zh: '系统会先填充预置模型;你可以在这里新增或删除。请填写当前提供商自己的模型 ID(不带当前 provider 前缀);若输入带当前 provider 前缀会自动去除,但会保留后续路径(如 openai/gpt-5)。',
244
244
  en: 'Built-in models are prefilled and can be added or removed here. Enter provider-local model ids without the current provider prefix; if prefixed input is entered, only the current provider prefix is removed while the remaining path is preserved (for example, openai/gpt-5).'
245
245
  },
246
+ providerModelsEmptyShort: { zh: '暂无可用模型', en: 'No models available' },
247
+ providerAddFirstModel: { zh: '添加第一个模型', en: 'Add first model' },
248
+ providerDisplayNameHelpShort: { zh: '便于区分多个自定义提供商', en: 'Helps distinguish multiple custom providers' },
249
+ providerApiBaseHelpShort: { zh: '一般只需填写域名,系统自动补全路径', en: 'Usually just the domain; path auto-appended' },
250
+ providerExtraHeadersHelpShort: { zh: '可选,用于自定义鉴权等场景', en: 'Optional, for custom auth etc.' },
251
+ providerAdvancedSettings: { zh: '高级设置', en: 'Advanced Settings' },
246
252
  resetToDefault: { zh: '恢复默认', en: 'Reset to Default' },
247
253
  leaveBlankToKeepUnchanged: { zh: '留空则保持不变', en: 'Leave blank to keep unchanged' },
248
254
 
@@ -1 +0,0 @@
1
- import{r as p,j as e,aa as Ae,ab as Ce,a8 as me,a6 as ne,ac as se,K as ue,a1 as Pe,m as De,ad as Le,ae as Me,af as Se,ag as te,a0 as Ee}from"./vendor-DN_iJQc4.js";import{I,u as xe,a as he,b as pe,g as Te,h as Be,i as Ke,j as $e}from"./useConfig-0trWsjx9.js";import{B as S,P as ke,a as Ie}from"./page-layout-C62p_dlG.js";import{L as W}from"./label-BowGMNYa.js";import{t,c as J,S as Fe,a as Oe,b as _e,d as Re,e as We}from"./index-C8UPX5Q7.js";import{C as He,a as ze,S as ye,b as Ge,c as Ve,L as qe,g as Ue}from"./logos-BlD3kGKz.js";import{h as q}from"./config-hints-CApS3K_7.js";import{T as Qe}from"./tabs-custom-D1fYWpL_.js";function Xe({maskedValue:s,isSet:a,className:n,...i}){const[u,b]=p.useState(!1);return e.jsxs("div",{className:"relative",children:[e.jsx(I,{type:u?"text":"password",className:J("pr-20",n),placeholder:a?`${t("apiKeySet")} (${t("unchanged")})`:"",...i}),e.jsx("div",{className:"absolute right-2 top-1/2 -translate-y-1/2 flex gap-1",children:(a||s)&&e.jsx(S,{type:"button",variant:"ghost",size:"icon",className:"h-7 w-7",onClick:()=>b(!u),children:u?e.jsx(Ae,{className:"h-4 w-4"}):e.jsx(Ce,{className:"h-4 w-4"})})})]})}function Ye({value:s,onChange:a,className:n}){const i=s?Object.entries(s):[],u=(m,x,g)=>{const f=[...i];f[m]=[x,g],a(Object.fromEntries(f))},b=()=>{a({...s,"":""})},y=m=>{const x=i.filter((g,f)=>f!==m);a(Object.fromEntries(x))};return e.jsxs("div",{className:J("space-y-2",n),children:[i.map(([m,x],g)=>e.jsxs("div",{className:"flex gap-2",children:[e.jsx(I,{type:"text",value:m,onChange:f=>u(g,f.target.value,x),placeholder:t("headerName"),className:"flex-1"}),e.jsx(I,{type:"text",value:x,onChange:f=>u(g,m,f.target.value),placeholder:t("headerValue"),className:"flex-1"}),e.jsx(S,{type:"button",variant:"ghost",size:"icon",onClick:()=>y(g),children:e.jsx(me,{className:"h-4 w-4 text-red-500"})})]},g)),e.jsxs(S,{type:"button",variant:"outline",size:"sm",onClick:b,children:[e.jsx(ne,{className:"h-4 w-4 mr-2"}),t("add")]})]})}function U(s){if(!s)return null;const a=Object.entries(s).map(([n,i])=>[n.trim(),i]).filter(([n])=>n.length>0);return a.length===0?null:Object.fromEntries(a)}function de(s,a){const n=U(s),i=U(a);if(n===null&&i===null)return!0;if(!n||!i)return!1;const u=Object.entries(n).sort(([y],[m])=>y.localeCompare(m)),b=Object.entries(i).sort(([y],[m])=>y.localeCompare(m));return u.length!==b.length?!1:u.every(([y,m],x)=>y===b[x][0]&&m===b[x][1])}function ae(s){if(!s||s.length===0)return[];const a=new Set;for(const n of s){const i=n.trim();i&&a.add(i)}return[...a]}function Je(s,a){const n=s.trim();if(!n||!a.trim())return n;const i=`${a.trim()}/`;return n.startsWith(i)?n.slice(i.length):n}function Y(s,a){let n=s.trim();if(!n)return"";for(const i of a){const u=i.trim();u&&(n=Je(n,u))}return n.trim()}function re(s,a){return s.length!==a.length?!1:s.every((n,i)=>n===a[i])}function Ze(s,a){const n=[...s];for(const i of a)n.includes(i)||n.push(i);return n}function es(s,a){return a.length===0?s:a.every(i=>!s.includes(i))?Ze(s,a):a}function ss(s,a){return re(s,a)?[]:s}function ts({providerName:s,onProviderDeleted:a}){const{data:n}=xe(),{data:i}=he(),{data:u}=pe(),b=Te(),y=Be(),m=Ke(),[x,g]=p.useState(""),[f,B]=p.useState(""),[K,F]=p.useState(null),[E,A]=p.useState("auto"),[C,$]=p.useState([]),[o,d]=p.useState(""),[N,v]=p.useState(""),r=i==null?void 0:i.providers.find(l=>l.name===s),c=s?n==null?void 0:n.providers[s]:null,L=u==null?void 0:u.uiHints,P=!!(r!=null&&r.isCustom),T=s?q(`providers.${s}.apiKey`,L):void 0,w=s?q(`providers.${s}.apiBase`,L):void 0,O=s?q(`providers.${s}.extraHeaders`,L):void 0,k=s?q(`providers.${s}.wireApi`,L):void 0,Z=(r==null?void 0:r.displayName)||s||"",_=((c==null?void 0:c.displayName)||"").trim()||Z,fe=N.trim()||_||s||t("providersSelectPlaceholder"),le=(r==null?void 0:r.modelPrefix)||s||"",R=p.useMemo(()=>ae([le,s||""]),[le,s]),Q=(r==null?void 0:r.defaultApiBase)||"",H=(c==null?void 0:c.apiBase)||Q,ee=U((c==null?void 0:c.extraHeaders)||null),z=(c==null?void 0:c.wireApi)||(r==null?void 0:r.defaultWireApi)||"auto",X=p.useMemo(()=>ae(((r==null?void 0:r.defaultModels)??[]).map(l=>Y(l,R))),[r==null?void 0:r.defaultModels,R]),ie=p.useMemo(()=>ae(((c==null?void 0:c.models)??[]).map(l=>Y(l,R))),[c==null?void 0:c.models,R]),G=p.useMemo(()=>es(X,ie),[X,ie]),ge=s==="minimax"?t("providerApiBaseHelpMinimax"):(w==null?void 0:w.help)||t("providerApiBaseHelp");p.useEffect(()=>{if(!s){g(""),B(""),F(null),A("auto"),$([]),d(""),v("");return}g(""),B(H),F((c==null?void 0:c.extraHeaders)||null),A(z),$(G),d(""),v(_)},[s,H,c==null?void 0:c.extraHeaders,z,G,_]);const ce=p.useMemo(()=>{if(!s)return!1;const l=x.trim().length>0,j=f.trim()!==H.trim(),D=!de(K,ee),h=r!=null&&r.supportsWireApi?E!==z:!1,M=!re(C,G),V=P?N.trim()!==_:!1;return l||j||D||h||M||V},[s,P,N,_,x,f,H,K,ee,r==null?void 0:r.supportsWireApi,E,z,C,G]),je=()=>{g(""),B(Q),F(null),A((r==null?void 0:r.defaultWireApi)||"auto"),$(X),d(""),v(Z)},oe=()=>{const l=Y(o,R);if(l){if(C.includes(l)){d("");return}$(j=>[...j,l]),d("")}},be=l=>{if(l.preventDefault(),!s)return;const j={},D=x.trim(),h=f.trim(),M=U(K),V=N.trim();P&&V!==_&&(j.displayName=V.length>0?V:null),D.length>0&&(j.apiKey=D),h!==H.trim()&&(j.apiBase=h.length>0&&h!==Q?h:null),de(M,ee)||(j.extraHeaders=M),r!=null&&r.supportsWireApi&&E!==z&&(j.wireApi=E),re(C,G)||(j.models=ss(C,X)),b.mutate({provider:s,data:j})},Ne=async()=>{if(!s)return;const l=C.find(h=>h.trim().length>0)??"",j=Y(l,R),D={apiBase:f.trim(),extraHeaders:U(K),model:j||null};x.trim().length>0&&(D.apiKey=x.trim()),r!=null&&r.supportsWireApi&&(D.wireApi=E);try{const h=await m.mutateAsync({provider:s,data:D});if(h.success){te.success(`${t("providerTestConnectionSuccess")} (${h.latencyMs}ms)`);return}const M=[`provider=${h.provider}`,`latency=${h.latencyMs}ms`];h.model&&M.push(`model=${h.model}`),te.error(`${t("providerTestConnectionFailed")}: ${h.message} | ${M.join(" | ")}`)}catch(h){const M=h instanceof Error?h.message:String(h);te.error(`${t("providerTestConnectionFailed")}: ${M}`)}},ve=async()=>{if(!(!s||!P||!window.confirm(t("providerDeleteConfirm"))))try{await y.mutateAsync({provider:s}),a==null||a(s)}catch{}};if(!s||!r||!c)return e.jsx("div",{className:He,children:e.jsxs("div",{children:[e.jsx("h3",{className:"text-base font-semibold text-gray-900",children:t("providersSelectTitle")}),e.jsx("p",{className:"mt-2 text-sm text-gray-500",children:t("providersSelectDescription")})]})});const we=c.apiKeySet?t("statusReady"):t("statusSetup");return e.jsxs("div",{className:ze,children:[e.jsx("div",{className:"border-b border-gray-100 px-6 py-5",children:e.jsxs("div",{className:"flex flex-wrap items-center justify-between gap-3",children:[e.jsxs("div",{className:"min-w-0",children:[e.jsx("h3",{className:"truncate text-lg font-semibold text-gray-900",children:fe}),e.jsx("p",{className:"mt-1 text-sm text-gray-500",children:t("providerFormDescription")})]}),e.jsx(ye,{status:c.apiKeySet?"ready":"setup",label:we})]})}),e.jsxs("form",{onSubmit:be,className:"flex min-h-0 flex-1 flex-col",children:[e.jsxs("div",{className:"min-h-0 flex-1 space-y-6 overflow-y-auto px-6 py-5",children:[P&&e.jsxs("div",{className:"space-y-2.5",children:[e.jsxs(W,{htmlFor:"providerDisplayName",className:"flex items-center gap-2 text-sm font-medium text-gray-900",children:[e.jsx(se,{className:"h-3.5 w-3.5 text-gray-500"}),t("providerDisplayName")]}),e.jsx(I,{id:"providerDisplayName",type:"text",value:N,onChange:l=>v(l.target.value),placeholder:Z||t("providerDisplayNamePlaceholder"),className:"rounded-xl"}),e.jsx("p",{className:"text-xs text-gray-500",children:t("providerDisplayNameHelp")})]}),e.jsxs("div",{className:"space-y-2.5",children:[e.jsxs(W,{htmlFor:"apiKey",className:"flex items-center gap-2 text-sm font-medium text-gray-900",children:[e.jsx(ue,{className:"h-3.5 w-3.5 text-gray-500"}),(T==null?void 0:T.label)??t("apiKey")]}),e.jsx(Xe,{id:"apiKey",value:x,isSet:c.apiKeySet,onChange:l=>g(l.target.value),placeholder:c.apiKeySet?t("apiKeySet"):(T==null?void 0:T.placeholder)??t("enterApiKey"),className:"rounded-xl"}),e.jsx("p",{className:"text-xs text-gray-500",children:t("leaveBlankToKeepUnchanged")})]}),e.jsxs("div",{className:"space-y-2.5",children:[e.jsxs(W,{htmlFor:"apiBase",className:"flex items-center gap-2 text-sm font-medium text-gray-900",children:[e.jsx(Pe,{className:"h-3.5 w-3.5 text-gray-500"}),(w==null?void 0:w.label)??t("apiBase")]}),e.jsx(I,{id:"apiBase",type:"text",value:f,onChange:l=>B(l.target.value),placeholder:Q||(w==null?void 0:w.placeholder)||"https://api.example.com",className:"rounded-xl"}),e.jsx("p",{className:"text-xs text-gray-500",children:ge}),P&&e.jsx("p",{className:"text-xs text-gray-500",children:t("providerOpenAICompatHint")})]}),r.supportsWireApi&&e.jsxs("div",{className:"space-y-2.5",children:[e.jsxs(W,{htmlFor:"wireApi",className:"flex items-center gap-2 text-sm font-medium text-gray-900",children:[e.jsx(se,{className:"h-3.5 w-3.5 text-gray-500"}),(k==null?void 0:k.label)??t("wireApi")]}),e.jsxs(Fe,{value:E,onValueChange:l=>A(l),children:[e.jsx(Oe,{className:"rounded-xl",children:e.jsx(_e,{})}),e.jsx(Re,{children:(r.wireApiOptions||["auto","chat","responses"]).map(l=>e.jsx(We,{value:l,children:l==="chat"?t("wireApiChat"):l==="responses"?t("wireApiResponses"):t("wireApiAuto")},l))})]}),(k==null?void 0:k.help)&&e.jsx("p",{className:"text-xs text-gray-500",children:k.help})]}),e.jsxs("div",{className:"space-y-2.5",children:[e.jsxs(W,{className:"flex items-center gap-2 text-sm font-medium text-gray-900",children:[e.jsx(se,{className:"h-3.5 w-3.5 text-gray-500"}),(O==null?void 0:O.label)??t("extraHeaders")]}),e.jsx(Ye,{value:K,onChange:F}),e.jsx("p",{className:"text-xs text-gray-500",children:(O==null?void 0:O.help)||t("providerExtraHeadersHelp")})]}),e.jsxs("div",{className:"space-y-2.5",children:[e.jsxs(W,{className:"flex items-center gap-2 text-sm font-medium text-gray-900",children:[e.jsx(De,{className:"h-3.5 w-3.5 text-gray-500"}),t("providerModelsTitle")]}),e.jsxs("div",{className:"flex items-center gap-2",children:[e.jsx(I,{value:o,onChange:l=>d(l.target.value),onKeyDown:l=>{l.key==="Enter"&&(l.preventDefault(),oe())},placeholder:t("providerModelInputPlaceholder"),className:"flex-1"}),e.jsxs(S,{type:"button",variant:"outline",onClick:oe,disabled:o.trim().length===0,children:[e.jsx(ne,{className:"mr-1.5 h-4 w-4"}),t("providerAddModel")]})]}),e.jsx("p",{className:"text-xs text-gray-500",children:t("providerModelInputHint")}),C.length===0?e.jsx("div",{className:"rounded-xl border border-dashed border-gray-200 bg-gray-50 px-3 py-2 text-xs text-gray-500",children:t("providerModelsEmpty")}):e.jsx("div",{className:"flex flex-wrap gap-2",children:C.map(l=>e.jsxs("div",{className:"group inline-flex max-w-full items-center gap-1 rounded-full border border-gray-200 bg-white px-3 py-1.5",children:[e.jsx("span",{className:"max-w-[180px] truncate text-sm text-gray-800 sm:max-w-[240px]",children:l}),e.jsx("button",{type:"button",onClick:()=>$(j=>j.filter(D=>D!==l)),className:"inline-flex h-5 w-5 items-center justify-center rounded-full text-gray-400 transition-opacity hover:bg-gray-100 hover:text-gray-600 opacity-100 md:opacity-0 md:group-hover:opacity-100 md:group-focus-within:opacity-100","aria-label":t("remove"),children:e.jsx(Le,{className:"h-3 w-3"})})]},l))}),e.jsx("p",{className:"text-xs text-gray-500",children:t("providerModelsHelp")})]})]}),e.jsxs("div",{className:"flex flex-wrap items-center justify-between gap-3 border-t border-gray-100 px-6 py-4",children:[e.jsxs("div",{className:"flex flex-wrap items-center gap-2",children:[e.jsxs(S,{type:"button",variant:"outline",onClick:je,children:[e.jsx(Me,{className:"mr-2 h-4 w-4"}),t("resetToDefault")]}),P&&e.jsxs(S,{type:"button",variant:"outline",onClick:ve,disabled:y.isPending,children:[e.jsx(me,{className:"mr-2 h-4 w-4"}),y.isPending?t("saving"):t("providerDelete")]}),e.jsxs(S,{type:"button",variant:"outline",onClick:Ne,disabled:m.isPending,children:[e.jsx(Se,{className:"mr-2 h-4 w-4"}),m.isPending?t("providerTestingConnection"):t("providerTestConnection")]})]}),e.jsx(S,{type:"submit",disabled:b.isPending||!ce,children:b.isPending?t("saving"):ce?t("save"):t("unchanged")})]})]})]})}function as(s){if(!s)return null;try{const a=new URL(s),n=a.pathname&&a.pathname!=="/"?a.pathname:"";return`${a.host}${n}`}catch{return s.replace(/^https?:\/\//,"")}}function xs(){const{data:s}=xe(),{data:a}=he(),{data:n}=pe(),i=$e(),[u,b]=p.useState("installed"),[y,m]=p.useState(),[x,g]=p.useState(""),f=n==null?void 0:n.uiHints,B=(a==null?void 0:a.providers)??[],K=(s==null?void 0:s.providers)??{},F=B.filter(o=>{var d;return(d=K[o.name])==null?void 0:d.apiKeySet}).length,E=[{id:"installed",label:t("providersTabConfigured"),count:F},{id:"all",label:t("providersTabAll"),count:B.length}],A=p.useMemo(()=>{const o=(a==null?void 0:a.providers)??[],d=(s==null?void 0:s.providers)??{},N=x.trim().toLowerCase();return o.filter(v=>{var r;return u==="installed"?!!((r=d[v.name])!=null&&r.apiKeySet):!0}).filter(v=>{var L,P;return N?(((P=(L=d[v.name])==null?void 0:L.displayName)==null?void 0:P.trim())||v.displayName||v.name).toLowerCase().includes(N)||v.name.toLowerCase().includes(N):!0})},[a,s,u,x]);p.useEffect(()=>{if(A.length===0){m(void 0);return}A.some(d=>d.name===y)||m(A[0].name)},[A,y]);const C=y,$=async()=>{try{const o=await i.mutateAsync({data:{}});b("all"),g(""),m(o.name)}catch{}};return!s||!a?e.jsx("div",{className:"p-8",children:t("providersLoading")}):e.jsxs(ke,{children:[e.jsx(Ie,{title:t("providersPageTitle"),description:t("providersPageDescription")}),e.jsxs("div",{className:Ge,children:[e.jsxs("section",{className:Ve,children:[e.jsxs("div",{className:"border-b border-gray-100 px-4 pt-4 pb-3 space-y-3",children:[e.jsx(Qe,{tabs:E,activeTab:u,onChange:b,className:"mb-0"}),e.jsxs(S,{type:"button",variant:"outline",className:"w-full justify-center",onClick:$,disabled:i.isPending,children:[e.jsx(ne,{className:"mr-2 h-4 w-4"}),i.isPending?t("saving"):t("providerAddCustom")]})]}),e.jsx("div",{className:"border-b border-gray-100 px-4 py-3",children:e.jsxs("div",{className:"relative",children:[e.jsx(Ee,{className:"pointer-events-none absolute left-3 top-1/2 h-4 w-4 -translate-y-1/2 text-gray-400"}),e.jsx(I,{value:x,onChange:o=>g(o.target.value),placeholder:t("providersFilterPlaceholder"),className:"h-10 rounded-xl pl-9"})]})}),e.jsxs("div",{className:"min-h-0 flex-1 space-y-2 overflow-y-auto p-3",children:[A.map(o=>{var w;const d=s.providers[o.name],N=!!(d!=null&&d.apiKeySet),v=C===o.name,r=((w=d==null?void 0:d.displayName)==null?void 0:w.trim())||o.displayName||o.name,c=q(`providers.${o.name}`,f),L=(d==null?void 0:d.apiBase)||o.defaultApiBase||"",T=as(L)||(c==null?void 0:c.help)||t("providersDefaultDescription");return e.jsx("button",{type:"button",onClick:()=>m(o.name),className:J("w-full rounded-xl border p-2.5 text-left transition-all",v?"border-primary/30 bg-primary-50/40 shadow-sm":"border-gray-200/70 bg-white hover:border-gray-300 hover:bg-gray-50/70"),children:e.jsxs("div",{className:"flex items-start justify-between gap-3",children:[e.jsxs("div",{className:"flex min-w-0 items-center gap-3",children:[e.jsx(qe,{name:o.name,src:Ue(o.name),className:J("h-10 w-10 rounded-lg border",N?"border-primary/30 bg-white":"border-gray-200/70 bg-white"),imgClassName:"h-5 w-5 object-contain",fallback:e.jsx("span",{className:"text-sm font-semibold uppercase text-gray-500",children:o.name[0]})}),e.jsxs("div",{className:"min-w-0",children:[e.jsx("p",{className:"truncate text-sm font-semibold text-gray-900",children:r}),e.jsx("p",{className:"line-clamp-1 text-[11px] text-gray-500",children:T})]})]}),e.jsx(ye,{status:N?"ready":"setup",label:N?t("statusReady"):t("statusSetup"),className:"min-w-[56px] justify-center"})]})},o.name)}),A.length===0&&e.jsxs("div",{className:"flex h-full min-h-[220px] flex-col items-center justify-center rounded-xl border border-dashed border-gray-200 bg-gray-50/70 py-10 text-center",children:[e.jsx("div",{className:"mb-3 flex h-10 w-10 items-center justify-center rounded-lg bg-white",children:e.jsx(ue,{className:"h-5 w-5 text-gray-300"})}),e.jsx("p",{className:"text-sm font-medium text-gray-700",children:t("providersNoMatch")})]})]})]}),e.jsx(ts,{providerName:C,onProviderDeleted:o=>{o===y&&m(void 0)}})]})]})}export{xs as ProvidersList};