@nextclaw/ui 0.5.28 → 0.5.30
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 +12 -0
- package/dist/assets/ChannelsList-DKiedAFa.js +1 -0
- package/dist/assets/{ChatPage-CjzR-76f.js → ChatPage-BwNLpkSy.js} +16 -16
- package/dist/assets/{CronConfig-DREWLzKD.js → CronConfig-DS4SLvdJ.js} +1 -1
- package/dist/assets/{DocBrowser-fXdmzwRi.js → DocBrowser-GX0i3zMz.js} +1 -1
- package/dist/assets/{MarketplacePage-zKC-b_O_.js → MarketplacePage-Cfary3pL.js} +1 -1
- package/dist/assets/{ModelConfig-BdogzFBq.js → ModelConfig-DqA1Fpq1.js} +1 -1
- package/dist/assets/ProvidersList-Bp8j2dHz.js +1 -0
- package/dist/assets/{RuntimeConfig-CGsoLtsV.js → RuntimeConfig-BPFqqOmo.js} +1 -1
- package/dist/assets/{SecretsConfig-B8ZDpRgB.js → SecretsConfig-817qoOc8.js} +1 -1
- package/dist/assets/{SessionsConfig-CcpfGazP.js → SessionsConfig-DSz76nPB.js} +1 -1
- package/dist/assets/{card-Bt8T3JA3.js → card-DsXZnE66.js} +1 -1
- package/dist/assets/{dialog-BNi5ymWD.js → dialog-CM-OB-VU.js} +2 -2
- package/dist/assets/index-BDnLuH5V.js +2 -0
- package/dist/assets/index-yypHrk9r.css +1 -0
- package/dist/assets/{label-BDCnjFl3.js → label-BryQW6Hh.js} +1 -1
- package/dist/assets/logos-DpQfWN7T.js +1 -0
- package/dist/assets/{page-layout-DWVnm0X8.js → page-layout-CljvdClt.js} +1 -1
- package/dist/assets/{switch-CrmdAOBw.js → switch-DK9xeFoj.js} +1 -1
- package/dist/assets/{tabs-custom-WJxQwDQy.js → tabs-custom-BWSgRC2i.js} +1 -1
- package/dist/assets/useConfig-Cr9T194r.js +6 -0
- package/dist/assets/{useConfirmDialog-C5hEiEeb.js → useConfirmDialog-CeUaLKWy.js} +1 -1
- package/dist/assets/{vendor-H2M3a_4Z.js → vendor-CrNxGKd2.js} +77 -72
- package/dist/index.html +3 -3
- package/package.json +1 -1
- package/src/components/chat/ChatPage.tsx +34 -4
- package/src/components/config/ProviderForm.tsx +198 -143
- package/src/components/config/ProvidersList.tsx +140 -80
- package/src/components/ui/status-dot.tsx +1 -1
- package/src/hooks/useConfig.ts +2 -1
- package/src/lib/i18n.ts +10 -1
- package/src/stores/ui.store.ts +0 -9
- package/dist/assets/ChannelsList-DgkIu7_t.js +0 -1
- package/dist/assets/ProvidersList-BLScOe9j.js +0 -1
- package/dist/assets/action-link-RjYHzlQk.js +0 -1
- package/dist/assets/index-B_OeEGic.css +0 -1
- package/dist/assets/index-CtNWUrVR.js +0 -2
- package/dist/assets/useConfig-COoN7EVf.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-BDnLuH5V.js"></script>
|
|
10
|
+
<link rel="modulepreload" crossorigin href="/assets/vendor-CrNxGKd2.js">
|
|
11
|
+
<link rel="stylesheet" crossorigin href="/assets/index-yypHrk9r.css">
|
|
12
12
|
</head>
|
|
13
13
|
|
|
14
14
|
<body>
|
package/package.json
CHANGED
|
@@ -94,10 +94,12 @@ export function ChatPage() {
|
|
|
94
94
|
const [draft, setDraft] = useState('');
|
|
95
95
|
const [selectedSessionKey, setSelectedSessionKey] = useState<string | null>(() => readStoredSessionKey());
|
|
96
96
|
const [selectedAgentId, setSelectedAgentId] = useState('main');
|
|
97
|
+
const [optimisticUserEvent, setOptimisticUserEvent] = useState<SessionEventView | null>(null);
|
|
97
98
|
const [streamingSessionEvents, setStreamingSessionEvents] = useState<SessionEventView[]>([]);
|
|
98
99
|
const [streamingAssistantText, setStreamingAssistantText] = useState('');
|
|
99
100
|
const [streamingAssistantTimestamp, setStreamingAssistantTimestamp] = useState<string | null>(null);
|
|
100
101
|
const [isSending, setIsSending] = useState(false);
|
|
102
|
+
const [isAwaitingAssistantOutput, setIsAwaitingAssistantOutput] = useState(false);
|
|
101
103
|
const [queuedMessages, setQueuedMessages] = useState<PendingChatMessage[]>([]);
|
|
102
104
|
|
|
103
105
|
const { confirm, ConfirmDialog } = useConfirmDialog();
|
|
@@ -152,7 +154,11 @@ export function ChatPage() {
|
|
|
152
154
|
? historyData.events
|
|
153
155
|
: buildFallbackEventsFromMessages(historyMessages);
|
|
154
156
|
const mergedEvents = useMemo(() => {
|
|
155
|
-
const next = [...historyEvents
|
|
157
|
+
const next = [...historyEvents];
|
|
158
|
+
if (optimisticUserEvent) {
|
|
159
|
+
next.push(optimisticUserEvent);
|
|
160
|
+
}
|
|
161
|
+
next.push(...streamingSessionEvents);
|
|
156
162
|
if (streamingAssistantText.trim()) {
|
|
157
163
|
const maxSeq = next.reduce((max, event) => {
|
|
158
164
|
const seq = Number.isFinite(event.seq) ? event.seq : 0;
|
|
@@ -170,7 +176,7 @@ export function ChatPage() {
|
|
|
170
176
|
});
|
|
171
177
|
}
|
|
172
178
|
return next;
|
|
173
|
-
}, [historyEvents, streamingAssistantText, streamingAssistantTimestamp, streamingSessionEvents]);
|
|
179
|
+
}, [historyEvents, optimisticUserEvent, streamingAssistantText, streamingAssistantTimestamp, streamingSessionEvents]);
|
|
174
180
|
|
|
175
181
|
useEffect(() => {
|
|
176
182
|
if (!selectedSessionKey && filteredSessions.length > 0) {
|
|
@@ -214,9 +220,11 @@ export function ChatPage() {
|
|
|
214
220
|
streamRunIdRef.current += 1;
|
|
215
221
|
setIsSending(false);
|
|
216
222
|
setQueuedMessages([]);
|
|
223
|
+
setOptimisticUserEvent(null);
|
|
217
224
|
setStreamingSessionEvents([]);
|
|
218
225
|
setStreamingAssistantText('');
|
|
219
226
|
setStreamingAssistantTimestamp(null);
|
|
227
|
+
setIsAwaitingAssistantOutput(false);
|
|
220
228
|
const next = buildNewSessionKey(selectedAgentId);
|
|
221
229
|
setSelectedSessionKey(next);
|
|
222
230
|
};
|
|
@@ -240,9 +248,11 @@ export function ChatPage() {
|
|
|
240
248
|
streamRunIdRef.current += 1;
|
|
241
249
|
setIsSending(false);
|
|
242
250
|
setQueuedMessages([]);
|
|
251
|
+
setOptimisticUserEvent(null);
|
|
243
252
|
setStreamingSessionEvents([]);
|
|
244
253
|
setStreamingAssistantText('');
|
|
245
254
|
setStreamingAssistantTimestamp(null);
|
|
255
|
+
setIsAwaitingAssistantOutput(false);
|
|
246
256
|
setSelectedSessionKey(null);
|
|
247
257
|
await sessionsQuery.refetch();
|
|
248
258
|
}
|
|
@@ -257,7 +267,18 @@ export function ChatPage() {
|
|
|
257
267
|
setStreamingSessionEvents([]);
|
|
258
268
|
setStreamingAssistantText('');
|
|
259
269
|
setStreamingAssistantTimestamp(null);
|
|
270
|
+
setOptimisticUserEvent({
|
|
271
|
+
seq: 0,
|
|
272
|
+
type: 'message.user.optimistic',
|
|
273
|
+
timestamp: new Date().toISOString(),
|
|
274
|
+
message: {
|
|
275
|
+
role: 'user',
|
|
276
|
+
content: item.message,
|
|
277
|
+
timestamp: new Date().toISOString()
|
|
278
|
+
}
|
|
279
|
+
});
|
|
260
280
|
setIsSending(true);
|
|
281
|
+
setIsAwaitingAssistantOutput(true);
|
|
261
282
|
|
|
262
283
|
try {
|
|
263
284
|
let streamText = '';
|
|
@@ -285,11 +306,15 @@ export function ChatPage() {
|
|
|
285
306
|
}
|
|
286
307
|
streamText += event.delta;
|
|
287
308
|
setStreamingAssistantText(streamText);
|
|
309
|
+
setIsAwaitingAssistantOutput(false);
|
|
288
310
|
},
|
|
289
311
|
onSessionEvent: (event) => {
|
|
290
312
|
if (runId !== streamRunIdRef.current) {
|
|
291
313
|
return;
|
|
292
314
|
}
|
|
315
|
+
if (event.data.message?.role === 'user') {
|
|
316
|
+
setOptimisticUserEvent(null);
|
|
317
|
+
}
|
|
293
318
|
setStreamingSessionEvents((prev) => {
|
|
294
319
|
const next = [...prev];
|
|
295
320
|
const hit = next.findIndex((item) => item.seq === event.data.seq);
|
|
@@ -302,12 +327,14 @@ export function ChatPage() {
|
|
|
302
327
|
});
|
|
303
328
|
if (event.data.message?.role === 'assistant') {
|
|
304
329
|
setStreamingAssistantText('');
|
|
330
|
+
setIsAwaitingAssistantOutput(false);
|
|
305
331
|
}
|
|
306
332
|
}
|
|
307
333
|
});
|
|
308
334
|
if (runId !== streamRunIdRef.current) {
|
|
309
335
|
return;
|
|
310
336
|
}
|
|
337
|
+
setOptimisticUserEvent(null);
|
|
311
338
|
if (result.sessionKey !== item.sessionKey) {
|
|
312
339
|
setSelectedSessionKey(result.sessionKey);
|
|
313
340
|
}
|
|
@@ -319,6 +346,7 @@ export function ChatPage() {
|
|
|
319
346
|
setStreamingSessionEvents([]);
|
|
320
347
|
setStreamingAssistantText('');
|
|
321
348
|
setStreamingAssistantTimestamp(null);
|
|
349
|
+
setIsAwaitingAssistantOutput(false);
|
|
322
350
|
setIsSending(false);
|
|
323
351
|
} catch {
|
|
324
352
|
if (runId !== streamRunIdRef.current) {
|
|
@@ -326,9 +354,11 @@ export function ChatPage() {
|
|
|
326
354
|
}
|
|
327
355
|
streamRunIdRef.current += 1;
|
|
328
356
|
setIsSending(false);
|
|
357
|
+
setOptimisticUserEvent(null);
|
|
329
358
|
setStreamingSessionEvents([]);
|
|
330
359
|
setStreamingAssistantText('');
|
|
331
360
|
setStreamingAssistantTimestamp(null);
|
|
361
|
+
setIsAwaitingAssistantOutput(false);
|
|
332
362
|
if (options?.restoreDraftOnError) {
|
|
333
363
|
setDraft((prev) => prev.trim().length === 0 ? item.message : prev);
|
|
334
364
|
}
|
|
@@ -512,14 +542,14 @@ export function ChatPage() {
|
|
|
512
542
|
<div className="text-xs mt-1">{t('chatNoSessionHint')}</div>
|
|
513
543
|
</div>
|
|
514
544
|
</div>
|
|
515
|
-
) : historyQuery.isLoading ? (
|
|
545
|
+
) : historyQuery.isLoading && mergedEvents.length === 0 && !isSending && !isAwaitingAssistantOutput && !streamingAssistantText.trim() ? (
|
|
516
546
|
<div className="text-sm text-gray-500">{t('chatHistoryLoading')}</div>
|
|
517
547
|
) : (
|
|
518
548
|
<>
|
|
519
549
|
{mergedEvents.length === 0 ? (
|
|
520
550
|
<div className="text-sm text-gray-500">{t('chatNoMessages')}</div>
|
|
521
551
|
) : (
|
|
522
|
-
<ChatThread events={mergedEvents} isSending={isSending &&
|
|
552
|
+
<ChatThread events={mergedEvents} isSending={isSending && isAwaitingAssistantOutput} />
|
|
523
553
|
)}
|
|
524
554
|
</>
|
|
525
555
|
)}
|
|
@@ -1,27 +1,57 @@
|
|
|
1
|
-
import { useEffect, useState } from 'react';
|
|
1
|
+
import { useEffect, useMemo, useState } from 'react';
|
|
2
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';
|
|
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
12
|
import type { ProviderConfigUpdate } from '@/api/types';
|
|
21
|
-
import { KeyRound, Globe, Hash } from 'lucide-react';
|
|
13
|
+
import { KeyRound, Globe, Hash, RotateCcw } from 'lucide-react';
|
|
22
14
|
|
|
23
|
-
|
|
24
|
-
|
|
15
|
+
type WireApiType = 'auto' | 'chat' | 'responses';
|
|
16
|
+
|
|
17
|
+
type ProviderFormProps = {
|
|
18
|
+
providerName?: string;
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
function normalizeHeaders(input: Record<string, string> | null | undefined): Record<string, string> | null {
|
|
22
|
+
if (!input) {
|
|
23
|
+
return null;
|
|
24
|
+
}
|
|
25
|
+
const entries = Object.entries(input)
|
|
26
|
+
.map(([key, value]) => [key.trim(), value] as const)
|
|
27
|
+
.filter(([key]) => key.length > 0);
|
|
28
|
+
if (entries.length === 0) {
|
|
29
|
+
return null;
|
|
30
|
+
}
|
|
31
|
+
return Object.fromEntries(entries);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function headersEqual(
|
|
35
|
+
left: Record<string, string> | null | undefined,
|
|
36
|
+
right: Record<string, string> | null | undefined
|
|
37
|
+
): boolean {
|
|
38
|
+
const a = normalizeHeaders(left);
|
|
39
|
+
const b = normalizeHeaders(right);
|
|
40
|
+
if (a === null && b === null) {
|
|
41
|
+
return true;
|
|
42
|
+
}
|
|
43
|
+
if (!a || !b) {
|
|
44
|
+
return false;
|
|
45
|
+
}
|
|
46
|
+
const aEntries = Object.entries(a).sort(([ak], [bk]) => ak.localeCompare(bk));
|
|
47
|
+
const bEntries = Object.entries(b).sort(([ak], [bk]) => ak.localeCompare(bk));
|
|
48
|
+
if (aEntries.length !== bEntries.length) {
|
|
49
|
+
return false;
|
|
50
|
+
}
|
|
51
|
+
return aEntries.every(([key, value], index) => key === bEntries[index][0] && value === bEntries[index][1]);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export function ProviderForm({ providerName }: ProviderFormProps) {
|
|
25
55
|
const { data: config } = useConfig();
|
|
26
56
|
const { data: meta } = useConfigMeta();
|
|
27
57
|
const { data: schema } = useConfigSchema();
|
|
@@ -30,174 +60,199 @@ export function ProviderForm() {
|
|
|
30
60
|
const [apiKey, setApiKey] = useState('');
|
|
31
61
|
const [apiBase, setApiBase] = useState('');
|
|
32
62
|
const [extraHeaders, setExtraHeaders] = useState<Record<string, string> | null>(null);
|
|
33
|
-
const [wireApi, setWireApi] = useState<
|
|
63
|
+
const [wireApi, setWireApi] = useState<WireApiType>('auto');
|
|
34
64
|
|
|
35
|
-
const providerName = providerModal.provider;
|
|
36
65
|
const providerSpec = meta?.providers.find((p) => p.name === providerName);
|
|
37
66
|
const providerConfig = providerName ? config?.providers[providerName] : null;
|
|
38
67
|
const uiHints = schema?.uiHints;
|
|
68
|
+
|
|
39
69
|
const apiKeyHint = providerName ? hintForPath(`providers.${providerName}.apiKey`, uiHints) : undefined;
|
|
40
70
|
const apiBaseHint = providerName ? hintForPath(`providers.${providerName}.apiBase`, uiHints) : undefined;
|
|
41
71
|
const extraHeadersHint = providerName ? hintForPath(`providers.${providerName}.extraHeaders`, uiHints) : undefined;
|
|
42
72
|
const wireApiHint = providerName ? hintForPath(`providers.${providerName}.wireApi`, uiHints) : undefined;
|
|
43
73
|
|
|
74
|
+
const providerTitle = providerSpec?.displayName || providerName || t('providersSelectPlaceholder');
|
|
75
|
+
const defaultApiBase = providerSpec?.defaultApiBase || '';
|
|
76
|
+
const currentApiBase = providerConfig?.apiBase || defaultApiBase;
|
|
77
|
+
const currentHeaders = normalizeHeaders(providerConfig?.extraHeaders || null);
|
|
78
|
+
const currentWireApi = (providerConfig?.wireApi || providerSpec?.defaultWireApi || 'auto') as WireApiType;
|
|
79
|
+
|
|
44
80
|
useEffect(() => {
|
|
45
|
-
if (
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
setWireApi(nextWireApi as 'auto' | 'chat' | 'responses');
|
|
81
|
+
if (!providerName) {
|
|
82
|
+
setApiKey('');
|
|
83
|
+
setApiBase('');
|
|
84
|
+
setExtraHeaders(null);
|
|
85
|
+
setWireApi('auto');
|
|
86
|
+
return;
|
|
52
87
|
}
|
|
53
|
-
|
|
88
|
+
|
|
89
|
+
setApiKey('');
|
|
90
|
+
setApiBase(currentApiBase);
|
|
91
|
+
setExtraHeaders(providerConfig?.extraHeaders || null);
|
|
92
|
+
setWireApi(currentWireApi);
|
|
93
|
+
}, [providerName, currentApiBase, providerConfig?.extraHeaders, currentWireApi]);
|
|
94
|
+
|
|
95
|
+
const hasChanges = useMemo(() => {
|
|
96
|
+
if (!providerName) {
|
|
97
|
+
return false;
|
|
98
|
+
}
|
|
99
|
+
const apiKeyChanged = apiKey.trim().length > 0;
|
|
100
|
+
const apiBaseChanged = apiBase.trim() !== currentApiBase.trim();
|
|
101
|
+
const headersChanged = !headersEqual(extraHeaders, currentHeaders);
|
|
102
|
+
const wireApiChanged = providerSpec?.supportsWireApi ? wireApi !== currentWireApi : false;
|
|
103
|
+
|
|
104
|
+
return apiKeyChanged || apiBaseChanged || headersChanged || wireApiChanged;
|
|
105
|
+
}, [
|
|
106
|
+
providerName,
|
|
107
|
+
apiKey,
|
|
108
|
+
apiBase,
|
|
109
|
+
currentApiBase,
|
|
110
|
+
extraHeaders,
|
|
111
|
+
currentHeaders,
|
|
112
|
+
providerSpec?.supportsWireApi,
|
|
113
|
+
wireApi,
|
|
114
|
+
currentWireApi
|
|
115
|
+
]);
|
|
116
|
+
|
|
117
|
+
const resetToDefault = () => {
|
|
118
|
+
setApiKey('');
|
|
119
|
+
setApiBase(defaultApiBase);
|
|
120
|
+
setExtraHeaders(null);
|
|
121
|
+
setWireApi((providerSpec?.defaultWireApi || 'auto') as WireApiType);
|
|
122
|
+
};
|
|
54
123
|
|
|
55
124
|
const handleSubmit = (e: React.FormEvent) => {
|
|
56
125
|
e.preventDefault();
|
|
57
126
|
|
|
127
|
+
if (!providerName) {
|
|
128
|
+
return;
|
|
129
|
+
}
|
|
130
|
+
|
|
58
131
|
const payload: ProviderConfigUpdate = {};
|
|
132
|
+
const trimmedApiKey = apiKey.trim();
|
|
133
|
+
const trimmedApiBase = apiBase.trim();
|
|
134
|
+
const normalizedHeaders = normalizeHeaders(extraHeaders);
|
|
59
135
|
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
payload.apiKey = apiKey;
|
|
136
|
+
if (trimmedApiKey.length > 0) {
|
|
137
|
+
payload.apiKey = trimmedApiKey;
|
|
63
138
|
}
|
|
64
139
|
|
|
65
|
-
if (
|
|
66
|
-
payload.apiBase =
|
|
140
|
+
if (trimmedApiBase !== currentApiBase.trim()) {
|
|
141
|
+
payload.apiBase = trimmedApiBase.length > 0 && trimmedApiBase !== defaultApiBase ? trimmedApiBase : null;
|
|
67
142
|
}
|
|
68
143
|
|
|
69
|
-
if (
|
|
70
|
-
payload.extraHeaders =
|
|
144
|
+
if (!headersEqual(normalizedHeaders, currentHeaders)) {
|
|
145
|
+
payload.extraHeaders = normalizedHeaders;
|
|
71
146
|
}
|
|
72
147
|
|
|
73
|
-
if (providerSpec?.supportsWireApi) {
|
|
74
|
-
|
|
75
|
-
providerConfig?.wireApi || providerSpec.defaultWireApi || 'auto';
|
|
76
|
-
if (wireApi !== currentWireApi) {
|
|
77
|
-
payload.wireApi = wireApi;
|
|
78
|
-
}
|
|
148
|
+
if (providerSpec?.supportsWireApi && wireApi !== currentWireApi) {
|
|
149
|
+
payload.wireApi = wireApi;
|
|
79
150
|
}
|
|
80
151
|
|
|
81
|
-
|
|
152
|
+
updateProvider.mutate({ provider: providerName, data: payload });
|
|
153
|
+
};
|
|
82
154
|
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
155
|
+
if (!providerName || !providerSpec || !providerConfig) {
|
|
156
|
+
return (
|
|
157
|
+
<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]">
|
|
158
|
+
<div>
|
|
159
|
+
<h3 className="text-base font-semibold text-gray-900">{t('providersSelectTitle')}</h3>
|
|
160
|
+
<p className="mt-2 text-sm text-gray-500">{t('providersSelectDescription')}</p>
|
|
161
|
+
</div>
|
|
162
|
+
</div>
|
|
86
163
|
);
|
|
87
|
-
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
const statusLabel = providerConfig.apiKeySet ? t('statusReady') : t('statusSetup');
|
|
88
167
|
|
|
89
168
|
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>
|
|
169
|
+
<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]">
|
|
170
|
+
<div className="border-b border-gray-100 px-6 py-5">
|
|
171
|
+
<div className="flex flex-wrap items-center justify-between gap-3">
|
|
172
|
+
<div className="min-w-0">
|
|
173
|
+
<h3 className="truncate text-lg font-semibold text-gray-900">{providerTitle}</h3>
|
|
174
|
+
<p className="mt-1 text-sm text-gray-500">{t('providerFormDescription')}</p>
|
|
101
175
|
</div>
|
|
102
|
-
|
|
176
|
+
<StatusDot status={providerConfig.apiKeySet ? 'ready' : 'setup'} label={statusLabel} />
|
|
177
|
+
</div>
|
|
178
|
+
</div>
|
|
103
179
|
|
|
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>
|
|
180
|
+
<form onSubmit={handleSubmit} className="flex min-h-0 flex-1 flex-col">
|
|
181
|
+
<div className="min-h-0 flex-1 space-y-6 overflow-y-auto px-6 py-5">
|
|
182
|
+
<div className="space-y-2.5">
|
|
183
|
+
<Label htmlFor="apiKey" className="flex items-center gap-2 text-sm font-medium text-gray-900">
|
|
184
|
+
<KeyRound className="h-3.5 w-3.5 text-gray-500" />
|
|
185
|
+
{apiKeyHint?.label ?? t('apiKey')}
|
|
186
|
+
</Label>
|
|
187
|
+
<MaskedInput
|
|
188
|
+
id="apiKey"
|
|
189
|
+
value={apiKey}
|
|
190
|
+
isSet={providerConfig.apiKeySet}
|
|
191
|
+
onChange={(e) => setApiKey(e.target.value)}
|
|
192
|
+
placeholder={providerConfig.apiKeySet ? t('apiKeySet') : apiKeyHint?.placeholder ?? t('enterApiKey')}
|
|
193
|
+
className="rounded-xl"
|
|
194
|
+
/>
|
|
195
|
+
<p className="text-xs text-gray-500">{t('leaveBlankToKeepUnchanged')}</p>
|
|
196
|
+
</div>
|
|
146
197
|
|
|
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
|
-
)}
|
|
198
|
+
<div className="space-y-2.5">
|
|
199
|
+
<Label htmlFor="apiBase" className="flex items-center gap-2 text-sm font-medium text-gray-900">
|
|
200
|
+
<Globe className="h-3.5 w-3.5 text-gray-500" />
|
|
201
|
+
{apiBaseHint?.label ?? t('apiBase')}
|
|
202
|
+
</Label>
|
|
203
|
+
<Input
|
|
204
|
+
id="apiBase"
|
|
205
|
+
type="text"
|
|
206
|
+
value={apiBase}
|
|
207
|
+
onChange={(e) => setApiBase(e.target.value)}
|
|
208
|
+
placeholder={defaultApiBase || apiBaseHint?.placeholder || 'https://api.example.com'}
|
|
209
|
+
className="rounded-xl"
|
|
210
|
+
/>
|
|
211
|
+
<p className="text-xs text-gray-500">{apiBaseHint?.help || t('providerApiBaseHelp')}</p>
|
|
212
|
+
</div>
|
|
171
213
|
|
|
214
|
+
{providerSpec.supportsWireApi && (
|
|
172
215
|
<div className="space-y-2.5">
|
|
173
|
-
<Label className="text-sm font-medium text-gray-900
|
|
216
|
+
<Label htmlFor="wireApi" className="flex items-center gap-2 text-sm font-medium text-gray-900">
|
|
174
217
|
<Hash className="h-3.5 w-3.5 text-gray-500" />
|
|
175
|
-
{
|
|
218
|
+
{wireApiHint?.label ?? t('wireApi')}
|
|
176
219
|
</Label>
|
|
177
|
-
<
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
220
|
+
<Select value={wireApi} onValueChange={(v) => setWireApi(v as WireApiType)}>
|
|
221
|
+
<SelectTrigger className="rounded-xl">
|
|
222
|
+
<SelectValue />
|
|
223
|
+
</SelectTrigger>
|
|
224
|
+
<SelectContent>
|
|
225
|
+
{(providerSpec.wireApiOptions || ['auto', 'chat', 'responses']).map((option) => (
|
|
226
|
+
<SelectItem key={option} value={option}>
|
|
227
|
+
{option === 'chat' ? t('wireApiChat') : option === 'responses' ? t('wireApiResponses') : t('wireApiAuto')}
|
|
228
|
+
</SelectItem>
|
|
229
|
+
))}
|
|
230
|
+
</SelectContent>
|
|
231
|
+
</Select>
|
|
232
|
+
{wireApiHint?.help && <p className="text-xs text-gray-500">{wireApiHint.help}</p>}
|
|
181
233
|
</div>
|
|
234
|
+
)}
|
|
235
|
+
|
|
236
|
+
<div className="space-y-2.5">
|
|
237
|
+
<Label className="flex items-center gap-2 text-sm font-medium text-gray-900">
|
|
238
|
+
<Hash className="h-3.5 w-3.5 text-gray-500" />
|
|
239
|
+
{extraHeadersHint?.label ?? t('extraHeaders')}
|
|
240
|
+
</Label>
|
|
241
|
+
<KeyValueEditor value={extraHeaders} onChange={setExtraHeaders} />
|
|
242
|
+
<p className="text-xs text-gray-500">{extraHeadersHint?.help || t('providerExtraHeadersHelp')}</p>
|
|
182
243
|
</div>
|
|
244
|
+
</div>
|
|
183
245
|
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
>
|
|
196
|
-
{updateProvider.isPending ? t('saving') : t('save')}
|
|
197
|
-
</Button>
|
|
198
|
-
</DialogFooter>
|
|
199
|
-
</form>
|
|
200
|
-
</DialogContent>
|
|
201
|
-
</Dialog>
|
|
246
|
+
<div className="flex flex-wrap items-center justify-between gap-3 border-t border-gray-100 px-6 py-4">
|
|
247
|
+
<Button type="button" variant="outline" onClick={resetToDefault}>
|
|
248
|
+
<RotateCcw className="mr-2 h-4 w-4" />
|
|
249
|
+
{t('resetToDefault')}
|
|
250
|
+
</Button>
|
|
251
|
+
<Button type="submit" disabled={updateProvider.isPending || !hasChanges}>
|
|
252
|
+
{updateProvider.isPending ? t('saving') : hasChanges ? t('save') : t('unchanged')}
|
|
253
|
+
</Button>
|
|
254
|
+
</div>
|
|
255
|
+
</form>
|
|
256
|
+
</div>
|
|
202
257
|
);
|
|
203
258
|
}
|