@nextclaw/ui 0.5.38 → 0.5.40

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 (33) hide show
  1. package/CHANGELOG.md +12 -0
  2. package/dist/assets/{ChannelsList-3B_zyiKA.js → ChannelsList-B30C2Tc1.js} +1 -1
  3. package/dist/assets/{ChatPage-DusH09PT.js → ChatPage-avQKMYYz.js} +1 -1
  4. package/dist/assets/{CronConfig-5GTz5wPt.js → CronConfig-BMlXWhYq.js} +1 -1
  5. package/dist/assets/{DocBrowser-BtqGmg0N.js → DocBrowser-DJvqOv4P.js} +1 -1
  6. package/dist/assets/{MarketplacePage-BEW4M9BT.js → MarketplacePage-ClpeHXw_.js} +1 -1
  7. package/dist/assets/ModelConfig-C7goS4WN.js +1 -0
  8. package/dist/assets/ProvidersList-D-PD0V-a.js +1 -0
  9. package/dist/assets/{RuntimeConfig-BwTxGi_U.js → RuntimeConfig-CIv4GGnC.js} +1 -1
  10. package/dist/assets/{SecretsConfig-x36MY4ym.js → SecretsConfig-4_Q0C2xp.js} +2 -2
  11. package/dist/assets/{SessionsConfig-qEffYDZ0.js → SessionsConfig-CtSAiFfb.js} +1 -1
  12. package/dist/assets/{card-Bq6uwDJQ.js → card-sNuXU_r7.js} +1 -1
  13. package/dist/assets/index-C1zSMeji.js +2 -0
  14. package/dist/assets/{label-Cq1vSfWg.js → label-D4Ubt_vJ.js} +1 -1
  15. package/dist/assets/{logos-BKBMs40Q.js → logos-DKUohpuv.js} +1 -1
  16. package/dist/assets/{page-layout-D8MW2vP-.js → page-layout-BrRUG9hW.js} +1 -1
  17. package/dist/assets/{switch-CycMxy31.js → switch-B3olqa--.js} +1 -1
  18. package/dist/assets/{tabs-custom-N4olWJSw.js → tabs-custom-ClgZ4Lyg.js} +1 -1
  19. package/dist/assets/useConfig-DiBU1IBR.js +6 -0
  20. package/dist/assets/{useConfirmDialog-DE0Yp8Ai.js → useConfirmDialog-i-F39tiz.js} +1 -1
  21. package/dist/index.html +1 -1
  22. package/package.json +1 -1
  23. package/src/api/config.ts +28 -0
  24. package/src/api/types.ts +15 -0
  25. package/src/components/config/ModelConfig.tsx +2 -1
  26. package/src/components/config/ProviderForm.tsx +87 -8
  27. package/src/components/config/ProvidersList.tsx +38 -6
  28. package/src/hooks/useConfig.ts +36 -0
  29. package/src/lib/i18n.ts +17 -0
  30. package/dist/assets/ModelConfig-CwxXYqME.js +0 -1
  31. package/dist/assets/ProvidersList-D4oaYHpJ.js +0 -1
  32. package/dist/assets/index-wB2uPrKu.js +0 -2
  33. package/dist/assets/useConfig-tR_KAfMV.js +0 -6
package/src/api/config.ts CHANGED
@@ -8,6 +8,9 @@ import type {
8
8
  ProviderConfigUpdate,
9
9
  ProviderConnectionTestRequest,
10
10
  ProviderConnectionTestResult,
11
+ ProviderCreateRequest,
12
+ ProviderCreateResult,
13
+ ProviderDeleteResult,
11
14
  RuntimeConfigUpdate,
12
15
  SecretsConfigUpdate,
13
16
  SecretsView,
@@ -81,6 +84,31 @@ export async function updateProvider(
81
84
  return response.data;
82
85
  }
83
86
 
87
+ // POST /api/config/providers
88
+ export async function createProvider(
89
+ data: ProviderCreateRequest = {}
90
+ ): Promise<ProviderCreateResult> {
91
+ const response = await api.post<ProviderCreateResult>(
92
+ '/api/config/providers',
93
+ data
94
+ );
95
+ if (!response.ok) {
96
+ throw new Error(response.error.message);
97
+ }
98
+ return response.data;
99
+ }
100
+
101
+ // DELETE /api/config/providers/:provider
102
+ export async function deleteProvider(provider: string): Promise<ProviderDeleteResult> {
103
+ const response = await api.delete<ProviderDeleteResult>(
104
+ `/api/config/providers/${provider}`
105
+ );
106
+ if (!response.ok) {
107
+ throw new Error(response.error.message);
108
+ }
109
+ return response.data;
110
+ }
111
+
84
112
  // POST /api/config/providers/:provider/test
85
113
  export async function testProviderConnection(
86
114
  provider: string,
package/src/api/types.ts CHANGED
@@ -10,6 +10,7 @@ export type ApiResponse<T> =
10
10
  | { ok: false; error: ApiError };
11
11
 
12
12
  export type ProviderConfigView = {
13
+ displayName?: string;
13
14
  apiKeySet: boolean;
14
15
  apiKeyMasked?: string;
15
16
  apiBase?: string | null;
@@ -19,6 +20,7 @@ export type ProviderConfigView = {
19
20
  };
20
21
 
21
22
  export type ProviderConfigUpdate = {
23
+ displayName?: string | null;
22
24
  apiKey?: string | null;
23
25
  apiBase?: string | null;
24
26
  extraHeaders?: Record<string, string> | null;
@@ -30,6 +32,18 @@ export type ProviderConnectionTestRequest = ProviderConfigUpdate & {
30
32
  model?: string | null;
31
33
  };
32
34
 
35
+ export type ProviderCreateRequest = ProviderConfigUpdate;
36
+
37
+ export type ProviderCreateResult = {
38
+ name: string;
39
+ provider: ProviderConfigView;
40
+ };
41
+
42
+ export type ProviderDeleteResult = {
43
+ deleted: boolean;
44
+ provider: string;
45
+ };
46
+
33
47
  export type ProviderConnectionTestErrorCode =
34
48
  | 'API_KEY_REQUIRED'
35
49
  | 'MODEL_REQUIRED'
@@ -325,6 +339,7 @@ export type ConfigView = {
325
339
  export type ProviderSpecView = {
326
340
  name: string;
327
341
  displayName?: string;
342
+ isCustom?: boolean;
328
343
  modelPrefix?: string;
329
344
  keywords: string[];
330
345
  envKey: string;
@@ -100,9 +100,10 @@ export function ModelConfig() {
100
100
  (config?.providers?.[provider.name]?.models ?? []).map((model) => toProviderLocalModel(model, aliases))
101
101
  );
102
102
  const allModels = normalizeStringList([...defaultModels, ...customModels]);
103
+ const configDisplayName = config?.providers?.[provider.name]?.displayName?.trim();
103
104
  return {
104
105
  name: provider.name,
105
- displayName: provider.displayName || provider.name,
106
+ displayName: configDisplayName || provider.displayName || provider.name,
106
107
  prefix,
107
108
  aliases,
108
109
  models: allModels
@@ -1,5 +1,12 @@
1
1
  import { useEffect, useMemo, useState } from 'react';
2
- import { useConfig, useConfigMeta, useConfigSchema, useTestProviderConnection, useUpdateProvider } from '@/hooks/useConfig';
2
+ import {
3
+ useConfig,
4
+ useConfigMeta,
5
+ useConfigSchema,
6
+ useDeleteProvider,
7
+ useTestProviderConnection,
8
+ useUpdateProvider
9
+ } from '@/hooks/useConfig';
3
10
  import { Button } from '@/components/ui/button';
4
11
  import { Input } from '@/components/ui/input';
5
12
  import { Label } from '@/components/ui/label';
@@ -10,7 +17,7 @@ import { StatusDot } from '@/components/ui/status-dot';
10
17
  import { t } from '@/lib/i18n';
11
18
  import { hintForPath } from '@/lib/config-hints';
12
19
  import type { ProviderConfigUpdate, ProviderConnectionTestRequest } from '@/api/types';
13
- import { KeyRound, Globe, Hash, RotateCcw, CircleDotDashed, Sparkles, Plus, X } from 'lucide-react';
20
+ import { KeyRound, Globe, Hash, RotateCcw, CircleDotDashed, Sparkles, Plus, X, Trash2 } from 'lucide-react';
14
21
  import { toast } from 'sonner';
15
22
  import { CONFIG_DETAIL_CARD_CLASS, CONFIG_EMPTY_DETAIL_CARD_CLASS } from './config-layout';
16
23
 
@@ -18,6 +25,7 @@ type WireApiType = 'auto' | 'chat' | 'responses';
18
25
 
19
26
  type ProviderFormProps = {
20
27
  providerName?: string;
28
+ onProviderDeleted?: (providerName: string) => void;
21
29
  };
22
30
 
23
31
  function normalizeHeaders(input: Record<string, string> | null | undefined): Record<string, string> | null {
@@ -129,11 +137,12 @@ function serializeModelsForSave(models: string[], defaultModels: string[]): stri
129
137
  return models;
130
138
  }
131
139
 
132
- export function ProviderForm({ providerName }: ProviderFormProps) {
140
+ export function ProviderForm({ providerName, onProviderDeleted }: ProviderFormProps) {
133
141
  const { data: config } = useConfig();
134
142
  const { data: meta } = useConfigMeta();
135
143
  const { data: schema } = useConfigSchema();
136
144
  const updateProvider = useUpdateProvider();
145
+ const deleteProvider = useDeleteProvider();
137
146
  const testProviderConnection = useTestProviderConnection();
138
147
 
139
148
  const [apiKey, setApiKey] = useState('');
@@ -142,17 +151,22 @@ export function ProviderForm({ providerName }: ProviderFormProps) {
142
151
  const [wireApi, setWireApi] = useState<WireApiType>('auto');
143
152
  const [models, setModels] = useState<string[]>([]);
144
153
  const [modelDraft, setModelDraft] = useState('');
154
+ const [providerDisplayName, setProviderDisplayName] = useState('');
145
155
 
146
156
  const providerSpec = meta?.providers.find((p) => p.name === providerName);
147
157
  const providerConfig = providerName ? config?.providers[providerName] : null;
148
158
  const uiHints = schema?.uiHints;
159
+ const isCustomProvider = Boolean(providerSpec?.isCustom);
149
160
 
150
161
  const apiKeyHint = providerName ? hintForPath(`providers.${providerName}.apiKey`, uiHints) : undefined;
151
162
  const apiBaseHint = providerName ? hintForPath(`providers.${providerName}.apiBase`, uiHints) : undefined;
152
163
  const extraHeadersHint = providerName ? hintForPath(`providers.${providerName}.extraHeaders`, uiHints) : undefined;
153
164
  const wireApiHint = providerName ? hintForPath(`providers.${providerName}.wireApi`, uiHints) : undefined;
165
+ const defaultDisplayName = providerSpec?.displayName || providerName || '';
166
+ const currentDisplayName = (providerConfig?.displayName || '').trim();
167
+ const effectiveDisplayName = currentDisplayName || defaultDisplayName;
154
168
 
155
- const providerTitle = providerSpec?.displayName || providerName || t('providersSelectPlaceholder');
169
+ const providerTitle = providerDisplayName.trim() || effectiveDisplayName || providerName || t('providersSelectPlaceholder');
156
170
  const providerModelPrefix = providerSpec?.modelPrefix || providerName || '';
157
171
  const providerModelAliases = useMemo(
158
172
  () => normalizeModelList([providerModelPrefix, providerName || '']),
@@ -180,6 +194,9 @@ export function ProviderForm({ providerName }: ProviderFormProps) {
180
194
  () => resolveEditableModels(defaultModels, currentModels),
181
195
  [defaultModels, currentModels]
182
196
  );
197
+ const apiBaseHelpText = providerName === 'minimax'
198
+ ? t('providerApiBaseHelpMinimax')
199
+ : (apiBaseHint?.help || t('providerApiBaseHelp'));
183
200
 
184
201
  useEffect(() => {
185
202
  if (!providerName) {
@@ -189,6 +206,7 @@ export function ProviderForm({ providerName }: ProviderFormProps) {
189
206
  setWireApi('auto');
190
207
  setModels([]);
191
208
  setModelDraft('');
209
+ setProviderDisplayName('');
192
210
  return;
193
211
  }
194
212
 
@@ -198,7 +216,8 @@ export function ProviderForm({ providerName }: ProviderFormProps) {
198
216
  setWireApi(currentWireApi);
199
217
  setModels(currentEditableModels);
200
218
  setModelDraft('');
201
- }, [providerName, currentApiBase, providerConfig?.extraHeaders, currentWireApi, currentEditableModels]);
219
+ setProviderDisplayName(effectiveDisplayName);
220
+ }, [providerName, currentApiBase, providerConfig?.extraHeaders, currentWireApi, currentEditableModels, effectiveDisplayName]);
202
221
 
203
222
  const hasChanges = useMemo(() => {
204
223
  if (!providerName) {
@@ -209,10 +228,16 @@ export function ProviderForm({ providerName }: ProviderFormProps) {
209
228
  const headersChanged = !headersEqual(extraHeaders, currentHeaders);
210
229
  const wireApiChanged = providerSpec?.supportsWireApi ? wireApi !== currentWireApi : false;
211
230
  const modelsChanged = !modelListsEqual(models, currentEditableModels);
231
+ const displayNameChanged = isCustomProvider
232
+ ? providerDisplayName.trim() !== effectiveDisplayName
233
+ : false;
212
234
 
213
- return apiKeyChanged || apiBaseChanged || headersChanged || wireApiChanged || modelsChanged;
235
+ return apiKeyChanged || apiBaseChanged || headersChanged || wireApiChanged || modelsChanged || displayNameChanged;
214
236
  }, [
215
237
  providerName,
238
+ isCustomProvider,
239
+ providerDisplayName,
240
+ effectiveDisplayName,
216
241
  apiKey,
217
242
  apiBase,
218
243
  currentApiBase,
@@ -232,6 +257,7 @@ export function ProviderForm({ providerName }: ProviderFormProps) {
232
257
  setWireApi((providerSpec?.defaultWireApi || 'auto') as WireApiType);
233
258
  setModels(defaultModels);
234
259
  setModelDraft('');
260
+ setProviderDisplayName(defaultDisplayName);
235
261
  };
236
262
 
237
263
  const handleAddModel = () => {
@@ -258,6 +284,11 @@ export function ProviderForm({ providerName }: ProviderFormProps) {
258
284
  const trimmedApiKey = apiKey.trim();
259
285
  const trimmedApiBase = apiBase.trim();
260
286
  const normalizedHeaders = normalizeHeaders(extraHeaders);
287
+ const trimmedDisplayName = providerDisplayName.trim();
288
+
289
+ if (isCustomProvider && trimmedDisplayName !== effectiveDisplayName) {
290
+ payload.displayName = trimmedDisplayName.length > 0 ? trimmedDisplayName : null;
291
+ }
261
292
 
262
293
  if (trimmedApiKey.length > 0) {
263
294
  payload.apiKey = trimmedApiKey;
@@ -286,10 +317,12 @@ export function ProviderForm({ providerName }: ProviderFormProps) {
286
317
  return;
287
318
  }
288
319
 
320
+ const preferredModel = models.find((modelName) => modelName.trim().length > 0) ?? '';
321
+ const testModel = toProviderLocalModelId(preferredModel, providerModelAliases);
289
322
  const payload: ProviderConnectionTestRequest = {
290
323
  apiBase: apiBase.trim(),
291
324
  extraHeaders: normalizeHeaders(extraHeaders),
292
- model: config?.agents.defaults.model ?? null
325
+ model: testModel || null
293
326
  };
294
327
  if (apiKey.trim().length > 0) {
295
328
  payload.apiKey = apiKey.trim();
@@ -318,6 +351,22 @@ export function ProviderForm({ providerName }: ProviderFormProps) {
318
351
  }
319
352
  };
320
353
 
354
+ const handleDeleteProvider = async () => {
355
+ if (!providerName || !isCustomProvider) {
356
+ return;
357
+ }
358
+ const confirmed = window.confirm(t('providerDeleteConfirm'));
359
+ if (!confirmed) {
360
+ return;
361
+ }
362
+ try {
363
+ await deleteProvider.mutateAsync({ provider: providerName });
364
+ onProviderDeleted?.(providerName);
365
+ } catch {
366
+ // toast handled by mutation hook
367
+ }
368
+ };
369
+
321
370
  if (!providerName || !providerSpec || !providerConfig) {
322
371
  return (
323
372
  <div className={CONFIG_EMPTY_DETAIL_CARD_CLASS}>
@@ -345,6 +394,24 @@ export function ProviderForm({ providerName }: ProviderFormProps) {
345
394
 
346
395
  <form onSubmit={handleSubmit} className="flex min-h-0 flex-1 flex-col">
347
396
  <div className="min-h-0 flex-1 space-y-6 overflow-y-auto px-6 py-5">
397
+ {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" />
401
+ {t('providerDisplayName')}
402
+ </Label>
403
+ <Input
404
+ id="providerDisplayName"
405
+ type="text"
406
+ value={providerDisplayName}
407
+ onChange={(e) => setProviderDisplayName(e.target.value)}
408
+ placeholder={defaultDisplayName || t('providerDisplayNamePlaceholder')}
409
+ className="rounded-xl"
410
+ />
411
+ <p className="text-xs text-gray-500">{t('providerDisplayNameHelp')}</p>
412
+ </div>
413
+ )}
414
+
348
415
  <div className="space-y-2.5">
349
416
  <Label htmlFor="apiKey" className="flex items-center gap-2 text-sm font-medium text-gray-900">
350
417
  <KeyRound className="h-3.5 w-3.5 text-gray-500" />
@@ -374,7 +441,8 @@ export function ProviderForm({ providerName }: ProviderFormProps) {
374
441
  placeholder={defaultApiBase || apiBaseHint?.placeholder || 'https://api.example.com'}
375
442
  className="rounded-xl"
376
443
  />
377
- <p className="text-xs text-gray-500">{apiBaseHint?.help || t('providerApiBaseHelp')}</p>
444
+ <p className="text-xs text-gray-500">{apiBaseHelpText}</p>
445
+ {isCustomProvider && <p className="text-xs text-gray-500">{t('providerOpenAICompatHint')}</p>}
378
446
  </div>
379
447
 
380
448
  {providerSpec.supportsWireApi && (
@@ -468,6 +536,17 @@ export function ProviderForm({ providerName }: ProviderFormProps) {
468
536
  <RotateCcw className="mr-2 h-4 w-4" />
469
537
  {t('resetToDefault')}
470
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>
549
+ )}
471
550
  <Button type="button" variant="outline" onClick={handleTestConnection} disabled={testProviderConnection.isPending}>
472
551
  <CircleDotDashed className="mr-2 h-4 w-4" />
473
552
  {testProviderConnection.isPending ? t('providerTestingConnection') : t('providerTestConnection')}
@@ -1,6 +1,6 @@
1
1
  import { useEffect, useMemo, useState } from 'react';
2
- import { useConfig, useConfigMeta, useConfigSchema } from '@/hooks/useConfig';
3
- import { Search, KeyRound } from 'lucide-react';
2
+ import { useConfig, useConfigMeta, useConfigSchema, useCreateProvider } from '@/hooks/useConfig';
3
+ import { Search, KeyRound, Plus } from 'lucide-react';
4
4
  import { ProviderForm } from './ProviderForm';
5
5
  import { cn } from '@/lib/utils';
6
6
  import { Tabs } from '@/components/ui/tabs-custom';
@@ -11,6 +11,7 @@ import { StatusDot } from '@/components/ui/status-dot';
11
11
  import { t } from '@/lib/i18n';
12
12
  import { PageLayout, PageHeader } from '@/components/layout/page-layout';
13
13
  import { Input } from '@/components/ui/input';
14
+ import { Button } from '@/components/ui/button';
14
15
  import { CONFIG_SIDEBAR_CARD_CLASS, CONFIG_SPLIT_GRID_CLASS } from './config-layout';
15
16
 
16
17
  function formatBasePreview(base?: string | null): string | null {
@@ -30,6 +31,7 @@ export function ProvidersList() {
30
31
  const { data: config } = useConfig();
31
32
  const { data: meta } = useConfigMeta();
32
33
  const { data: schema } = useConfigSchema();
34
+ const createProvider = useCreateProvider();
33
35
 
34
36
  const [activeTab, setActiveTab] = useState('installed');
35
37
  const [selectedProvider, setSelectedProvider] = useState<string | undefined>();
@@ -60,7 +62,8 @@ export function ProvidersList() {
60
62
  if (!keyword) {
61
63
  return true;
62
64
  }
63
- const display = (provider.displayName || provider.name).toLowerCase();
65
+ const configDisplayName = baseConfig[provider.name]?.displayName?.trim();
66
+ const display = (configDisplayName || provider.displayName || provider.name).toLowerCase();
64
67
  return display.includes(keyword) || provider.name.toLowerCase().includes(keyword);
65
68
  });
66
69
  }, [meta, config, activeTab, query]);
@@ -79,6 +82,17 @@ export function ProvidersList() {
79
82
 
80
83
  const selectedName = selectedProvider;
81
84
 
85
+ const handleCreateCustomProvider = async () => {
86
+ try {
87
+ const result = await createProvider.mutateAsync({ data: {} });
88
+ setActiveTab('all');
89
+ setQuery('');
90
+ setSelectedProvider(result.name);
91
+ } catch {
92
+ // toast handled in hook
93
+ }
94
+ };
95
+
82
96
  if (!config || !meta) {
83
97
  return <div className="p-8">{t('providersLoading')}</div>;
84
98
  }
@@ -89,8 +103,18 @@ export function ProvidersList() {
89
103
 
90
104
  <div className={CONFIG_SPLIT_GRID_CLASS}>
91
105
  <section className={CONFIG_SIDEBAR_CARD_CLASS}>
92
- <div className="border-b border-gray-100 px-4 pt-4">
106
+ <div className="border-b border-gray-100 px-4 pt-4 pb-3 space-y-3">
93
107
  <Tabs tabs={tabs} activeTab={activeTab} onChange={setActiveTab} className="mb-0" />
108
+ <Button
109
+ type="button"
110
+ variant="outline"
111
+ className="w-full justify-center"
112
+ onClick={handleCreateCustomProvider}
113
+ disabled={createProvider.isPending}
114
+ >
115
+ <Plus className="mr-2 h-4 w-4" />
116
+ {createProvider.isPending ? t('saving') : t('providerAddCustom')}
117
+ </Button>
94
118
  </div>
95
119
 
96
120
  <div className="border-b border-gray-100 px-4 py-3">
@@ -110,6 +134,7 @@ export function ProvidersList() {
110
134
  const providerConfig = config.providers[provider.name];
111
135
  const isReady = Boolean(providerConfig?.apiKeySet);
112
136
  const isActive = selectedName === provider.name;
137
+ const providerLabel = providerConfig?.displayName?.trim() || provider.displayName || provider.name;
113
138
  const providerHint = hintForPath(`providers.${provider.name}`, uiHints);
114
139
  const resolvedBase = providerConfig?.apiBase || provider.defaultApiBase || '';
115
140
  const basePreview = formatBasePreview(resolvedBase);
@@ -140,7 +165,7 @@ export function ProvidersList() {
140
165
  fallback={<span className="text-sm font-semibold uppercase text-gray-500">{provider.name[0]}</span>}
141
166
  />
142
167
  <div className="min-w-0">
143
- <p className="truncate text-sm font-semibold text-gray-900">{provider.displayName || provider.name}</p>
168
+ <p className="truncate text-sm font-semibold text-gray-900">{providerLabel}</p>
144
169
  <p className="line-clamp-1 text-[11px] text-gray-500">{description}</p>
145
170
  </div>
146
171
  </div>
@@ -165,7 +190,14 @@ export function ProvidersList() {
165
190
  </div>
166
191
  </section>
167
192
 
168
- <ProviderForm providerName={selectedName} />
193
+ <ProviderForm
194
+ providerName={selectedName}
195
+ onProviderDeleted={(deletedProvider) => {
196
+ if (deletedProvider === selectedProvider) {
197
+ setSelectedProvider(undefined);
198
+ }
199
+ }}
200
+ />
169
201
  </div>
170
202
  </PageLayout>
171
203
  );
@@ -4,6 +4,8 @@ import {
4
4
  fetchConfigMeta,
5
5
  fetchConfigSchema,
6
6
  updateModel,
7
+ createProvider,
8
+ deleteProvider,
7
9
  updateProvider,
8
10
  testProviderConnection,
9
11
  updateChannel,
@@ -71,6 +73,40 @@ export function useUpdateProvider() {
71
73
  updateProvider(provider, data as Parameters<typeof updateProvider>[1]),
72
74
  onSuccess: () => {
73
75
  queryClient.invalidateQueries({ queryKey: ['config'] });
76
+ queryClient.invalidateQueries({ queryKey: ['config-meta'] });
77
+ toast.success(t('configSaved'));
78
+ },
79
+ onError: (error: Error) => {
80
+ toast.error(t('configSaveFailed') + ': ' + error.message);
81
+ }
82
+ });
83
+ }
84
+
85
+ export function useCreateProvider() {
86
+ const queryClient = useQueryClient();
87
+
88
+ return useMutation({
89
+ mutationFn: ({ data }: { data?: unknown }) =>
90
+ createProvider((data ?? {}) as Parameters<typeof createProvider>[0]),
91
+ onSuccess: () => {
92
+ queryClient.invalidateQueries({ queryKey: ['config'] });
93
+ queryClient.invalidateQueries({ queryKey: ['config-meta'] });
94
+ toast.success(t('configSaved'));
95
+ },
96
+ onError: (error: Error) => {
97
+ toast.error(t('configSaveFailed') + ': ' + error.message);
98
+ }
99
+ });
100
+ }
101
+
102
+ export function useDeleteProvider() {
103
+ const queryClient = useQueryClient();
104
+
105
+ return useMutation({
106
+ mutationFn: ({ provider }: { provider: string }) => deleteProvider(provider),
107
+ onSuccess: () => {
108
+ queryClient.invalidateQueries({ queryKey: ['config'] });
109
+ queryClient.invalidateQueries({ queryKey: ['config-meta'] });
74
110
  toast.success(t('configSaved'));
75
111
  },
76
112
  onError: (error: Error) => {
package/src/lib/i18n.ts CHANGED
@@ -184,6 +184,9 @@ export const LABELS: Record<string, { zh: string; en: string }> = {
184
184
  providersTabAll: { zh: '全部提供商', en: 'All Providers' },
185
185
  providersFilterPlaceholder: { zh: '搜索提供商', en: 'Search providers' },
186
186
  providersNoMatch: { zh: '没有匹配的提供商', en: 'No matching providers' },
187
+ providerAddCustom: { zh: '新增自定义提供商', en: 'Add Custom Provider' },
188
+ providerDelete: { zh: '删除该提供商', en: 'Delete Provider' },
189
+ providerDeleteConfirm: { zh: '确认删除这个自定义提供商吗?删除后不可恢复。', en: 'Delete this custom provider? This action cannot be undone.' },
187
190
  providersSelectPlaceholder: { zh: '选择提供商', en: 'Select Provider' },
188
191
  providersSelectTitle: { zh: '选择左侧提供商开始配置', en: 'Select a provider from the left to configure' },
189
192
  providersSelectDescription: { zh: '你可以连续切换多个提供商并逐个保存配置。', en: 'Switch between providers continuously and save each configuration.' },
@@ -202,8 +205,22 @@ export const LABELS: Record<string, { zh: string; en: string }> = {
202
205
  showKey: { zh: '显示密钥', en: 'Show Key' },
203
206
  hideKey: { zh: '隐藏密钥', en: 'Hide Key' },
204
207
  providerFormDescription: { zh: '配置 AI 提供商的 API 密钥与参数', en: 'Configure API keys and parameters for AI provider' },
208
+ providerDisplayName: { zh: '自定义名称', en: 'Custom Name' },
209
+ providerDisplayNamePlaceholder: { zh: '例如:中转站 A', en: 'For example: Relay A' },
210
+ providerDisplayNameHelp: {
211
+ zh: '仅用于界面展示,便于区分多个自定义 Provider。',
212
+ en: 'Display-only label to distinguish multiple custom providers.'
213
+ },
205
214
  enterApiKey: { zh: '请输入 API 密钥', en: 'Enter API Key' },
206
215
  providerApiBaseHelp: { zh: '留空或恢复默认即可使用预置 API Base。', en: 'Leave empty or reset to use the default API base.' },
216
+ providerApiBaseHelpMinimax: {
217
+ zh: 'MiniMax 中国区请使用 https://api.minimaxi.com/v1;海外请使用 https://api.minimax.io/v1。',
218
+ en: 'Use https://api.minimaxi.com/v1 for Mainland China accounts, and https://api.minimax.io/v1 for overseas accounts.'
219
+ },
220
+ providerOpenAICompatHint: {
221
+ zh: '自定义提供商默认按 OpenAI 兼容 API 格式接入(Chat Completions / Responses)。',
222
+ en: 'Custom providers use OpenAI-compatible API format by default (Chat Completions / Responses).'
223
+ },
207
224
  providerExtraHeadersHelp: { zh: '用于自定义请求头(可选)。', en: 'Optional custom request headers.' },
208
225
  providerTestConnection: { zh: '测试连接', en: 'Test Connection' },
209
226
  providerTestingConnection: { zh: '测试中...', en: 'Testing...' },
@@ -1 +0,0 @@
1
- import{j as e,r as c,F as Q,g as V,m as X,s as Z,J as H,O as ee,U as se}from"./vendor-DN_iJQc4.js";import{P as te,a as ae,B as re}from"./page-layout-D8MW2vP-.js";import{C as _}from"./card-Bq6uwDJQ.js";import{I as q,u as ne,a as le,b as oe,c as ie}from"./useConfig-tR_KAfMV.js";import{L as $}from"./label-Cq1vSfWg.js";import{c as B,t as d,S as ce,a as de,b as me,d as xe,e as ue,D as he,f as fe}from"./index-wB2uPrKu.js";import{h as A}from"./config-hints-CApS3K_7.js";function u({className:a,...n}){return e.jsx("div",{className:B("animate-pulse rounded-md bg-slate-200",a),...n})}function pe(a){const n=new Set;for(const r of a){const l=r.trim();l.length>0&&n.add(l)}return[...n]}function ge({id:a,value:n,onChange:r,options:l,placeholder:m,className:j,inputClassName:x,emptyText:C,createText:b,maxItems:S=Number.POSITIVE_INFINITY,onEnter:k}){const[v,h]=c.useState(!1),N=c.useMemo(()=>pe(l),[l]),i=n.trim().toLowerCase(),f=c.useMemo(()=>{const t=N.map((o,w)=>({option:o,index:w}));i.length>0&&t.sort((o,w)=>{const y=o.option.toLowerCase(),M=w.option.toLowerCase(),I=y===i?0:y.startsWith(i)?1:y.includes(i)?2:3,L=M===i?0:M.startsWith(i)?1:M.includes(i)?2:3;return I!==L?I-L:o.index-w.index});const g=t.map(o=>o.option);return Number.isFinite(S)?g.slice(0,Math.max(1,S)):g},[N,i,S]),P=n.trim().length>0&&N.some(t=>t===n.trim());return e.jsxs("div",{className:B("relative",j),onBlur:()=>{setTimeout(()=>h(!1),120)},children:[e.jsx(q,{id:a,value:n,onFocus:()=>h(!0),onChange:t=>{r(t.target.value),v||h(!0)},onKeyDown:t=>{t.key==="Enter"&&(k&&(t.preventDefault(),k()),h(!1))},placeholder:m,className:B("pr-10",x)}),e.jsx("button",{type:"button",onMouseDown:t=>t.preventDefault(),onClick:()=>h(t=>!t),className:"absolute inset-y-0 right-0 inline-flex w-10 items-center justify-center text-gray-400 hover:text-gray-600","aria-label":"toggle model options",children:e.jsx(Q,{className:"h-4 w-4"})}),v&&e.jsx("div",{className:"absolute z-20 mt-1 w-full overflow-hidden rounded-xl border border-gray-200 bg-white shadow-lg",children:e.jsxs("div",{className:"max-h-60 overflow-y-auto py-1",children:[!P&&n.trim().length>0&&e.jsxs("button",{type:"button",onMouseDown:t=>t.preventDefault(),onClick:()=>{r(n.trim()),h(!1)},className:"flex w-full items-center gap-2 px-3 py-2 text-left text-sm hover:bg-gray-50",children:[e.jsx(V,{className:"h-4 w-4 text-transparent"}),e.jsx("span",{className:"truncate text-gray-700",children:b?b.replace("{value}",n.trim()):n.trim()})]}),f.map(t=>e.jsxs("button",{type:"button",onMouseDown:g=>g.preventDefault(),onClick:()=>{r(t),h(!1)},className:"flex w-full items-center gap-2 px-3 py-2 text-left text-sm hover:bg-gray-50",children:[e.jsx(V,{className:B("h-4 w-4",t===n.trim()?"text-primary":"text-transparent")}),e.jsx("span",{className:"truncate text-gray-700",children:t})]},t)),f.length===0&&n.trim().length===0&&e.jsx("div",{className:"px-3 py-2 text-sm text-gray-500",children:C??"No models available"})]})})]})}function z(a){if(!a||a.length===0)return[];const n=new Set;for(const r of a){const l=r.trim();l&&n.add(l)}return[...n]}function je(a,n){const r=a.trim(),l=n.trim();if(!r||!l)return r;const m=`${l}/`;return r.startsWith(m)?r.slice(m.length):r}function T(a,n){let r=a.trim();if(!r)return"";for(const l of n)r=je(r,l);return r.trim()}function Ne(a,n){const r=a.trim();if(!r)return null;let l=null;for(const m of n)for(const j of m.aliases){const x=j.trim();x&&(r===x||r.startsWith(`${x}/`))&&(!l||x.length>l.score)&&(l={name:m.name,score:x.length})}return(l==null?void 0:l.name)??null}function ke(){const{data:a,isLoading:n}=ne(),{data:r}=le(),{data:l}=oe(),m=ie(),[j,x]=c.useState(""),[C,b]=c.useState(""),[S,k]=c.useState(""),[v,h]=c.useState(8192),N=l==null?void 0:l.uiHints,i=A("agents.defaults.model",N),f=A("agents.defaults.workspace",N),P=A("agents.defaults.maxTokens",N),t=c.useMemo(()=>((r==null?void 0:r.providers)??[]).map(s=>{var D,R;const O=(s.modelPrefix||s.name||"").trim(),p=z([s.modelPrefix||"",s.name||""]),U=z((s.defaultModels??[]).map(W=>T(W,p))),F=z((((R=(D=a==null?void 0:a.providers)==null?void 0:D[s.name])==null?void 0:R.models)??[]).map(W=>T(W,p))),E=z([...U,...F]);return{name:s.name,displayName:s.displayName||s.name,prefix:O,aliases:p,models:E}}),[r,a]),g=c.useMemo(()=>new Map(t.map(s=>[s.name,s])),[t]),o=g.get(j)??t[0],w=(o==null?void 0:o.name)??"",y=c.useMemo(()=>(o==null?void 0:o.aliases)??[],[o]),M=c.useMemo(()=>(o==null?void 0:o.models)??[],[o]);c.useEffect(()=>{j||t.length===0||x(t[0].name)},[j,t]),c.useEffect(()=>{var F,E,D;if(!((F=a==null?void 0:a.agents)!=null&&F.defaults))return;const s=(a.agents.defaults.model||"").trim(),p=Ne(s,t)??((E=t[0])==null?void 0:E.name)??"",U=((D=g.get(p))==null?void 0:D.aliases)??[];x(p),b(T(s,U)),k(a.agents.defaults.workspace||""),h(a.agents.defaults.maxTokens||8192)},[a,t,g]);const I=c.useMemo(()=>{const s=new Set;for(const O of M){const p=O.trim();p&&s.add(p)}return[...s]},[M]),L=c.useMemo(()=>{const s=T(C,y);return s?!o||!o.prefix?s:`${o.prefix}/${s}`:""},[C,o,y]),G=(i==null?void 0:i.help)??"",J=s=>{x(s),b("")},K=s=>{b(T(s,y))},Y=s=>{s.preventDefault(),m.mutate({model:L,maxTokens:v})};return n?e.jsxs("div",{className:"max-w-2xl space-y-6",children:[e.jsxs("div",{className:"space-y-2",children:[e.jsx(u,{className:"h-8 w-32"}),e.jsx(u,{className:"h-4 w-48"})]}),e.jsxs(_,{className:"rounded-2xl border-gray-200 p-6",children:[e.jsxs("div",{className:"flex items-center gap-4 mb-6",children:[e.jsx(u,{className:"h-12 w-12 rounded-xl"}),e.jsxs("div",{className:"space-y-2",children:[e.jsx(u,{className:"h-5 w-24"}),e.jsx(u,{className:"h-3 w-32"})]})]}),e.jsx(u,{className:"h-4 w-20 mb-2"}),e.jsx(u,{className:"h-10 w-full rounded-xl"})]}),e.jsxs(_,{className:"rounded-2xl border-gray-200 p-6",children:[e.jsx(u,{className:"h-5 w-24 mb-2"}),e.jsx(u,{className:"h-3 w-40 mb-6"}),e.jsx("div",{className:"space-y-6",children:e.jsxs("div",{children:[e.jsx(u,{className:"h-4 w-28 mb-3"}),e.jsx(u,{className:"h-2 w-full rounded-full"})]})})]})]}):e.jsxs(te,{children:[e.jsx(ae,{title:d("modelPageTitle"),description:d("modelPageDescription")}),e.jsxs("form",{onSubmit:Y,className:"space-y-8",children:[e.jsxs("div",{className:"grid grid-cols-1 md:grid-cols-2 gap-8",children:[e.jsxs("div",{className:"p-8 rounded-2xl bg-white border border-gray-200 shadow-card",children:[e.jsxs("div",{className:"flex items-center gap-4 mb-8",children:[e.jsx("div",{className:"h-10 w-10 rounded-xl bg-primary flex items-center justify-center text-white",children:e.jsx(X,{className:"h-5 w-5"})}),e.jsx("h3",{className:"text-lg font-bold text-gray-900",children:d("defaultModel")})]}),e.jsxs("div",{className:"space-y-2",children:[e.jsx($,{htmlFor:"model",className:"text-xs font-semibold text-gray-500 uppercase tracking-wider",children:(i==null?void 0:i.label)??"Model Name"}),e.jsxs("div",{className:"flex flex-col gap-2 sm:flex-row sm:items-center",children:[e.jsx("div",{className:"sm:w-[38%] sm:min-w-[170px]",children:e.jsxs(ce,{value:w,onValueChange:J,children:[e.jsx(de,{className:"h-10 w-full rounded-xl",children:e.jsx(me,{placeholder:d("providersSelectPlaceholder")})}),e.jsx(xe,{children:t.map(s=>e.jsx(ue,{value:s.name,children:s.displayName},s.name))})]})}),e.jsx("span",{className:"hidden h-10 items-center justify-center leading-none text-lg font-semibold text-gray-300 sm:inline-flex",children:"/"}),e.jsx(ge,{id:"model",value:C,onChange:K,options:I,placeholder:(i==null?void 0:i.placeholder)??"gpt-5.1",className:"sm:flex-1",inputClassName:"h-10 rounded-xl",emptyText:d("modelPickerNoOptions"),createText:d("modelPickerUseCustom")},w)]}),e.jsx("p",{className:"text-xs text-gray-400",children:G}),e.jsx("p",{className:"text-xs text-gray-500",children:d("modelInputCustomHint")}),e.jsxs("a",{href:`${he}/guide/model-selection`,className:"inline-flex items-center gap-1.5 text-xs text-primary hover:text-primary-hover transition-colors",children:[e.jsx(Z,{className:"h-3.5 w-3.5"}),d("channelsGuideTitle")]})]})]}),e.jsxs("div",{className:"p-8 rounded-2xl bg-white border border-gray-200 shadow-card",children:[e.jsxs("div",{className:"flex items-center gap-4 mb-8",children:[e.jsx("div",{className:"h-10 w-10 rounded-xl bg-primary flex items-center justify-center text-white",children:e.jsx(H,{className:"h-5 w-5"})}),e.jsx("h3",{className:"text-lg font-bold text-gray-900",children:d("workspace")})]}),e.jsxs("div",{className:"space-y-2",children:[e.jsx($,{htmlFor:"workspace",className:"text-xs font-semibold text-gray-500 uppercase tracking-wider",children:(f==null?void 0:f.label)??"Default Path"}),e.jsx(q,{id:"workspace",value:S,onChange:s=>k(s.target.value),placeholder:(f==null?void 0:f.placeholder)??"/path/to/workspace",className:"rounded-xl"})]})]})]}),e.jsxs("div",{className:"p-8 rounded-2xl bg-white border border-gray-200 shadow-card",children:[e.jsxs("div",{className:"flex items-center gap-4 mb-10",children:[e.jsx("div",{className:"h-10 w-10 rounded-xl bg-primary flex items-center justify-center text-white",children:e.jsx(ee,{className:"h-5 w-5"})}),e.jsx("h3",{className:"text-lg font-bold text-gray-900",children:d("generationParameters")})]}),e.jsx("div",{className:"grid grid-cols-1 gap-12",children:e.jsxs("div",{className:"space-y-4",children:[e.jsxs("div",{className:"flex justify-between items-center mb-2",children:[e.jsx($,{className:"text-xs font-semibold text-gray-500 uppercase tracking-wider",children:(P==null?void 0:P.label)??d("maxTokens")}),e.jsx("span",{className:"text-sm font-semibold text-gray-900",children:fe(v)})]}),e.jsx("input",{type:"range",min:"1000",max:"32000",step:"1000",value:v,onChange:s=>h(parseInt(s.target.value)),className:"w-full h-1 bg-gray-200 rounded-full appearance-none cursor-pointer accent-primary"})]})})]}),e.jsx("div",{className:"flex justify-end pt-4",children:e.jsx(re,{type:"submit",disabled:m.isPending,size:"lg",children:m.isPending?e.jsx(se,{className:"h-5 w-5 animate-spin"}):d("saveChanges")})})]})]})}export{ke as ModelConfig};
@@ -1 +0,0 @@
1
- import{r as p,j as e,aa as xe,ab as he,a8 as pe,a6 as te,K as ae,a1 as fe,ac as ee,m as ye,ad as ge,ae as je,af as be,ag as V,a0 as we}from"./vendor-DN_iJQc4.js";import{I as D,u as re,a as ne,b as le,g as Ne,h as ve}from"./useConfig-tR_KAfMV.js";import{B as L,P as Ae,a as Ce}from"./page-layout-D8MW2vP-.js";import{L as F}from"./label-Cq1vSfWg.js";import{t as a,c as G,S as Pe,a as Se,b as Ee,d as Le,e as Me}from"./index-wB2uPrKu.js";import{C as Te,a as Ke,S as ie,b as De,c as Be,L as $e,g as ke}from"./logos-BKBMs40Q.js";import{h as _}from"./config-hints-CApS3K_7.js";import{T as Ie}from"./tabs-custom-N4olWJSw.js";function Fe({maskedValue:s,isSet:t,className:n,...i}){const[x,g]=p.useState(!1);return e.jsxs("div",{className:"relative",children:[e.jsx(D,{type:x?"text":"password",className:G("pr-20",n),placeholder:t?`${a("apiKeySet")} (${a("unchanged")})`:"",...i}),e.jsx("div",{className:"absolute right-2 top-1/2 -translate-y-1/2 flex gap-1",children:(t||s)&&e.jsx(L,{type:"button",variant:"ghost",size:"icon",className:"h-7 w-7",onClick:()=>g(!x),children:x?e.jsx(xe,{className:"h-4 w-4"}):e.jsx(he,{className:"h-4 w-4"})})})]})}function _e({value:s,onChange:t,className:n}){const i=s?Object.entries(s):[],x=(m,f,j)=>{const y=[...i];y[m]=[f,j],t(Object.fromEntries(y))},g=()=>{t({...s,"":""})},h=m=>{const f=i.filter((j,y)=>y!==m);t(Object.fromEntries(f))};return e.jsxs("div",{className:G("space-y-2",n),children:[i.map(([m,f],j)=>e.jsxs("div",{className:"flex gap-2",children:[e.jsx(D,{type:"text",value:m,onChange:y=>x(j,y.target.value,f),placeholder:a("headerName"),className:"flex-1"}),e.jsx(D,{type:"text",value:f,onChange:y=>x(j,m,y.target.value),placeholder:a("headerValue"),className:"flex-1"}),e.jsx(L,{type:"button",variant:"ghost",size:"icon",onClick:()=>h(j),children:e.jsx(pe,{className:"h-4 w-4 text-red-500"})})]},j)),e.jsxs(L,{type:"button",variant:"outline",size:"sm",onClick:g,children:[e.jsx(te,{className:"h-4 w-4 mr-2"}),a("add")]})]})}function O(s){if(!s)return null;const t=Object.entries(s).map(([n,i])=>[n.trim(),i]).filter(([n])=>n.length>0);return t.length===0?null:Object.fromEntries(t)}function se(s,t){const n=O(s),i=O(t);if(n===null&&i===null)return!0;if(!n||!i)return!1;const x=Object.entries(n).sort(([h],[m])=>h.localeCompare(m)),g=Object.entries(i).sort(([h],[m])=>h.localeCompare(m));return x.length!==g.length?!1:x.every(([h,m],f)=>h===g[f][0]&&m===g[f][1])}function q(s){if(!s||s.length===0)return[];const t=new Set;for(const n of s){const i=n.trim();i&&t.add(i)}return[...t]}function Oe(s,t){const n=s.trim();if(!n||!t.trim())return n;const i=`${t.trim()}/`;return n.startsWith(i)?n.slice(i.length):n}function U(s,t){let n=s.trim();if(!n)return"";for(const i of t){const x=i.trim();x&&(n=Oe(n,x))}return n.trim()}function Q(s,t){return s.length!==t.length?!1:s.every((n,i)=>n===t[i])}function Re(s,t){const n=[...s];for(const i of t)n.includes(i)||n.push(i);return n}function We(s,t){return t.length===0?s:t.every(i=>!s.includes(i))?Re(s,t):t}function ze(s,t){return Q(s,t)?[]:s}function Ge({providerName:s}){const{data:t}=re(),{data:n}=ne(),{data:i}=le(),x=Ne(),g=ve(),[h,m]=p.useState(""),[f,j]=p.useState(""),[y,M]=p.useState(null),[A,T]=p.useState("auto"),[b,C]=p.useState([]),[d,u]=p.useState(""),r=n==null?void 0:n.providers.find(l=>l.name===s),c=s?t==null?void 0:t.providers[s]:null,w=i==null?void 0:i.uiHints,P=s?_(`providers.${s}.apiKey`,w):void 0,v=s?_(`providers.${s}.apiBase`,w):void 0,S=s?_(`providers.${s}.extraHeaders`,w):void 0,E=s?_(`providers.${s}.wireApi`,w):void 0,ce=(r==null?void 0:r.displayName)||s||a("providersSelectPlaceholder"),X=(r==null?void 0:r.modelPrefix)||s||"",B=p.useMemo(()=>q([X,s||""]),[X,s]),R=(r==null?void 0:r.defaultApiBase)||"",$=(c==null?void 0:c.apiBase)||R,H=O((c==null?void 0:c.extraHeaders)||null),k=(c==null?void 0:c.wireApi)||(r==null?void 0:r.defaultWireApi)||"auto",W=p.useMemo(()=>q(((r==null?void 0:r.defaultModels)??[]).map(l=>U(l,B))),[r==null?void 0:r.defaultModels,B]),Y=p.useMemo(()=>q(((c==null?void 0:c.models)??[]).map(l=>U(l,B))),[c==null?void 0:c.models,B]),I=p.useMemo(()=>We(W,Y),[W,Y]);p.useEffect(()=>{if(!s){m(""),j(""),M(null),T("auto"),C([]),u("");return}m(""),j($),M((c==null?void 0:c.extraHeaders)||null),T(k),C(I),u("")},[s,$,c==null?void 0:c.extraHeaders,k,I]);const J=p.useMemo(()=>{if(!s)return!1;const l=h.trim().length>0,o=f.trim()!==$.trim(),N=!se(y,H),K=r!=null&&r.supportsWireApi?A!==k:!1,z=!Q(b,I);return l||o||N||K||z},[s,h,f,$,y,H,r==null?void 0:r.supportsWireApi,A,k,b,I]),oe=()=>{m(""),j(R),M(null),T((r==null?void 0:r.defaultWireApi)||"auto"),C(W),u("")},Z=()=>{const l=U(d,B);if(l){if(b.includes(l)){u("");return}C(o=>[...o,l]),u("")}},de=l=>{if(l.preventDefault(),!s)return;const o={},N=h.trim(),K=f.trim(),z=O(y);N.length>0&&(o.apiKey=N),K!==$.trim()&&(o.apiBase=K.length>0&&K!==R?K:null),se(z,H)||(o.extraHeaders=z),r!=null&&r.supportsWireApi&&A!==k&&(o.wireApi=A),Q(b,I)||(o.models=ze(b,W)),x.mutate({provider:s,data:o})},me=async()=>{if(!s)return;const l={apiBase:f.trim(),extraHeaders:O(y),model:(t==null?void 0:t.agents.defaults.model)??null};h.trim().length>0&&(l.apiKey=h.trim()),r!=null&&r.supportsWireApi&&(l.wireApi=A);try{const o=await g.mutateAsync({provider:s,data:l});if(o.success){V.success(`${a("providerTestConnectionSuccess")} (${o.latencyMs}ms)`);return}const N=[`provider=${o.provider}`,`latency=${o.latencyMs}ms`];o.model&&N.push(`model=${o.model}`),V.error(`${a("providerTestConnectionFailed")}: ${o.message} | ${N.join(" | ")}`)}catch(o){const N=o instanceof Error?o.message:String(o);V.error(`${a("providerTestConnectionFailed")}: ${N}`)}};if(!s||!r||!c)return e.jsx("div",{className:Te,children:e.jsxs("div",{children:[e.jsx("h3",{className:"text-base font-semibold text-gray-900",children:a("providersSelectTitle")}),e.jsx("p",{className:"mt-2 text-sm text-gray-500",children:a("providersSelectDescription")})]})});const ue=c.apiKeySet?a("statusReady"):a("statusSetup");return e.jsxs("div",{className:Ke,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:ce}),e.jsx("p",{className:"mt-1 text-sm text-gray-500",children:a("providerFormDescription")})]}),e.jsx(ie,{status:c.apiKeySet?"ready":"setup",label:ue})]})}),e.jsxs("form",{onSubmit:de,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:[e.jsxs("div",{className:"space-y-2.5",children:[e.jsxs(F,{htmlFor:"apiKey",className:"flex items-center gap-2 text-sm font-medium text-gray-900",children:[e.jsx(ae,{className:"h-3.5 w-3.5 text-gray-500"}),(P==null?void 0:P.label)??a("apiKey")]}),e.jsx(Fe,{id:"apiKey",value:h,isSet:c.apiKeySet,onChange:l=>m(l.target.value),placeholder:c.apiKeySet?a("apiKeySet"):(P==null?void 0:P.placeholder)??a("enterApiKey"),className:"rounded-xl"}),e.jsx("p",{className:"text-xs text-gray-500",children:a("leaveBlankToKeepUnchanged")})]}),e.jsxs("div",{className:"space-y-2.5",children:[e.jsxs(F,{htmlFor:"apiBase",className:"flex items-center gap-2 text-sm font-medium text-gray-900",children:[e.jsx(fe,{className:"h-3.5 w-3.5 text-gray-500"}),(v==null?void 0:v.label)??a("apiBase")]}),e.jsx(D,{id:"apiBase",type:"text",value:f,onChange:l=>j(l.target.value),placeholder:R||(v==null?void 0:v.placeholder)||"https://api.example.com",className:"rounded-xl"}),e.jsx("p",{className:"text-xs text-gray-500",children:(v==null?void 0:v.help)||a("providerApiBaseHelp")})]}),r.supportsWireApi&&e.jsxs("div",{className:"space-y-2.5",children:[e.jsxs(F,{htmlFor:"wireApi",className:"flex items-center gap-2 text-sm font-medium text-gray-900",children:[e.jsx(ee,{className:"h-3.5 w-3.5 text-gray-500"}),(E==null?void 0:E.label)??a("wireApi")]}),e.jsxs(Pe,{value:A,onValueChange:l=>T(l),children:[e.jsx(Se,{className:"rounded-xl",children:e.jsx(Ee,{})}),e.jsx(Le,{children:(r.wireApiOptions||["auto","chat","responses"]).map(l=>e.jsx(Me,{value:l,children:l==="chat"?a("wireApiChat"):l==="responses"?a("wireApiResponses"):a("wireApiAuto")},l))})]}),(E==null?void 0:E.help)&&e.jsx("p",{className:"text-xs text-gray-500",children:E.help})]}),e.jsxs("div",{className:"space-y-2.5",children:[e.jsxs(F,{className:"flex items-center gap-2 text-sm font-medium text-gray-900",children:[e.jsx(ee,{className:"h-3.5 w-3.5 text-gray-500"}),(S==null?void 0:S.label)??a("extraHeaders")]}),e.jsx(_e,{value:y,onChange:M}),e.jsx("p",{className:"text-xs text-gray-500",children:(S==null?void 0:S.help)||a("providerExtraHeadersHelp")})]}),e.jsxs("div",{className:"space-y-2.5",children:[e.jsxs(F,{className:"flex items-center gap-2 text-sm font-medium text-gray-900",children:[e.jsx(ye,{className:"h-3.5 w-3.5 text-gray-500"}),a("providerModelsTitle")]}),e.jsxs("div",{className:"flex items-center gap-2",children:[e.jsx(D,{value:d,onChange:l=>u(l.target.value),onKeyDown:l=>{l.key==="Enter"&&(l.preventDefault(),Z())},placeholder:a("providerModelInputPlaceholder"),className:"flex-1"}),e.jsxs(L,{type:"button",variant:"outline",onClick:Z,disabled:d.trim().length===0,children:[e.jsx(te,{className:"mr-1.5 h-4 w-4"}),a("providerAddModel")]})]}),e.jsx("p",{className:"text-xs text-gray-500",children:a("providerModelInputHint")}),b.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:a("providerModelsEmpty")}):e.jsx("div",{className:"flex flex-wrap gap-2",children:b.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:()=>C(o=>o.filter(N=>N!==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":a("remove"),children:e.jsx(ge,{className:"h-3 w-3"})})]},l))}),e.jsx("p",{className:"text-xs text-gray-500",children:a("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(L,{type:"button",variant:"outline",onClick:oe,children:[e.jsx(je,{className:"mr-2 h-4 w-4"}),a("resetToDefault")]}),e.jsxs(L,{type:"button",variant:"outline",onClick:me,disabled:g.isPending,children:[e.jsx(be,{className:"mr-2 h-4 w-4"}),g.isPending?a("providerTestingConnection"):a("providerTestConnection")]})]}),e.jsx(L,{type:"submit",disabled:x.isPending||!J,children:x.isPending?a("saving"):J?a("save"):a("unchanged")})]})]})]})}function He(s){if(!s)return null;try{const t=new URL(s),n=t.pathname&&t.pathname!=="/"?t.pathname:"";return`${t.host}${n}`}catch{return s.replace(/^https?:\/\//,"")}}function es(){const{data:s}=re(),{data:t}=ne(),{data:n}=le(),[i,x]=p.useState("installed"),[g,h]=p.useState(),[m,f]=p.useState(""),j=n==null?void 0:n.uiHints,y=(t==null?void 0:t.providers)??[],M=(s==null?void 0:s.providers)??{},A=y.filter(d=>{var u;return(u=M[d.name])==null?void 0:u.apiKeySet}).length,T=[{id:"installed",label:a("providersTabConfigured"),count:A},{id:"all",label:a("providersTabAll"),count:y.length}],b=p.useMemo(()=>{const d=(t==null?void 0:t.providers)??[],u=(s==null?void 0:s.providers)??{},r=m.trim().toLowerCase();return d.filter(c=>{var w;return i==="installed"?!!((w=u[c.name])!=null&&w.apiKeySet):!0}).filter(c=>r?(c.displayName||c.name).toLowerCase().includes(r)||c.name.toLowerCase().includes(r):!0)},[t,s,i,m]);p.useEffect(()=>{if(b.length===0){h(void 0);return}b.some(u=>u.name===g)||h(b[0].name)},[b,g]);const C=g;return!s||!t?e.jsx("div",{className:"p-8",children:a("providersLoading")}):e.jsxs(Ae,{children:[e.jsx(Ce,{title:a("providersPageTitle"),description:a("providersPageDescription")}),e.jsxs("div",{className:De,children:[e.jsxs("section",{className:Be,children:[e.jsx("div",{className:"border-b border-gray-100 px-4 pt-4",children:e.jsx(Ie,{tabs:T,activeTab:i,onChange:x,className:"mb-0"})}),e.jsx("div",{className:"border-b border-gray-100 px-4 py-3",children:e.jsxs("div",{className:"relative",children:[e.jsx(we,{className:"pointer-events-none absolute left-3 top-1/2 h-4 w-4 -translate-y-1/2 text-gray-400"}),e.jsx(D,{value:m,onChange:d=>f(d.target.value),placeholder:a("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:[b.map(d=>{const u=s.providers[d.name],r=!!(u!=null&&u.apiKeySet),c=C===d.name,w=_(`providers.${d.name}`,j),P=(u==null?void 0:u.apiBase)||d.defaultApiBase||"",S=He(P)||(w==null?void 0:w.help)||a("providersDefaultDescription");return e.jsx("button",{type:"button",onClick:()=>h(d.name),className:G("w-full rounded-xl border p-2.5 text-left transition-all",c?"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($e,{name:d.name,src:ke(d.name),className:G("h-10 w-10 rounded-lg border",r?"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:d.name[0]})}),e.jsxs("div",{className:"min-w-0",children:[e.jsx("p",{className:"truncate text-sm font-semibold text-gray-900",children:d.displayName||d.name}),e.jsx("p",{className:"line-clamp-1 text-[11px] text-gray-500",children:S})]})]}),e.jsx(ie,{status:r?"ready":"setup",label:r?a("statusReady"):a("statusSetup"),className:"min-w-[56px] justify-center"})]})},d.name)}),b.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(ae,{className:"h-5 w-5 text-gray-300"})}),e.jsx("p",{className:"text-sm font-medium text-gray-700",children:a("providersNoMatch")})]})]})]}),e.jsx(Ge,{providerName:C})]})]})}export{es as ProvidersList};