@nextclaw/ui 0.5.29 → 0.5.32
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +18 -0
- package/dist/assets/ChannelsList-CroolNE8.js +1 -0
- package/dist/assets/{ChatPage-DI2euxZy.js → ChatPage-CNrt5Gfh.js} +1 -1
- package/dist/assets/{CronConfig-DAlt-x5i.js → CronConfig-CqjAHF25.js} +1 -1
- package/dist/assets/{DocBrowser-TrMsdXgx.js → DocBrowser-JxtaErnQ.js} +1 -1
- package/dist/assets/{MarketplacePage-Dwm527F7.js → MarketplacePage-CwRpEHu7.js} +1 -1
- package/dist/assets/ModelConfig-0bbUHFSf.js +1 -0
- package/dist/assets/ProvidersList-BluNat2q.js +1 -0
- package/dist/assets/{RuntimeConfig-CLbdKAlo.js → RuntimeConfig-IfToQkFH.js} +1 -1
- package/dist/assets/{SecretsConfig-DXCdR0Be.js → SecretsConfig-BkizgSNf.js} +2 -2
- package/dist/assets/{SessionsConfig-iKpz3Sts.js → SessionsConfig-CjVmna9f.js} +1 -1
- package/dist/assets/{card-CVj65Dvi.js → card-FVpEr1qe.js} +1 -1
- package/dist/assets/{dialog-lK79rlAw.js → dialog-o8XweWHQ.js} +2 -2
- package/dist/assets/index-nZ3tdMe1.js +2 -0
- package/dist/assets/index-yypHrk9r.css +1 -0
- package/dist/assets/{label-l-fECYi3.js → label-PpMv4yho.js} +1 -1
- package/dist/assets/logos-B2FZM2a7.js +1 -0
- package/dist/assets/{page-layout-BghxFaNt.js → page-layout-DXAYLIUr.js} +1 -1
- package/dist/assets/{switch-B4yFzIbc.js → switch-CGhksmrM.js} +1 -1
- package/dist/assets/{tabs-custom-B4q02QSV.js → tabs-custom-4AOSVaDz.js} +1 -1
- package/dist/assets/useConfig-CPpZMJSX.js +6 -0
- package/dist/assets/{useConfirmDialog-C20D5SYn.js → useConfirmDialog-DGWSgJUB.js} +1 -1
- package/dist/assets/{vendor-H2M3a_4Z.js → vendor-C8fQ0Vej.js} +84 -74
- package/dist/index.html +3 -3
- package/package.json +1 -1
- package/src/api/config.ts +17 -0
- package/src/api/types.ts +12 -0
- package/src/components/config/ModelConfig.tsx +10 -2
- package/src/components/config/ProviderForm.tsx +238 -142
- package/src/components/config/ProvidersList.tsx +140 -80
- package/src/components/ui/status-dot.tsx +1 -1
- package/src/hooks/useConfig.ts +8 -0
- package/src/lib/i18n.ts +14 -1
- package/src/stores/ui.store.ts +0 -9
- package/dist/assets/ChannelsList-DEr4kE7H.js +0 -1
- package/dist/assets/ModelConfig-srggzgfA.js +0 -1
- package/dist/assets/ProvidersList-8kFCDiqC.js +0 -1
- package/dist/assets/action-link-w4jS8X9q.js +0 -1
- package/dist/assets/index-BXgULtdk.js +0 -2
- package/dist/assets/index-B_OeEGic.css +0 -1
- package/dist/assets/useConfig-C9k3TmQk.js +0 -6
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-
|
|
10
|
-
<link rel="modulepreload" crossorigin href="/assets/vendor-
|
|
11
|
-
<link rel="stylesheet" crossorigin href="/assets/index-
|
|
9
|
+
<script type="module" crossorigin src="/assets/index-nZ3tdMe1.js"></script>
|
|
10
|
+
<link rel="modulepreload" crossorigin href="/assets/vendor-C8fQ0Vej.js">
|
|
11
|
+
<link rel="stylesheet" crossorigin href="/assets/index-yypHrk9r.css">
|
|
12
12
|
</head>
|
|
13
13
|
|
|
14
14
|
<body>
|
package/package.json
CHANGED
package/src/api/config.ts
CHANGED
|
@@ -6,6 +6,8 @@ import type {
|
|
|
6
6
|
ProviderConfigView,
|
|
7
7
|
ChannelConfigUpdate,
|
|
8
8
|
ProviderConfigUpdate,
|
|
9
|
+
ProviderConnectionTestRequest,
|
|
10
|
+
ProviderConnectionTestResult,
|
|
9
11
|
RuntimeConfigUpdate,
|
|
10
12
|
SecretsConfigUpdate,
|
|
11
13
|
SecretsView,
|
|
@@ -79,6 +81,21 @@ export async function updateProvider(
|
|
|
79
81
|
return response.data;
|
|
80
82
|
}
|
|
81
83
|
|
|
84
|
+
// POST /api/config/providers/:provider/test
|
|
85
|
+
export async function testProviderConnection(
|
|
86
|
+
provider: string,
|
|
87
|
+
data: ProviderConnectionTestRequest
|
|
88
|
+
): Promise<ProviderConnectionTestResult> {
|
|
89
|
+
const response = await api.post<ProviderConnectionTestResult>(
|
|
90
|
+
`/api/config/providers/${provider}/test`,
|
|
91
|
+
data
|
|
92
|
+
);
|
|
93
|
+
if (!response.ok) {
|
|
94
|
+
throw new Error(response.error.message);
|
|
95
|
+
}
|
|
96
|
+
return response.data;
|
|
97
|
+
}
|
|
98
|
+
|
|
82
99
|
// PUT /api/config/channels/:channel
|
|
83
100
|
export async function updateChannel(
|
|
84
101
|
channel: string,
|
package/src/api/types.ts
CHANGED
|
@@ -24,6 +24,18 @@ export type ProviderConfigUpdate = {
|
|
|
24
24
|
wireApi?: "auto" | "chat" | "responses" | null;
|
|
25
25
|
};
|
|
26
26
|
|
|
27
|
+
export type ProviderConnectionTestRequest = ProviderConfigUpdate & {
|
|
28
|
+
model?: string | null;
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
export type ProviderConnectionTestResult = {
|
|
32
|
+
success: boolean;
|
|
33
|
+
provider: string;
|
|
34
|
+
model?: string;
|
|
35
|
+
latencyMs: number;
|
|
36
|
+
message: string;
|
|
37
|
+
};
|
|
38
|
+
|
|
27
39
|
export type AgentProfileView = {
|
|
28
40
|
id: string;
|
|
29
41
|
default?: boolean;
|
|
@@ -7,7 +7,8 @@ import { useConfig, useConfigSchema, useUpdateModel } from '@/hooks/useConfig';
|
|
|
7
7
|
import { hintForPath } from '@/lib/config-hints';
|
|
8
8
|
import { formatNumber, t } from '@/lib/i18n';
|
|
9
9
|
import { PageLayout, PageHeader } from '@/components/layout/page-layout';
|
|
10
|
-
import {
|
|
10
|
+
import { DOCS_DEFAULT_BASE_URL } from '@/components/doc-browser/DocBrowserContext';
|
|
11
|
+
import { BookOpen, Folder, Loader2, Sliders, Sparkles } from 'lucide-react';
|
|
11
12
|
import { useEffect, useState } from 'react';
|
|
12
13
|
|
|
13
14
|
export function ModelConfig() {
|
|
@@ -96,8 +97,15 @@ export function ModelConfig() {
|
|
|
96
97
|
/>
|
|
97
98
|
<p className="text-xs text-gray-400">
|
|
98
99
|
{modelHint?.help ??
|
|
99
|
-
'Examples:
|
|
100
|
+
'Examples: gpt-5.1 · claude-opus-4-1 · deepseek/deepseek-chat · dashscope/qwen-max-latest · openrouter/openai/gpt-5.3-codex'}
|
|
100
101
|
</p>
|
|
102
|
+
<a
|
|
103
|
+
href={`${DOCS_DEFAULT_BASE_URL}/guide/model-selection`}
|
|
104
|
+
className="inline-flex items-center gap-1.5 text-xs text-primary hover:text-primary-hover transition-colors"
|
|
105
|
+
>
|
|
106
|
+
<BookOpen className="h-3.5 w-3.5" />
|
|
107
|
+
{t('channelsGuideTitle')}
|
|
108
|
+
</a>
|
|
101
109
|
</div>
|
|
102
110
|
</div>
|
|
103
111
|
|
|
@@ -1,203 +1,299 @@
|
|
|
1
|
-
import { useEffect, useState } from 'react';
|
|
2
|
-
import { useConfig, useConfigMeta, useConfigSchema, useUpdateProvider } from '@/hooks/useConfig';
|
|
3
|
-
import { useUiStore } from '@/stores/ui.store';
|
|
4
|
-
import {
|
|
5
|
-
Dialog,
|
|
6
|
-
DialogContent,
|
|
7
|
-
DialogHeader,
|
|
8
|
-
DialogTitle,
|
|
9
|
-
DialogDescription,
|
|
10
|
-
DialogFooter,
|
|
11
|
-
} from '@/components/ui/dialog';
|
|
1
|
+
import { useEffect, useMemo, useState } from 'react';
|
|
2
|
+
import { useConfig, useConfigMeta, useConfigSchema, useTestProviderConnection, useUpdateProvider } from '@/hooks/useConfig';
|
|
12
3
|
import { Button } from '@/components/ui/button';
|
|
13
4
|
import { Input } from '@/components/ui/input';
|
|
14
5
|
import { Label } from '@/components/ui/label';
|
|
15
6
|
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
|
|
16
7
|
import { MaskedInput } from '@/components/common/MaskedInput';
|
|
17
8
|
import { KeyValueEditor } from '@/components/common/KeyValueEditor';
|
|
9
|
+
import { StatusDot } from '@/components/ui/status-dot';
|
|
18
10
|
import { t } from '@/lib/i18n';
|
|
19
11
|
import { hintForPath } from '@/lib/config-hints';
|
|
20
|
-
import type { ProviderConfigUpdate } from '@/api/types';
|
|
21
|
-
import { KeyRound, Globe, Hash } from 'lucide-react';
|
|
12
|
+
import type { ProviderConfigUpdate, ProviderConnectionTestRequest } from '@/api/types';
|
|
13
|
+
import { KeyRound, Globe, Hash, RotateCcw, CircleDotDashed } from 'lucide-react';
|
|
14
|
+
import { toast } from 'sonner';
|
|
22
15
|
|
|
23
|
-
|
|
24
|
-
|
|
16
|
+
type WireApiType = 'auto' | 'chat' | 'responses';
|
|
17
|
+
|
|
18
|
+
type ProviderFormProps = {
|
|
19
|
+
providerName?: string;
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
function normalizeHeaders(input: Record<string, string> | null | undefined): Record<string, string> | null {
|
|
23
|
+
if (!input) {
|
|
24
|
+
return null;
|
|
25
|
+
}
|
|
26
|
+
const entries = Object.entries(input)
|
|
27
|
+
.map(([key, value]) => [key.trim(), value] as const)
|
|
28
|
+
.filter(([key]) => key.length > 0);
|
|
29
|
+
if (entries.length === 0) {
|
|
30
|
+
return null;
|
|
31
|
+
}
|
|
32
|
+
return Object.fromEntries(entries);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function headersEqual(
|
|
36
|
+
left: Record<string, string> | null | undefined,
|
|
37
|
+
right: Record<string, string> | null | undefined
|
|
38
|
+
): boolean {
|
|
39
|
+
const a = normalizeHeaders(left);
|
|
40
|
+
const b = normalizeHeaders(right);
|
|
41
|
+
if (a === null && b === null) {
|
|
42
|
+
return true;
|
|
43
|
+
}
|
|
44
|
+
if (!a || !b) {
|
|
45
|
+
return false;
|
|
46
|
+
}
|
|
47
|
+
const aEntries = Object.entries(a).sort(([ak], [bk]) => ak.localeCompare(bk));
|
|
48
|
+
const bEntries = Object.entries(b).sort(([ak], [bk]) => ak.localeCompare(bk));
|
|
49
|
+
if (aEntries.length !== bEntries.length) {
|
|
50
|
+
return false;
|
|
51
|
+
}
|
|
52
|
+
return aEntries.every(([key, value], index) => key === bEntries[index][0] && value === bEntries[index][1]);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export function ProviderForm({ providerName }: ProviderFormProps) {
|
|
25
56
|
const { data: config } = useConfig();
|
|
26
57
|
const { data: meta } = useConfigMeta();
|
|
27
58
|
const { data: schema } = useConfigSchema();
|
|
28
59
|
const updateProvider = useUpdateProvider();
|
|
60
|
+
const testProviderConnection = useTestProviderConnection();
|
|
29
61
|
|
|
30
62
|
const [apiKey, setApiKey] = useState('');
|
|
31
63
|
const [apiBase, setApiBase] = useState('');
|
|
32
64
|
const [extraHeaders, setExtraHeaders] = useState<Record<string, string> | null>(null);
|
|
33
|
-
const [wireApi, setWireApi] = useState<
|
|
65
|
+
const [wireApi, setWireApi] = useState<WireApiType>('auto');
|
|
34
66
|
|
|
35
|
-
const providerName = providerModal.provider;
|
|
36
67
|
const providerSpec = meta?.providers.find((p) => p.name === providerName);
|
|
37
68
|
const providerConfig = providerName ? config?.providers[providerName] : null;
|
|
38
69
|
const uiHints = schema?.uiHints;
|
|
70
|
+
|
|
39
71
|
const apiKeyHint = providerName ? hintForPath(`providers.${providerName}.apiKey`, uiHints) : undefined;
|
|
40
72
|
const apiBaseHint = providerName ? hintForPath(`providers.${providerName}.apiBase`, uiHints) : undefined;
|
|
41
73
|
const extraHeadersHint = providerName ? hintForPath(`providers.${providerName}.extraHeaders`, uiHints) : undefined;
|
|
42
74
|
const wireApiHint = providerName ? hintForPath(`providers.${providerName}.wireApi`, uiHints) : undefined;
|
|
43
75
|
|
|
76
|
+
const providerTitle = providerSpec?.displayName || providerName || t('providersSelectPlaceholder');
|
|
77
|
+
const defaultApiBase = providerSpec?.defaultApiBase || '';
|
|
78
|
+
const currentApiBase = providerConfig?.apiBase || defaultApiBase;
|
|
79
|
+
const currentHeaders = normalizeHeaders(providerConfig?.extraHeaders || null);
|
|
80
|
+
const currentWireApi = (providerConfig?.wireApi || providerSpec?.defaultWireApi || 'auto') as WireApiType;
|
|
81
|
+
|
|
44
82
|
useEffect(() => {
|
|
45
|
-
if (
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
83
|
+
if (!providerName) {
|
|
84
|
+
setApiKey('');
|
|
85
|
+
setApiBase('');
|
|
86
|
+
setExtraHeaders(null);
|
|
87
|
+
setWireApi('auto');
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
setApiKey('');
|
|
92
|
+
setApiBase(currentApiBase);
|
|
93
|
+
setExtraHeaders(providerConfig?.extraHeaders || null);
|
|
94
|
+
setWireApi(currentWireApi);
|
|
95
|
+
}, [providerName, currentApiBase, providerConfig?.extraHeaders, currentWireApi]);
|
|
96
|
+
|
|
97
|
+
const hasChanges = useMemo(() => {
|
|
98
|
+
if (!providerName) {
|
|
99
|
+
return false;
|
|
52
100
|
}
|
|
53
|
-
|
|
101
|
+
const apiKeyChanged = apiKey.trim().length > 0;
|
|
102
|
+
const apiBaseChanged = apiBase.trim() !== currentApiBase.trim();
|
|
103
|
+
const headersChanged = !headersEqual(extraHeaders, currentHeaders);
|
|
104
|
+
const wireApiChanged = providerSpec?.supportsWireApi ? wireApi !== currentWireApi : false;
|
|
105
|
+
|
|
106
|
+
return apiKeyChanged || apiBaseChanged || headersChanged || wireApiChanged;
|
|
107
|
+
}, [
|
|
108
|
+
providerName,
|
|
109
|
+
apiKey,
|
|
110
|
+
apiBase,
|
|
111
|
+
currentApiBase,
|
|
112
|
+
extraHeaders,
|
|
113
|
+
currentHeaders,
|
|
114
|
+
providerSpec?.supportsWireApi,
|
|
115
|
+
wireApi,
|
|
116
|
+
currentWireApi
|
|
117
|
+
]);
|
|
118
|
+
|
|
119
|
+
const resetToDefault = () => {
|
|
120
|
+
setApiKey('');
|
|
121
|
+
setApiBase(defaultApiBase);
|
|
122
|
+
setExtraHeaders(null);
|
|
123
|
+
setWireApi((providerSpec?.defaultWireApi || 'auto') as WireApiType);
|
|
124
|
+
};
|
|
54
125
|
|
|
55
126
|
const handleSubmit = (e: React.FormEvent) => {
|
|
56
127
|
e.preventDefault();
|
|
57
128
|
|
|
129
|
+
if (!providerName) {
|
|
130
|
+
return;
|
|
131
|
+
}
|
|
132
|
+
|
|
58
133
|
const payload: ProviderConfigUpdate = {};
|
|
134
|
+
const trimmedApiKey = apiKey.trim();
|
|
135
|
+
const trimmedApiBase = apiBase.trim();
|
|
136
|
+
const normalizedHeaders = normalizeHeaders(extraHeaders);
|
|
59
137
|
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
payload.apiKey = apiKey;
|
|
138
|
+
if (trimmedApiKey.length > 0) {
|
|
139
|
+
payload.apiKey = trimmedApiKey;
|
|
63
140
|
}
|
|
64
141
|
|
|
65
|
-
if (
|
|
66
|
-
payload.apiBase =
|
|
142
|
+
if (trimmedApiBase !== currentApiBase.trim()) {
|
|
143
|
+
payload.apiBase = trimmedApiBase.length > 0 && trimmedApiBase !== defaultApiBase ? trimmedApiBase : null;
|
|
67
144
|
}
|
|
68
145
|
|
|
69
|
-
if (
|
|
70
|
-
payload.extraHeaders =
|
|
146
|
+
if (!headersEqual(normalizedHeaders, currentHeaders)) {
|
|
147
|
+
payload.extraHeaders = normalizedHeaders;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
if (providerSpec?.supportsWireApi && wireApi !== currentWireApi) {
|
|
151
|
+
payload.wireApi = wireApi;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
updateProvider.mutate({ provider: providerName, data: payload });
|
|
155
|
+
};
|
|
156
|
+
|
|
157
|
+
const handleTestConnection = async () => {
|
|
158
|
+
if (!providerName) {
|
|
159
|
+
return;
|
|
71
160
|
}
|
|
72
161
|
|
|
162
|
+
const payload: ProviderConnectionTestRequest = {
|
|
163
|
+
apiBase: apiBase.trim(),
|
|
164
|
+
extraHeaders: normalizeHeaders(extraHeaders),
|
|
165
|
+
model: config?.agents.defaults.model ?? null
|
|
166
|
+
};
|
|
167
|
+
if (apiKey.trim().length > 0) {
|
|
168
|
+
payload.apiKey = apiKey.trim();
|
|
169
|
+
}
|
|
73
170
|
if (providerSpec?.supportsWireApi) {
|
|
74
|
-
|
|
75
|
-
providerConfig?.wireApi || providerSpec.defaultWireApi || 'auto';
|
|
76
|
-
if (wireApi !== currentWireApi) {
|
|
77
|
-
payload.wireApi = wireApi;
|
|
78
|
-
}
|
|
171
|
+
payload.wireApi = wireApi;
|
|
79
172
|
}
|
|
80
173
|
|
|
81
|
-
|
|
174
|
+
try {
|
|
175
|
+
const result = await testProviderConnection.mutateAsync({
|
|
176
|
+
provider: providerName,
|
|
177
|
+
data: payload
|
|
178
|
+
});
|
|
179
|
+
if (result.success) {
|
|
180
|
+
toast.success(`${t('providerTestConnectionSuccess')} (${result.latencyMs}ms)`);
|
|
181
|
+
return;
|
|
182
|
+
}
|
|
183
|
+
toast.error(`${t('providerTestConnectionFailed')}: ${result.message}`);
|
|
184
|
+
} catch (error) {
|
|
185
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
186
|
+
toast.error(`${t('providerTestConnectionFailed')}: ${message}`);
|
|
187
|
+
}
|
|
188
|
+
};
|
|
82
189
|
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
190
|
+
if (!providerName || !providerSpec || !providerConfig) {
|
|
191
|
+
return (
|
|
192
|
+
<div className="flex min-h-[520px] items-center justify-center rounded-2xl border border-gray-200/70 bg-white px-6 text-center xl:h-[calc(100vh-180px)] xl:min-h-[600px] xl:max-h-[860px]">
|
|
193
|
+
<div>
|
|
194
|
+
<h3 className="text-base font-semibold text-gray-900">{t('providersSelectTitle')}</h3>
|
|
195
|
+
<p className="mt-2 text-sm text-gray-500">{t('providersSelectDescription')}</p>
|
|
196
|
+
</div>
|
|
197
|
+
</div>
|
|
86
198
|
);
|
|
87
|
-
}
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
const statusLabel = providerConfig.apiKeySet ? t('statusReady') : t('statusSetup');
|
|
88
202
|
|
|
89
203
|
return (
|
|
90
|
-
<
|
|
91
|
-
<
|
|
92
|
-
<
|
|
93
|
-
<div className="
|
|
94
|
-
<
|
|
95
|
-
|
|
96
|
-
</div>
|
|
97
|
-
<div>
|
|
98
|
-
<DialogTitle>{providerSpec?.displayName || providerName}</DialogTitle>
|
|
99
|
-
<DialogDescription>{t('providerFormDescription')}</DialogDescription>
|
|
100
|
-
</div>
|
|
204
|
+
<div className="flex min-h-[520px] flex-col rounded-2xl border border-gray-200/70 bg-white shadow-card xl:h-[calc(100vh-180px)] xl:min-h-[600px] xl:max-h-[860px]">
|
|
205
|
+
<div className="border-b border-gray-100 px-6 py-5">
|
|
206
|
+
<div className="flex flex-wrap items-center justify-between gap-3">
|
|
207
|
+
<div className="min-w-0">
|
|
208
|
+
<h3 className="truncate text-lg font-semibold text-gray-900">{providerTitle}</h3>
|
|
209
|
+
<p className="mt-1 text-sm text-gray-500">{t('providerFormDescription')}</p>
|
|
101
210
|
</div>
|
|
102
|
-
|
|
211
|
+
<StatusDot status={providerConfig.apiKeySet ? 'ready' : 'setup'} label={statusLabel} />
|
|
212
|
+
</div>
|
|
213
|
+
</div>
|
|
103
214
|
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
className="rounded-xl"
|
|
122
|
-
/>
|
|
123
|
-
</div>
|
|
124
|
-
|
|
125
|
-
<div className="space-y-2.5">
|
|
126
|
-
<Label htmlFor="apiBase" className="text-sm font-medium text-gray-900 flex items-center gap-2">
|
|
127
|
-
<Globe className="h-3.5 w-3.5 text-gray-500" />
|
|
128
|
-
{apiBaseHint?.label ?? t('apiBase')}
|
|
129
|
-
</Label>
|
|
130
|
-
<Input
|
|
131
|
-
id="apiBase"
|
|
132
|
-
type="text"
|
|
133
|
-
value={apiBase}
|
|
134
|
-
onChange={(e) => setApiBase(e.target.value)}
|
|
135
|
-
placeholder={
|
|
136
|
-
providerSpec?.defaultApiBase ||
|
|
137
|
-
apiBaseHint?.placeholder ||
|
|
138
|
-
'https://api.example.com'
|
|
139
|
-
}
|
|
140
|
-
className="rounded-xl"
|
|
141
|
-
/>
|
|
142
|
-
{apiBaseHint?.help && (
|
|
143
|
-
<p className="text-xs text-gray-500">{apiBaseHint.help}</p>
|
|
144
|
-
)}
|
|
145
|
-
</div>
|
|
215
|
+
<form onSubmit={handleSubmit} className="flex min-h-0 flex-1 flex-col">
|
|
216
|
+
<div className="min-h-0 flex-1 space-y-6 overflow-y-auto px-6 py-5">
|
|
217
|
+
<div className="space-y-2.5">
|
|
218
|
+
<Label htmlFor="apiKey" className="flex items-center gap-2 text-sm font-medium text-gray-900">
|
|
219
|
+
<KeyRound className="h-3.5 w-3.5 text-gray-500" />
|
|
220
|
+
{apiKeyHint?.label ?? t('apiKey')}
|
|
221
|
+
</Label>
|
|
222
|
+
<MaskedInput
|
|
223
|
+
id="apiKey"
|
|
224
|
+
value={apiKey}
|
|
225
|
+
isSet={providerConfig.apiKeySet}
|
|
226
|
+
onChange={(e) => setApiKey(e.target.value)}
|
|
227
|
+
placeholder={providerConfig.apiKeySet ? t('apiKeySet') : apiKeyHint?.placeholder ?? t('enterApiKey')}
|
|
228
|
+
className="rounded-xl"
|
|
229
|
+
/>
|
|
230
|
+
<p className="text-xs text-gray-500">{t('leaveBlankToKeepUnchanged')}</p>
|
|
231
|
+
</div>
|
|
146
232
|
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
: option === 'responses'
|
|
163
|
-
? t('wireApiResponses')
|
|
164
|
-
: t('wireApiAuto')}
|
|
165
|
-
</SelectItem>
|
|
166
|
-
))}
|
|
167
|
-
</SelectContent>
|
|
168
|
-
</Select>
|
|
169
|
-
</div>
|
|
170
|
-
)}
|
|
233
|
+
<div className="space-y-2.5">
|
|
234
|
+
<Label htmlFor="apiBase" className="flex items-center gap-2 text-sm font-medium text-gray-900">
|
|
235
|
+
<Globe className="h-3.5 w-3.5 text-gray-500" />
|
|
236
|
+
{apiBaseHint?.label ?? t('apiBase')}
|
|
237
|
+
</Label>
|
|
238
|
+
<Input
|
|
239
|
+
id="apiBase"
|
|
240
|
+
type="text"
|
|
241
|
+
value={apiBase}
|
|
242
|
+
onChange={(e) => setApiBase(e.target.value)}
|
|
243
|
+
placeholder={defaultApiBase || apiBaseHint?.placeholder || 'https://api.example.com'}
|
|
244
|
+
className="rounded-xl"
|
|
245
|
+
/>
|
|
246
|
+
<p className="text-xs text-gray-500">{apiBaseHint?.help || t('providerApiBaseHelp')}</p>
|
|
247
|
+
</div>
|
|
171
248
|
|
|
249
|
+
{providerSpec.supportsWireApi && (
|
|
172
250
|
<div className="space-y-2.5">
|
|
173
|
-
<Label className="text-sm font-medium text-gray-900
|
|
251
|
+
<Label htmlFor="wireApi" className="flex items-center gap-2 text-sm font-medium text-gray-900">
|
|
174
252
|
<Hash className="h-3.5 w-3.5 text-gray-500" />
|
|
175
|
-
{
|
|
253
|
+
{wireApiHint?.label ?? t('wireApi')}
|
|
176
254
|
</Label>
|
|
177
|
-
<
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
255
|
+
<Select value={wireApi} onValueChange={(v) => setWireApi(v as WireApiType)}>
|
|
256
|
+
<SelectTrigger className="rounded-xl">
|
|
257
|
+
<SelectValue />
|
|
258
|
+
</SelectTrigger>
|
|
259
|
+
<SelectContent>
|
|
260
|
+
{(providerSpec.wireApiOptions || ['auto', 'chat', 'responses']).map((option) => (
|
|
261
|
+
<SelectItem key={option} value={option}>
|
|
262
|
+
{option === 'chat' ? t('wireApiChat') : option === 'responses' ? t('wireApiResponses') : t('wireApiAuto')}
|
|
263
|
+
</SelectItem>
|
|
264
|
+
))}
|
|
265
|
+
</SelectContent>
|
|
266
|
+
</Select>
|
|
267
|
+
{wireApiHint?.help && <p className="text-xs text-gray-500">{wireApiHint.help}</p>}
|
|
181
268
|
</div>
|
|
269
|
+
)}
|
|
270
|
+
|
|
271
|
+
<div className="space-y-2.5">
|
|
272
|
+
<Label className="flex items-center gap-2 text-sm font-medium text-gray-900">
|
|
273
|
+
<Hash className="h-3.5 w-3.5 text-gray-500" />
|
|
274
|
+
{extraHeadersHint?.label ?? t('extraHeaders')}
|
|
275
|
+
</Label>
|
|
276
|
+
<KeyValueEditor value={extraHeaders} onChange={setExtraHeaders} />
|
|
277
|
+
<p className="text-xs text-gray-500">{extraHeadersHint?.help || t('providerExtraHeadersHelp')}</p>
|
|
182
278
|
</div>
|
|
279
|
+
</div>
|
|
183
280
|
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
>
|
|
190
|
-
{t('cancel')}
|
|
281
|
+
<div className="flex flex-wrap items-center justify-between gap-3 border-t border-gray-100 px-6 py-4">
|
|
282
|
+
<div className="flex flex-wrap items-center gap-2">
|
|
283
|
+
<Button type="button" variant="outline" onClick={resetToDefault}>
|
|
284
|
+
<RotateCcw className="mr-2 h-4 w-4" />
|
|
285
|
+
{t('resetToDefault')}
|
|
191
286
|
</Button>
|
|
192
|
-
<Button
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
>
|
|
196
|
-
{updateProvider.isPending ? t('saving') : t('save')}
|
|
287
|
+
<Button type="button" variant="outline" onClick={handleTestConnection} disabled={testProviderConnection.isPending}>
|
|
288
|
+
<CircleDotDashed className="mr-2 h-4 w-4" />
|
|
289
|
+
{testProviderConnection.isPending ? t('providerTestingConnection') : t('providerTestConnection')}
|
|
197
290
|
</Button>
|
|
198
|
-
</
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
291
|
+
</div>
|
|
292
|
+
<Button type="submit" disabled={updateProvider.isPending || !hasChanges}>
|
|
293
|
+
{updateProvider.isPending ? t('saving') : hasChanges ? t('save') : t('unchanged')}
|
|
294
|
+
</Button>
|
|
295
|
+
</div>
|
|
296
|
+
</form>
|
|
297
|
+
</div>
|
|
202
298
|
);
|
|
203
299
|
}
|