@nextclaw/ui 0.6.6 → 0.6.8
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-DH5fzlPu.js +1 -0
- package/dist/assets/ChatPage-BrLCnJSb.js +34 -0
- package/dist/assets/DocBrowser-DPQHJVsZ.js +1 -0
- package/dist/assets/LogoBadge-FEb4_vSq.js +1 -0
- package/dist/assets/{MarketplacePage--wFfsNH0.js → MarketplacePage-BAVXYeZA.js} +2 -2
- package/dist/assets/ModelConfig-BqPXe7nw.js +1 -0
- package/dist/assets/ProvidersList-vpKPuIxV.js +1 -0
- package/dist/assets/RuntimeConfig-DTYSU4_d.js +1 -0
- package/dist/assets/{SecretsConfig-B25P3J7V.js → SecretsConfig-nNzs3YDm.js} +2 -2
- package/dist/assets/SessionsConfig-CHjeyqEQ.js +2 -0
- package/dist/assets/{card-CCSDsedj.js → card-73MmEZi7.js} +1 -1
- package/dist/assets/index-CTLvVlk8.js +7 -0
- package/dist/assets/index-DI6BuShn.css +1 -0
- package/dist/assets/input-1MCMs6Yf.js +1 -0
- package/dist/assets/{label-BxzAKPzU.js → label-C4Q8RlBJ.js} +1 -1
- package/dist/assets/{page-layout-DaLNSFKw.js → page-layout-CK0vcVmV.js} +1 -1
- package/dist/assets/session-run-status-BaNlKvi6.js +5 -0
- package/dist/assets/{switch-DHOCEi5L.js → switch-Bf8w_cF1.js} +1 -1
- package/dist/assets/{tabs-custom-zdFy3fnK.js → tabs-custom-B6Gw8gax.js} +1 -1
- package/dist/assets/{useConfirmDialog-D3ZVa92J.js → useConfirmDialog-B5CZ4EDN.js} +1 -1
- package/dist/assets/{vendor-Dj2ULvht.js → vendor-C--HHaLf.js} +6 -6
- package/dist/index.html +3 -3
- package/package.json +1 -1
- package/src/api/config.ts +53 -0
- package/src/api/types.ts +48 -0
- package/src/components/chat/ChatInputBar.tsx +341 -24
- package/src/components/chat/ChatPage.tsx +28 -12
- package/src/components/chat/ChatSidebar.tsx +12 -7
- package/src/components/common/BrandHeader.tsx +23 -0
- package/src/components/common/SessionRunBadge.tsx +23 -0
- package/src/components/config/ProviderForm.tsx +193 -29
- package/src/components/config/ProvidersList.tsx +1 -2
- package/src/components/config/SessionsConfig.tsx +22 -2
- package/src/components/layout/Sidebar.tsx +2 -6
- package/src/hooks/useConfig.ts +31 -0
- package/src/lib/i18n.ts +28 -1
- package/src/lib/logos.ts +0 -19
- package/src/lib/session-run-status.ts +63 -0
- package/dist/assets/ChannelsList-VqzbAMCc.js +0 -1
- package/dist/assets/ChatPage-CjZqsBmn.js +0 -34
- package/dist/assets/DocBrowser-DvU-iUeB.js +0 -1
- package/dist/assets/ModelConfig-cY5UsbfA.js +0 -1
- package/dist/assets/ProvidersList-qZwaFoFt.js +0 -1
- package/dist/assets/RuntimeConfig-BY2Axlte.js +0 -1
- package/dist/assets/SessionsConfig-CxA9gIBw.js +0 -2
- package/dist/assets/chat-message-pw9oafI4.js +0 -5
- package/dist/assets/index-CD8a2KMH.js +0 -2
- package/dist/assets/index-DKOXGZc8.css +0 -1
- package/dist/assets/logos-C3oHQ9kv.js +0 -1
- package/dist/assets/useConfig-CDl9UK5m.js +0 -6
|
@@ -1,10 +1,13 @@
|
|
|
1
1
|
import { useMemo } from 'react';
|
|
2
2
|
import type { SessionEntryView } from '@/api/types';
|
|
3
3
|
import { Button } from '@/components/ui/button';
|
|
4
|
+
import { BrandHeader } from '@/components/common/BrandHeader';
|
|
4
5
|
import { Input } from '@/components/ui/input';
|
|
5
6
|
import { Select, SelectContent, SelectItem, SelectTrigger } from '@/components/ui/select';
|
|
7
|
+
import { SessionRunBadge } from '@/components/common/SessionRunBadge';
|
|
6
8
|
import { cn } from '@/lib/utils';
|
|
7
9
|
import { LANGUAGE_OPTIONS, formatDateTime, t, type I18nLanguage } from '@/lib/i18n';
|
|
10
|
+
import type { SessionRunStatus } from '@/lib/session-run-status';
|
|
8
11
|
import { THEME_OPTIONS, type UiTheme } from '@/lib/theme';
|
|
9
12
|
import { useI18n } from '@/components/providers/I18nProvider';
|
|
10
13
|
import { useTheme } from '@/components/providers/ThemeProvider';
|
|
@@ -13,6 +16,7 @@ import { AlarmClock, BrainCircuit, Languages, MessageSquareText, Palette, Plus,
|
|
|
13
16
|
|
|
14
17
|
type ChatSidebarProps = {
|
|
15
18
|
sessions: SessionEntryView[];
|
|
19
|
+
sessionRunStatusByKey: ReadonlyMap<string, SessionRunStatus>;
|
|
16
20
|
selectedSessionKey: string | null;
|
|
17
21
|
onSelectSession: (key: string) => void;
|
|
18
22
|
onCreateSession: () => void;
|
|
@@ -82,12 +86,7 @@ export function ChatSidebar(props: ChatSidebarProps) {
|
|
|
82
86
|
<aside className="w-[280px] shrink-0 flex flex-col h-full bg-secondary border-r border-gray-200/60">
|
|
83
87
|
{/* Logo */}
|
|
84
88
|
<div className="px-5 pt-5 pb-3">
|
|
85
|
-
<
|
|
86
|
-
<div className="h-7 w-7 rounded-lg overflow-hidden flex items-center justify-center">
|
|
87
|
-
<img src="/logo.svg" alt="NextClaw" className="h-full w-full object-contain" />
|
|
88
|
-
</div>
|
|
89
|
-
<span className="text-[15px] font-semibold text-gray-800 tracking-[-0.01em]">NextClaw</span>
|
|
90
|
-
</div>
|
|
89
|
+
<BrandHeader />
|
|
91
90
|
</div>
|
|
92
91
|
|
|
93
92
|
{/* New Task button */}
|
|
@@ -165,6 +164,7 @@ export function ChatSidebar(props: ChatSidebarProps) {
|
|
|
165
164
|
<div className="space-y-0.5">
|
|
166
165
|
{group.sessions.map((session) => {
|
|
167
166
|
const active = props.selectedSessionKey === session.key;
|
|
167
|
+
const runStatus = props.sessionRunStatusByKey.get(session.key);
|
|
168
168
|
return (
|
|
169
169
|
<button
|
|
170
170
|
key={session.key}
|
|
@@ -176,7 +176,12 @@ export function ChatSidebar(props: ChatSidebarProps) {
|
|
|
176
176
|
: 'text-gray-700 hover:bg-gray-200/60 hover:text-gray-900'
|
|
177
177
|
)}
|
|
178
178
|
>
|
|
179
|
-
<div className="
|
|
179
|
+
<div className="grid grid-cols-[minmax(0,1fr)_0.875rem] items-center gap-1.5">
|
|
180
|
+
<span className="truncate font-medium">{props.sessionTitle(session)}</span>
|
|
181
|
+
<span className="inline-flex h-3.5 w-3.5 shrink-0 items-center justify-center">
|
|
182
|
+
{runStatus ? <SessionRunBadge status={runStatus} /> : null}
|
|
183
|
+
</span>
|
|
184
|
+
</div>
|
|
180
185
|
<div className="mt-0.5 text-[11px] text-gray-400 truncate">
|
|
181
186
|
{session.messageCount} · {formatDateTime(session.updatedAt)}
|
|
182
187
|
</div>
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { useAppMeta } from '@/hooks/useConfig';
|
|
2
|
+
|
|
3
|
+
type BrandHeaderProps = {
|
|
4
|
+
className?: string;
|
|
5
|
+
};
|
|
6
|
+
|
|
7
|
+
export function BrandHeader({ className }: BrandHeaderProps) {
|
|
8
|
+
const { data } = useAppMeta();
|
|
9
|
+
const productName = data?.name ?? 'NextClaw';
|
|
10
|
+
const productVersion = data?.productVersion?.trim();
|
|
11
|
+
|
|
12
|
+
return (
|
|
13
|
+
<div className={className ?? 'flex items-center gap-2.5'}>
|
|
14
|
+
<div className="h-7 w-7 rounded-lg overflow-hidden flex items-center justify-center">
|
|
15
|
+
<img src="/logo.svg" alt={productName} className="h-full w-full object-contain" />
|
|
16
|
+
</div>
|
|
17
|
+
<div className="flex items-baseline gap-2 min-w-0">
|
|
18
|
+
<span className="truncate text-[15px] font-semibold tracking-[-0.01em] text-gray-800">{productName}</span>
|
|
19
|
+
{productVersion ? <span className="text-[13px] font-medium text-gray-500">v{productVersion}</span> : null}
|
|
20
|
+
</div>
|
|
21
|
+
</div>
|
|
22
|
+
);
|
|
23
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { Loader2 } from 'lucide-react';
|
|
2
|
+
import { cn } from '@/lib/utils';
|
|
3
|
+
import { t } from '@/lib/i18n';
|
|
4
|
+
import type { SessionRunStatus } from '@/lib/session-run-status';
|
|
5
|
+
|
|
6
|
+
type SessionRunBadgeProps = {
|
|
7
|
+
status: SessionRunStatus;
|
|
8
|
+
className?: string;
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
export function SessionRunBadge({ status, className }: SessionRunBadgeProps) {
|
|
12
|
+
const label = status === 'running' ? t('sessionsRunStatusRunning') : t('sessionsRunStatusQueued');
|
|
13
|
+
return (
|
|
14
|
+
<span
|
|
15
|
+
className={cn('inline-flex h-3.5 w-3.5 items-center justify-center text-gray-400', className)}
|
|
16
|
+
title={label}
|
|
17
|
+
aria-label={label}
|
|
18
|
+
>
|
|
19
|
+
<Loader2 className="h-3 w-3 animate-spin" />
|
|
20
|
+
<span className="sr-only">{label}</span>
|
|
21
|
+
</span>
|
|
22
|
+
);
|
|
23
|
+
}
|
|
@@ -1,9 +1,13 @@
|
|
|
1
|
-
import { useEffect, useMemo, useState } from 'react';
|
|
1
|
+
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
|
2
|
+
import { useQueryClient } from '@tanstack/react-query';
|
|
2
3
|
import {
|
|
3
4
|
useConfig,
|
|
4
5
|
useConfigMeta,
|
|
5
6
|
useConfigSchema,
|
|
6
7
|
useDeleteProvider,
|
|
8
|
+
useImportProviderAuthFromCli,
|
|
9
|
+
usePollProviderAuth,
|
|
10
|
+
useStartProviderAuth,
|
|
7
11
|
useTestProviderConnection,
|
|
8
12
|
useUpdateProvider
|
|
9
13
|
} from '@/hooks/useConfig';
|
|
@@ -14,9 +18,9 @@ import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@
|
|
|
14
18
|
import { MaskedInput } from '@/components/common/MaskedInput';
|
|
15
19
|
import { KeyValueEditor } from '@/components/common/KeyValueEditor';
|
|
16
20
|
import { StatusDot } from '@/components/ui/status-dot';
|
|
17
|
-
import { t } from '@/lib/i18n';
|
|
21
|
+
import { getLanguage, t } from '@/lib/i18n';
|
|
18
22
|
import { hintForPath } from '@/lib/config-hints';
|
|
19
|
-
import type { ProviderConfigUpdate, ProviderConnectionTestRequest } from '@/api/types';
|
|
23
|
+
import type { ProviderConfigView, ProviderConfigUpdate, ProviderConnectionTestRequest } from '@/api/types';
|
|
20
24
|
import { CircleDotDashed, Plus, X, Trash2, ChevronDown, Settings2 } from 'lucide-react';
|
|
21
25
|
import { toast } from 'sonner';
|
|
22
26
|
import { CONFIG_DETAIL_CARD_CLASS, CONFIG_EMPTY_DETAIL_CARD_CLASS } from './config-layout';
|
|
@@ -28,6 +32,16 @@ type ProviderFormProps = {
|
|
|
28
32
|
onProviderDeleted?: (providerName: string) => void;
|
|
29
33
|
};
|
|
30
34
|
|
|
35
|
+
const EMPTY_PROVIDER_CONFIG: ProviderConfigView = {
|
|
36
|
+
displayName: '',
|
|
37
|
+
apiKeySet: false,
|
|
38
|
+
apiKeyMasked: undefined,
|
|
39
|
+
apiBase: null,
|
|
40
|
+
extraHeaders: null,
|
|
41
|
+
wireApi: null,
|
|
42
|
+
models: []
|
|
43
|
+
};
|
|
44
|
+
|
|
31
45
|
function normalizeHeaders(input: Record<string, string> | null | undefined): Record<string, string> | null {
|
|
32
46
|
if (!input) {
|
|
33
47
|
return null;
|
|
@@ -138,12 +152,16 @@ function serializeModelsForSave(models: string[], defaultModels: string[]): stri
|
|
|
138
152
|
}
|
|
139
153
|
|
|
140
154
|
export function ProviderForm({ providerName, onProviderDeleted }: ProviderFormProps) {
|
|
155
|
+
const queryClient = useQueryClient();
|
|
141
156
|
const { data: config } = useConfig();
|
|
142
157
|
const { data: meta } = useConfigMeta();
|
|
143
158
|
const { data: schema } = useConfigSchema();
|
|
144
159
|
const updateProvider = useUpdateProvider();
|
|
145
160
|
const deleteProvider = useDeleteProvider();
|
|
146
161
|
const testProviderConnection = useTestProviderConnection();
|
|
162
|
+
const startProviderAuth = useStartProviderAuth();
|
|
163
|
+
const pollProviderAuth = usePollProviderAuth();
|
|
164
|
+
const importProviderAuthFromCli = useImportProviderAuthFromCli();
|
|
147
165
|
|
|
148
166
|
const [apiKey, setApiKey] = useState('');
|
|
149
167
|
const [apiBase, setApiBase] = useState('');
|
|
@@ -154,9 +172,13 @@ export function ProviderForm({ providerName, onProviderDeleted }: ProviderFormPr
|
|
|
154
172
|
const [providerDisplayName, setProviderDisplayName] = useState('');
|
|
155
173
|
const [showAdvanced, setShowAdvanced] = useState(false);
|
|
156
174
|
const [showModelInput, setShowModelInput] = useState(false);
|
|
175
|
+
const [authSessionId, setAuthSessionId] = useState<string | null>(null);
|
|
176
|
+
const [authStatusMessage, setAuthStatusMessage] = useState('');
|
|
177
|
+
const authPollTimerRef = useRef<number | null>(null);
|
|
157
178
|
|
|
158
179
|
const providerSpec = meta?.providers.find((p) => p.name === providerName);
|
|
159
180
|
const providerConfig = providerName ? config?.providers[providerName] : null;
|
|
181
|
+
const resolvedProviderConfig = providerConfig ?? EMPTY_PROVIDER_CONFIG;
|
|
160
182
|
const uiHints = schema?.uiHints;
|
|
161
183
|
const isCustomProvider = Boolean(providerSpec?.isCustom);
|
|
162
184
|
|
|
@@ -165,7 +187,7 @@ export function ProviderForm({ providerName, onProviderDeleted }: ProviderFormPr
|
|
|
165
187
|
const extraHeadersHint = providerName ? hintForPath(`providers.${providerName}.extraHeaders`, uiHints) : undefined;
|
|
166
188
|
const wireApiHint = providerName ? hintForPath(`providers.${providerName}.wireApi`, uiHints) : undefined;
|
|
167
189
|
const defaultDisplayName = providerSpec?.displayName || providerName || '';
|
|
168
|
-
const currentDisplayName = (
|
|
190
|
+
const currentDisplayName = (resolvedProviderConfig.displayName || '').trim();
|
|
169
191
|
const effectiveDisplayName = currentDisplayName || defaultDisplayName;
|
|
170
192
|
|
|
171
193
|
const providerTitle = providerDisplayName.trim() || effectiveDisplayName || providerName || t('providersSelectPlaceholder');
|
|
@@ -175,9 +197,9 @@ export function ProviderForm({ providerName, onProviderDeleted }: ProviderFormPr
|
|
|
175
197
|
[providerModelPrefix, providerName]
|
|
176
198
|
);
|
|
177
199
|
const defaultApiBase = providerSpec?.defaultApiBase || '';
|
|
178
|
-
const currentApiBase =
|
|
179
|
-
const currentHeaders = normalizeHeaders(
|
|
180
|
-
const currentWireApi = (
|
|
200
|
+
const currentApiBase = resolvedProviderConfig.apiBase || defaultApiBase;
|
|
201
|
+
const currentHeaders = normalizeHeaders(resolvedProviderConfig.extraHeaders || null);
|
|
202
|
+
const currentWireApi = (resolvedProviderConfig.wireApi || providerSpec?.defaultWireApi || 'auto') as WireApiType;
|
|
181
203
|
const defaultModels = useMemo(
|
|
182
204
|
() =>
|
|
183
205
|
normalizeModelList(
|
|
@@ -188,17 +210,74 @@ export function ProviderForm({ providerName, onProviderDeleted }: ProviderFormPr
|
|
|
188
210
|
const currentModels = useMemo(
|
|
189
211
|
() =>
|
|
190
212
|
normalizeModelList(
|
|
191
|
-
(
|
|
213
|
+
(resolvedProviderConfig.models ?? []).map((model) => toProviderLocalModelId(model, providerModelAliases))
|
|
192
214
|
),
|
|
193
|
-
[
|
|
215
|
+
[resolvedProviderConfig.models, providerModelAliases]
|
|
194
216
|
);
|
|
195
217
|
const currentEditableModels = useMemo(
|
|
196
218
|
() => resolveEditableModels(defaultModels, currentModels),
|
|
197
219
|
[defaultModels, currentModels]
|
|
198
220
|
);
|
|
199
|
-
const
|
|
200
|
-
|
|
201
|
-
|
|
221
|
+
const language = getLanguage();
|
|
222
|
+
const apiBaseHelpText =
|
|
223
|
+
providerSpec?.apiBaseHelp?.[language] ||
|
|
224
|
+
providerSpec?.apiBaseHelp?.en ||
|
|
225
|
+
apiBaseHint?.help ||
|
|
226
|
+
t('providerApiBaseHelp');
|
|
227
|
+
const providerAuth = providerSpec?.auth;
|
|
228
|
+
const providerAuthNote =
|
|
229
|
+
providerAuth?.note?.[language] ||
|
|
230
|
+
providerAuth?.note?.en ||
|
|
231
|
+
providerAuth?.displayName ||
|
|
232
|
+
'';
|
|
233
|
+
|
|
234
|
+
const clearAuthPollTimer = useCallback(() => {
|
|
235
|
+
if (authPollTimerRef.current !== null) {
|
|
236
|
+
window.clearTimeout(authPollTimerRef.current);
|
|
237
|
+
authPollTimerRef.current = null;
|
|
238
|
+
}
|
|
239
|
+
}, []);
|
|
240
|
+
|
|
241
|
+
const scheduleProviderAuthPoll = useCallback((sessionId: string, delayMs: number) => {
|
|
242
|
+
clearAuthPollTimer();
|
|
243
|
+
authPollTimerRef.current = window.setTimeout(() => {
|
|
244
|
+
void (async () => {
|
|
245
|
+
if (!providerName) {
|
|
246
|
+
return;
|
|
247
|
+
}
|
|
248
|
+
try {
|
|
249
|
+
const result = await pollProviderAuth.mutateAsync({
|
|
250
|
+
provider: providerName,
|
|
251
|
+
data: { sessionId }
|
|
252
|
+
});
|
|
253
|
+
if (result.status === 'pending') {
|
|
254
|
+
setAuthStatusMessage(t('providerAuthWaitingBrowser'));
|
|
255
|
+
scheduleProviderAuthPoll(sessionId, result.nextPollMs ?? delayMs);
|
|
256
|
+
return;
|
|
257
|
+
}
|
|
258
|
+
if (result.status === 'authorized') {
|
|
259
|
+
setAuthSessionId(null);
|
|
260
|
+
clearAuthPollTimer();
|
|
261
|
+
setAuthStatusMessage(t('providerAuthCompleted'));
|
|
262
|
+
toast.success(t('providerAuthCompleted'));
|
|
263
|
+
queryClient.invalidateQueries({ queryKey: ['config'] });
|
|
264
|
+
queryClient.invalidateQueries({ queryKey: ['config-meta'] });
|
|
265
|
+
return;
|
|
266
|
+
}
|
|
267
|
+
setAuthSessionId(null);
|
|
268
|
+
clearAuthPollTimer();
|
|
269
|
+
setAuthStatusMessage(result.message || `Authorization ${result.status}.`);
|
|
270
|
+
toast.error(result.message || `Authorization ${result.status}.`);
|
|
271
|
+
} catch (error) {
|
|
272
|
+
setAuthSessionId(null);
|
|
273
|
+
clearAuthPollTimer();
|
|
274
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
275
|
+
setAuthStatusMessage(message);
|
|
276
|
+
toast.error(`Authorization failed: ${message}`);
|
|
277
|
+
}
|
|
278
|
+
})();
|
|
279
|
+
}, Math.max(1000, delayMs));
|
|
280
|
+
}, [clearAuthPollTimer, pollProviderAuth, providerName, queryClient]);
|
|
202
281
|
|
|
203
282
|
useEffect(() => {
|
|
204
283
|
if (!providerName) {
|
|
@@ -209,17 +288,25 @@ export function ProviderForm({ providerName, onProviderDeleted }: ProviderFormPr
|
|
|
209
288
|
setModels([]);
|
|
210
289
|
setModelDraft('');
|
|
211
290
|
setProviderDisplayName('');
|
|
291
|
+
setAuthSessionId(null);
|
|
292
|
+
setAuthStatusMessage('');
|
|
293
|
+
clearAuthPollTimer();
|
|
212
294
|
return;
|
|
213
295
|
}
|
|
214
296
|
|
|
215
297
|
setApiKey('');
|
|
216
298
|
setApiBase(currentApiBase);
|
|
217
|
-
setExtraHeaders(
|
|
299
|
+
setExtraHeaders(resolvedProviderConfig.extraHeaders || null);
|
|
218
300
|
setWireApi(currentWireApi);
|
|
219
301
|
setModels(currentEditableModels);
|
|
220
302
|
setModelDraft('');
|
|
221
303
|
setProviderDisplayName(effectiveDisplayName);
|
|
222
|
-
|
|
304
|
+
setAuthSessionId(null);
|
|
305
|
+
setAuthStatusMessage('');
|
|
306
|
+
clearAuthPollTimer();
|
|
307
|
+
}, [providerName, currentApiBase, resolvedProviderConfig.extraHeaders, currentWireApi, currentEditableModels, effectiveDisplayName, clearAuthPollTimer]);
|
|
308
|
+
|
|
309
|
+
useEffect(() => () => clearAuthPollTimer(), [clearAuthPollTimer]);
|
|
223
310
|
|
|
224
311
|
const hasChanges = useMemo(() => {
|
|
225
312
|
if (!providerName) {
|
|
@@ -252,16 +339,6 @@ export function ProviderForm({ providerName, onProviderDeleted }: ProviderFormPr
|
|
|
252
339
|
currentEditableModels
|
|
253
340
|
]);
|
|
254
341
|
|
|
255
|
-
const resetToDefault = () => {
|
|
256
|
-
setApiKey('');
|
|
257
|
-
setApiBase(defaultApiBase);
|
|
258
|
-
setExtraHeaders(null);
|
|
259
|
-
setWireApi((providerSpec?.defaultWireApi || 'auto') as WireApiType);
|
|
260
|
-
setModels(defaultModels);
|
|
261
|
-
setModelDraft('');
|
|
262
|
-
setProviderDisplayName(defaultDisplayName);
|
|
263
|
-
};
|
|
264
|
-
|
|
265
342
|
const handleAddModel = () => {
|
|
266
343
|
const next = toProviderLocalModelId(modelDraft, providerModelAliases);
|
|
267
344
|
if (!next) {
|
|
@@ -369,7 +446,51 @@ export function ProviderForm({ providerName, onProviderDeleted }: ProviderFormPr
|
|
|
369
446
|
}
|
|
370
447
|
};
|
|
371
448
|
|
|
372
|
-
|
|
449
|
+
const handleStartProviderAuth = async () => {
|
|
450
|
+
if (!providerName || providerAuth?.kind !== 'device_code') {
|
|
451
|
+
return;
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
try {
|
|
455
|
+
setAuthStatusMessage('');
|
|
456
|
+
const result = await startProviderAuth.mutateAsync({ provider: providerName });
|
|
457
|
+
if (!result.sessionId || !result.verificationUri) {
|
|
458
|
+
throw new Error(t('providerAuthStartFailed'));
|
|
459
|
+
}
|
|
460
|
+
setAuthSessionId(result.sessionId);
|
|
461
|
+
setAuthStatusMessage(`${t('providerAuthOpenPrompt')}${result.userCode}${t('providerAuthOpenPromptSuffix')}`);
|
|
462
|
+
window.open(result.verificationUri, '_blank', 'noopener,noreferrer');
|
|
463
|
+
scheduleProviderAuthPoll(result.sessionId, result.intervalMs);
|
|
464
|
+
} catch (error) {
|
|
465
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
466
|
+
setAuthSessionId(null);
|
|
467
|
+
clearAuthPollTimer();
|
|
468
|
+
setAuthStatusMessage(message);
|
|
469
|
+
toast.error(`${t('providerAuthStartFailed')}: ${message}`);
|
|
470
|
+
}
|
|
471
|
+
};
|
|
472
|
+
|
|
473
|
+
const handleImportProviderAuthFromCli = async () => {
|
|
474
|
+
if (!providerName || providerAuth?.kind !== 'device_code') {
|
|
475
|
+
return;
|
|
476
|
+
}
|
|
477
|
+
try {
|
|
478
|
+
clearAuthPollTimer();
|
|
479
|
+
setAuthSessionId(null);
|
|
480
|
+
const result = await importProviderAuthFromCli.mutateAsync({ provider: providerName });
|
|
481
|
+
const expiresText = result.expiresAt ? ` (expires: ${result.expiresAt})` : '';
|
|
482
|
+
setAuthStatusMessage(`${t('providerAuthImportStatusPrefix')}${expiresText}`);
|
|
483
|
+
toast.success(t('providerAuthImportSuccess'));
|
|
484
|
+
queryClient.invalidateQueries({ queryKey: ['config'] });
|
|
485
|
+
queryClient.invalidateQueries({ queryKey: ['config-meta'] });
|
|
486
|
+
} catch (error) {
|
|
487
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
488
|
+
setAuthStatusMessage(message);
|
|
489
|
+
toast.error(`${t('providerAuthImportFailed')}: ${message}`);
|
|
490
|
+
}
|
|
491
|
+
};
|
|
492
|
+
|
|
493
|
+
if (!providerName || !providerSpec) {
|
|
373
494
|
return (
|
|
374
495
|
<div className={CONFIG_EMPTY_DETAIL_CARD_CLASS}>
|
|
375
496
|
<div>
|
|
@@ -380,7 +501,7 @@ export function ProviderForm({ providerName, onProviderDeleted }: ProviderFormPr
|
|
|
380
501
|
);
|
|
381
502
|
}
|
|
382
503
|
|
|
383
|
-
const statusLabel =
|
|
504
|
+
const statusLabel = resolvedProviderConfig.apiKeySet ? t('statusReady') : t('statusSetup');
|
|
384
505
|
|
|
385
506
|
return (
|
|
386
507
|
<div className={CONFIG_DETAIL_CARD_CLASS}>
|
|
@@ -399,7 +520,7 @@ export function ProviderForm({ providerName, onProviderDeleted }: ProviderFormPr
|
|
|
399
520
|
<Trash2 className="h-4 w-4" />
|
|
400
521
|
</button>
|
|
401
522
|
)}
|
|
402
|
-
<StatusDot status={
|
|
523
|
+
<StatusDot status={resolvedProviderConfig.apiKeySet ? 'ready' : 'setup'} label={statusLabel} />
|
|
403
524
|
</div>
|
|
404
525
|
</div>
|
|
405
526
|
</div>
|
|
@@ -430,7 +551,7 @@ export function ProviderForm({ providerName, onProviderDeleted }: ProviderFormPr
|
|
|
430
551
|
<MaskedInput
|
|
431
552
|
id="apiKey"
|
|
432
553
|
value={apiKey}
|
|
433
|
-
isSet={
|
|
554
|
+
isSet={resolvedProviderConfig.apiKeySet}
|
|
434
555
|
onChange={(e) => setApiKey(e.target.value)}
|
|
435
556
|
placeholder={apiKeyHint?.placeholder ?? t('enterApiKey')}
|
|
436
557
|
className="rounded-xl"
|
|
@@ -438,6 +559,49 @@ export function ProviderForm({ providerName, onProviderDeleted }: ProviderFormPr
|
|
|
438
559
|
<p className="text-xs text-gray-500">{t('leaveBlankToKeepUnchanged')}</p>
|
|
439
560
|
</div>
|
|
440
561
|
|
|
562
|
+
{providerAuth?.kind === 'device_code' && (
|
|
563
|
+
<div className="space-y-2 rounded-xl border border-primary/20 bg-primary-50/50 p-3">
|
|
564
|
+
<Label className="text-sm font-medium text-gray-900">
|
|
565
|
+
{providerAuth.displayName || t('providerAuthSectionTitle')}
|
|
566
|
+
</Label>
|
|
567
|
+
{providerAuthNote ? (
|
|
568
|
+
<p className="text-xs text-gray-600">{providerAuthNote}</p>
|
|
569
|
+
) : null}
|
|
570
|
+
<div className="flex flex-wrap items-center gap-2">
|
|
571
|
+
<Button
|
|
572
|
+
type="button"
|
|
573
|
+
variant="outline"
|
|
574
|
+
size="sm"
|
|
575
|
+
onClick={handleStartProviderAuth}
|
|
576
|
+
disabled={startProviderAuth.isPending || Boolean(authSessionId)}
|
|
577
|
+
>
|
|
578
|
+
{startProviderAuth.isPending
|
|
579
|
+
? t('providerAuthStarting')
|
|
580
|
+
: authSessionId
|
|
581
|
+
? t('providerAuthAuthorizing')
|
|
582
|
+
: t('providerAuthAuthorizeInBrowser')}
|
|
583
|
+
</Button>
|
|
584
|
+
{providerAuth.supportsCliImport ? (
|
|
585
|
+
<Button
|
|
586
|
+
type="button"
|
|
587
|
+
variant="outline"
|
|
588
|
+
size="sm"
|
|
589
|
+
onClick={handleImportProviderAuthFromCli}
|
|
590
|
+
disabled={importProviderAuthFromCli.isPending}
|
|
591
|
+
>
|
|
592
|
+
{importProviderAuthFromCli.isPending ? t('providerAuthImporting') : t('providerAuthImportFromCli')}
|
|
593
|
+
</Button>
|
|
594
|
+
) : null}
|
|
595
|
+
{authSessionId ? (
|
|
596
|
+
<span className="text-xs text-gray-500">{t('providerAuthSessionLabel')}: {authSessionId.slice(0, 8)}…</span>
|
|
597
|
+
) : null}
|
|
598
|
+
</div>
|
|
599
|
+
{authStatusMessage ? (
|
|
600
|
+
<p className="text-xs text-gray-600">{authStatusMessage}</p>
|
|
601
|
+
) : null}
|
|
602
|
+
</div>
|
|
603
|
+
)}
|
|
604
|
+
|
|
441
605
|
<div className="space-y-2">
|
|
442
606
|
<Label htmlFor="apiBase" className="text-sm font-medium text-gray-900">
|
|
443
607
|
{apiBaseHint?.label ?? t('apiBase')}
|
|
@@ -450,7 +614,7 @@ export function ProviderForm({ providerName, onProviderDeleted }: ProviderFormPr
|
|
|
450
614
|
placeholder={defaultApiBase || apiBaseHint?.placeholder || 'https://api.example.com'}
|
|
451
615
|
className="rounded-xl"
|
|
452
616
|
/>
|
|
453
|
-
<p className="text-xs text-gray-500">{
|
|
617
|
+
<p className="text-xs text-gray-500">{apiBaseHelpText}</p>
|
|
454
618
|
</div>
|
|
455
619
|
|
|
456
620
|
<div className="space-y-2">
|
|
@@ -5,7 +5,6 @@ import { ProviderForm } from './ProviderForm';
|
|
|
5
5
|
import { cn } from '@/lib/utils';
|
|
6
6
|
import { Tabs } from '@/components/ui/tabs-custom';
|
|
7
7
|
import { LogoBadge } from '@/components/common/LogoBadge';
|
|
8
|
-
import { getProviderLogo } from '@/lib/logos';
|
|
9
8
|
import { hintForPath } from '@/lib/config-hints';
|
|
10
9
|
import { StatusDot } from '@/components/ui/status-dot';
|
|
11
10
|
import { t } from '@/lib/i18n';
|
|
@@ -156,7 +155,7 @@ export function ProvidersList() {
|
|
|
156
155
|
<div className="flex min-w-0 items-center gap-3">
|
|
157
156
|
<LogoBadge
|
|
158
157
|
name={provider.name}
|
|
159
|
-
src={
|
|
158
|
+
src={provider.logo ? `/logos/${provider.logo}` : null}
|
|
160
159
|
className={cn(
|
|
161
160
|
'h-10 w-10 rounded-lg border',
|
|
162
161
|
isReady ? 'border-primary/30 bg-white' : 'border-gray-200/70 bg-white'
|
|
@@ -1,13 +1,19 @@
|
|
|
1
1
|
import { useEffect, useMemo, useState } from 'react';
|
|
2
2
|
import type { SessionEntryView, SessionMessageView } from '@/api/types';
|
|
3
3
|
import { useConfirmDialog } from '@/hooks/useConfirmDialog';
|
|
4
|
-
import { useDeleteSession, useSessionHistory, useSessions, useUpdateSession } from '@/hooks/useConfig';
|
|
4
|
+
import { useChatRuns, useDeleteSession, useSessionHistory, useSessions, useUpdateSession } from '@/hooks/useConfig';
|
|
5
5
|
import { Button } from '@/components/ui/button';
|
|
6
6
|
import { Input } from '@/components/ui/input';
|
|
7
7
|
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
|
|
8
|
+
import { SessionRunBadge } from '@/components/common/SessionRunBadge';
|
|
8
9
|
import { cn } from '@/lib/utils';
|
|
9
10
|
import { formatDateShort, formatDateTime, t } from '@/lib/i18n';
|
|
10
11
|
import { extractMessageText } from '@/lib/chat-message';
|
|
12
|
+
import {
|
|
13
|
+
buildActiveRunBySessionKey,
|
|
14
|
+
buildSessionRunStatusByKey,
|
|
15
|
+
type SessionRunStatus
|
|
16
|
+
} from '@/lib/session-run-status';
|
|
11
17
|
import { PageLayout, PageHeader } from '@/components/layout/page-layout';
|
|
12
18
|
import { RefreshCw, Search, Clock, Inbox, Hash, Bot, User, MessageCircle, Settings as SettingsIcon } from 'lucide-react';
|
|
13
19
|
|
|
@@ -40,11 +46,12 @@ function displayChannelName(channel: string): string {
|
|
|
40
46
|
type SessionListItemProps = {
|
|
41
47
|
session: SessionEntryView;
|
|
42
48
|
channel: string;
|
|
49
|
+
runStatus?: SessionRunStatus;
|
|
43
50
|
isSelected: boolean;
|
|
44
51
|
onSelect: () => void;
|
|
45
52
|
};
|
|
46
53
|
|
|
47
|
-
function SessionListItem({ session, channel, isSelected, onSelect }: SessionListItemProps) {
|
|
54
|
+
function SessionListItem({ session, channel, runStatus, isSelected, onSelect }: SessionListItemProps) {
|
|
48
55
|
const channelDisplay = displayChannelName(channel);
|
|
49
56
|
const displayName = session.label || session.key.split(':').pop() || session.key;
|
|
50
57
|
|
|
@@ -69,6 +76,9 @@ function SessionListItem({ session, channel, isSelected, onSelect }: SessionList
|
|
|
69
76
|
|
|
70
77
|
<div className={cn("flex items-center text-xs justify-between mt-2 font-medium", isSelected ? "text-brand-600/80" : "text-gray-400")}>
|
|
71
78
|
<div className="flex items-center gap-1.5">
|
|
79
|
+
<span className="inline-flex h-3.5 w-3.5 shrink-0 items-center justify-center">
|
|
80
|
+
{runStatus ? <SessionRunBadge status={runStatus} /> : null}
|
|
81
|
+
</span>
|
|
72
82
|
<Clock className="w-3.5 h-3.5 opacity-70" />
|
|
73
83
|
<span className="truncate max-w-[100px]">{formatDateShort(session.updatedAt)}</span>
|
|
74
84
|
</div>
|
|
@@ -136,6 +146,7 @@ export function SessionsConfig() {
|
|
|
136
146
|
|
|
137
147
|
const sessionsParams = useMemo(() => ({ q: query.trim() || undefined, limit, activeMinutes }), [query, limit, activeMinutes]);
|
|
138
148
|
const sessionsQuery = useSessions(sessionsParams);
|
|
149
|
+
const activeRunsQuery = useChatRuns({ states: ['queued', 'running'], limit: 200 });
|
|
139
150
|
const historyQuery = useSessionHistory(selectedKey, 200);
|
|
140
151
|
|
|
141
152
|
const updateSession = useUpdateSession();
|
|
@@ -143,6 +154,14 @@ export function SessionsConfig() {
|
|
|
143
154
|
const { confirm, ConfirmDialog } = useConfirmDialog();
|
|
144
155
|
|
|
145
156
|
const sessions = useMemo(() => sessionsQuery.data?.sessions ?? [], [sessionsQuery.data?.sessions]);
|
|
157
|
+
const activeRunBySessionKey = useMemo(
|
|
158
|
+
() => buildActiveRunBySessionKey(activeRunsQuery.data?.runs ?? []),
|
|
159
|
+
[activeRunsQuery.data?.runs]
|
|
160
|
+
);
|
|
161
|
+
const sessionRunStatusByKey = useMemo(
|
|
162
|
+
() => buildSessionRunStatusByKey(activeRunBySessionKey),
|
|
163
|
+
[activeRunBySessionKey]
|
|
164
|
+
);
|
|
146
165
|
const selectedSession = useMemo(() => sessions.find(s => s.key === selectedKey), [sessions, selectedKey]);
|
|
147
166
|
|
|
148
167
|
const channels = useMemo(() => {
|
|
@@ -273,6 +292,7 @@ export function SessionsConfig() {
|
|
|
273
292
|
key={session.key}
|
|
274
293
|
session={session}
|
|
275
294
|
channel={resolveChannelFromSessionKey(session.key)}
|
|
295
|
+
runStatus={sessionRunStatusByKey.get(session.key)}
|
|
276
296
|
isSelected={selectedKey === session.key}
|
|
277
297
|
onSelect={() => setSelectedKey(session.key)}
|
|
278
298
|
/>
|
|
@@ -4,6 +4,7 @@ import { THEME_OPTIONS, type UiTheme } from '@/lib/theme';
|
|
|
4
4
|
import { Cpu, GitBranch, History, MessageCircle, MessageSquare, Sparkles, BookOpen, Plug, BrainCircuit, AlarmClock, Languages, Palette, KeyRound, Settings, ArrowLeft } from 'lucide-react';
|
|
5
5
|
import { NavLink } from 'react-router-dom';
|
|
6
6
|
import { useDocBrowser } from '@/components/doc-browser';
|
|
7
|
+
import { BrandHeader } from '@/components/common/BrandHeader';
|
|
7
8
|
import { useI18n } from '@/components/providers/I18nProvider';
|
|
8
9
|
import { useTheme } from '@/components/providers/ThemeProvider';
|
|
9
10
|
import { Select, SelectContent, SelectItem, SelectTrigger } from '@/components/ui/select';
|
|
@@ -114,12 +115,7 @@ export function Sidebar({ mode }: SidebarProps) {
|
|
|
114
115
|
</div>
|
|
115
116
|
) : (
|
|
116
117
|
<div className="px-2 mb-8">
|
|
117
|
-
<
|
|
118
|
-
<div className="h-7 w-7 rounded-lg overflow-hidden flex items-center justify-center">
|
|
119
|
-
<img src="/logo.svg" alt="NextClaw" className="h-full w-full object-contain" />
|
|
120
|
-
</div>
|
|
121
|
-
<span className="text-[15px] font-semibold text-gray-800 tracking-[-0.01em]">NextClaw</span>
|
|
122
|
-
</div>
|
|
118
|
+
<BrandHeader className="flex items-center gap-2.5 cursor-pointer" />
|
|
123
119
|
</div>
|
|
124
120
|
)}
|
|
125
121
|
|
package/src/hooks/useConfig.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
|
|
2
2
|
import {
|
|
3
|
+
fetchAppMeta,
|
|
3
4
|
fetchConfig,
|
|
4
5
|
fetchConfigMeta,
|
|
5
6
|
fetchConfigSchema,
|
|
@@ -8,6 +9,9 @@ import {
|
|
|
8
9
|
deleteProvider,
|
|
9
10
|
updateProvider,
|
|
10
11
|
testProviderConnection,
|
|
12
|
+
startProviderAuth,
|
|
13
|
+
pollProviderAuth,
|
|
14
|
+
importProviderAuthFromCli,
|
|
11
15
|
updateChannel,
|
|
12
16
|
updateRuntime,
|
|
13
17
|
updateSecrets,
|
|
@@ -37,6 +41,14 @@ export function useConfig() {
|
|
|
37
41
|
});
|
|
38
42
|
}
|
|
39
43
|
|
|
44
|
+
export function useAppMeta() {
|
|
45
|
+
return useQuery({
|
|
46
|
+
queryKey: ['app-meta'],
|
|
47
|
+
queryFn: fetchAppMeta,
|
|
48
|
+
staleTime: Infinity
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
|
|
40
52
|
export function useConfigMeta() {
|
|
41
53
|
return useQuery({
|
|
42
54
|
queryKey: ['config-meta'],
|
|
@@ -125,6 +137,25 @@ export function useTestProviderConnection() {
|
|
|
125
137
|
});
|
|
126
138
|
}
|
|
127
139
|
|
|
140
|
+
export function useStartProviderAuth() {
|
|
141
|
+
return useMutation({
|
|
142
|
+
mutationFn: ({ provider }: { provider: string }) => startProviderAuth(provider)
|
|
143
|
+
});
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
export function usePollProviderAuth() {
|
|
147
|
+
return useMutation({
|
|
148
|
+
mutationFn: ({ provider, data }: { provider: string; data: unknown }) =>
|
|
149
|
+
pollProviderAuth(provider, data as Parameters<typeof pollProviderAuth>[1])
|
|
150
|
+
});
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
export function useImportProviderAuthFromCli() {
|
|
154
|
+
return useMutation({
|
|
155
|
+
mutationFn: ({ provider }: { provider: string }) => importProviderAuthFromCli(provider)
|
|
156
|
+
});
|
|
157
|
+
}
|
|
158
|
+
|
|
128
159
|
export function useUpdateChannel() {
|
|
129
160
|
const queryClient = useQueryClient();
|
|
130
161
|
|