@nextclaw/ui 0.6.9 → 0.6.11
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/.eslintrc.cjs +10 -0
- package/CHANGELOG.md +15 -0
- package/dist/assets/{ChannelsList-DACqpUYZ.js → ChannelsList-C49JQ-Zt.js} +1 -1
- package/dist/assets/ChatPage-DIx05c6s.js +36 -0
- package/dist/assets/{DocBrowser-D7mjKkGe.js → DocBrowser-CpOosDEI.js} +1 -1
- package/dist/assets/{LogoBadge-BlDT-g9R.js → LogoBadge-CL_8ZPXU.js} +1 -1
- package/dist/assets/MarketplacePage-BOzko5s9.js +49 -0
- package/dist/assets/{ModelConfig-DwRU5qrw.js → ModelConfig-BZ4ZfaQB.js} +1 -1
- package/dist/assets/ProvidersList-fPpJ5gl6.js +1 -0
- package/dist/assets/{RuntimeConfig-C7BRLGSC.js → RuntimeConfig-Dt9pLB9P.js} +1 -1
- package/dist/assets/{SecretsConfig-D5xZh7VF.js → SecretsConfig-C1PU0Yy8.js} +2 -2
- package/dist/assets/{SessionsConfig-ovpj_otA.js → SessionsConfig-EskBOofQ.js} +2 -2
- package/dist/assets/{card-Bf4CtrW8.js → card-C7Gtw2Vs.js} +1 -1
- package/dist/assets/index-Cn6_2To7.js +8 -0
- package/dist/assets/index-nEYGCJTC.css +1 -0
- package/dist/assets/{input-CaKJyoWZ.js → input-oBvxsnV9.js} +1 -1
- package/dist/assets/{label-BaXSWTKI.js → label-C7F8lMpQ.js} +1 -1
- package/dist/assets/{page-layout-DA6PFRtQ.js → page-layout-DO8BlScF.js} +1 -1
- package/dist/assets/session-run-status-Kg0FwAPn.js +3 -0
- package/dist/assets/{switch-Cvd5wZs-.js → switch-C6a5GyZB.js} +1 -1
- package/dist/assets/{tabs-custom-0PybLkXs.js → tabs-custom-BatFap5k.js} +1 -1
- package/dist/assets/{useConfirmDialog-DdtpSju1.js → useConfirmDialog-zJzVKMdu.js} +2 -2
- package/dist/assets/{vendor-C--HHaLf.js → vendor-TlME1INH.js} +84 -84
- package/dist/index.html +3 -3
- package/package.json +4 -2
- package/src/App.tsx +1 -2
- package/src/api/config.ts +205 -202
- package/src/api/types.ts +54 -24
- package/src/components/chat/ChatConversationPanel.tsx +102 -121
- package/src/components/chat/ChatPage.tsx +165 -437
- package/src/components/chat/ChatSidebar.tsx +30 -36
- package/src/components/chat/ChatThread.tsx +73 -131
- package/src/components/chat/chat-input/ChatInputBarView.tsx +82 -0
- package/src/components/chat/chat-input/ChatInputBottomToolbar.tsx +71 -0
- package/src/components/chat/chat-input/components/ChatInputModelStateHint.tsx +39 -0
- package/src/components/chat/chat-input/components/ChatInputSelectedSkillsSection.tsx +31 -0
- package/src/components/chat/chat-input/components/ChatInputSlashPanelSection.tsx +112 -0
- package/src/components/chat/chat-input/components/bottom-toolbar/ChatInputAttachButton.tsx +24 -0
- package/src/components/chat/chat-input/components/bottom-toolbar/ChatInputModelSelector.tsx +58 -0
- package/src/components/chat/chat-input/components/bottom-toolbar/ChatInputSendControls.tsx +56 -0
- package/src/components/chat/chat-input/components/bottom-toolbar/ChatInputSessionTypeSelector.tsx +40 -0
- package/src/components/chat/chat-input/useChatInputBarController.ts +313 -0
- package/src/components/chat/chat-input.types.ts +15 -0
- package/src/components/chat/chat-page-data.ts +121 -0
- package/src/components/chat/chat-page-runtime.ts +221 -0
- package/src/components/chat/chat-session-route.ts +59 -0
- package/src/components/chat/chat-stream/nextbot-parsers.ts +52 -0
- package/src/components/chat/chat-stream/nextbot-runtime-agent.ts +413 -0
- package/src/components/chat/chat-stream/stream-event-adapter.ts +98 -0
- package/src/components/chat/chat-stream/transport.ts +159 -0
- package/src/components/chat/chat-stream/types.ts +76 -0
- package/src/components/chat/managers/chat-input.manager.ts +142 -0
- package/src/components/chat/managers/chat-run-status.manager.ts +32 -0
- package/src/components/chat/managers/chat-session-list.manager.ts +77 -0
- package/src/components/chat/managers/chat-stream-actions.manager.ts +34 -0
- package/src/components/chat/managers/chat-thread.manager.ts +86 -0
- package/src/components/chat/managers/chat-ui.manager.ts +65 -0
- package/src/components/chat/presenter/chat-presenter-context.tsx +25 -0
- package/src/components/chat/presenter/chat.presenter.ts +32 -0
- package/src/components/chat/stores/chat-input.store.ts +62 -0
- package/src/components/chat/stores/chat-run-status.store.ts +30 -0
- package/src/components/chat/stores/chat-session-list.store.ts +34 -0
- package/src/components/chat/stores/chat-thread.store.ts +52 -0
- package/src/components/chat/useChatRuntimeController.ts +134 -0
- package/src/components/chat/useChatSessionTypeState.ts +148 -0
- package/src/components/common/MaskedInput.tsx +1 -1
- package/src/components/config/ProviderForm.tsx +221 -14
- package/src/hooks/useConfig.ts +33 -2
- package/src/hooks/useObservable.ts +20 -0
- package/src/hooks/useWebSocket.ts +23 -1
- package/src/lib/chat-message.ts +2 -202
- package/src/lib/chat-runtime-utils.ts +250 -0
- package/src/lib/i18n.ts +11 -0
- package/tsconfig.json +2 -1
- package/vite.config.ts +2 -1
- package/dist/assets/ChatPage-iji0RkTR.js +0 -34
- package/dist/assets/MarketplacePage-CZq3jVgg.js +0 -49
- package/dist/assets/ProvidersList-DFxN3pjx.js +0 -1
- package/dist/assets/index-C_DhisNo.css +0 -1
- package/dist/assets/index-dKTqKCJo.js +0 -7
- package/dist/assets/session-run-status-CllIZxNf.js +0 -5
- package/src/components/chat/ChatInputBar.tsx +0 -590
- package/src/components/chat/useChatStreamController.ts +0 -591
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { create } from 'zustand';
|
|
2
|
+
import type { SessionEntryView } from '@/api/types';
|
|
3
|
+
|
|
4
|
+
export type ChatSessionListSnapshot = {
|
|
5
|
+
sessions: SessionEntryView[];
|
|
6
|
+
selectedSessionKey: string | null;
|
|
7
|
+
selectedAgentId: string;
|
|
8
|
+
query: string;
|
|
9
|
+
isLoading: boolean;
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
type ChatSessionListStore = {
|
|
13
|
+
snapshot: ChatSessionListSnapshot;
|
|
14
|
+
setSnapshot: (patch: Partial<ChatSessionListSnapshot>) => void;
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
const initialSnapshot: ChatSessionListSnapshot = {
|
|
18
|
+
sessions: [],
|
|
19
|
+
selectedSessionKey: null,
|
|
20
|
+
selectedAgentId: 'main',
|
|
21
|
+
query: '',
|
|
22
|
+
isLoading: false
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
export const useChatSessionListStore = create<ChatSessionListStore>((set) => ({
|
|
26
|
+
snapshot: initialSnapshot,
|
|
27
|
+
setSnapshot: (patch) =>
|
|
28
|
+
set((state) => ({
|
|
29
|
+
snapshot: {
|
|
30
|
+
...state.snapshot,
|
|
31
|
+
...patch
|
|
32
|
+
}
|
|
33
|
+
}))
|
|
34
|
+
}));
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { create } from 'zustand';
|
|
2
|
+
import type { MutableRefObject } from 'react';
|
|
3
|
+
import type { UiMessage } from '@nextclaw/agent-chat';
|
|
4
|
+
import type { ChatModelOption } from '@/components/chat/chat-input.types';
|
|
5
|
+
|
|
6
|
+
export type ChatThreadSnapshot = {
|
|
7
|
+
isProviderStateResolved: boolean;
|
|
8
|
+
modelOptions: ChatModelOption[];
|
|
9
|
+
sessionTypeUnavailable: boolean;
|
|
10
|
+
sessionTypeUnavailableMessage?: string | null;
|
|
11
|
+
selectedSessionKey: string | null;
|
|
12
|
+
sessionDisplayName?: string;
|
|
13
|
+
canDeleteSession: boolean;
|
|
14
|
+
isDeletePending: boolean;
|
|
15
|
+
threadRef: MutableRefObject<HTMLDivElement | null> | null;
|
|
16
|
+
isHistoryLoading: boolean;
|
|
17
|
+
uiMessages: UiMessage[];
|
|
18
|
+
isSending: boolean;
|
|
19
|
+
isAwaitingAssistantOutput: boolean;
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
type ChatThreadStore = {
|
|
23
|
+
snapshot: ChatThreadSnapshot;
|
|
24
|
+
setSnapshot: (patch: Partial<ChatThreadSnapshot>) => void;
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
const initialSnapshot: ChatThreadSnapshot = {
|
|
28
|
+
isProviderStateResolved: false,
|
|
29
|
+
modelOptions: [],
|
|
30
|
+
sessionTypeUnavailable: false,
|
|
31
|
+
sessionTypeUnavailableMessage: null,
|
|
32
|
+
selectedSessionKey: null,
|
|
33
|
+
sessionDisplayName: undefined,
|
|
34
|
+
canDeleteSession: false,
|
|
35
|
+
isDeletePending: false,
|
|
36
|
+
threadRef: null,
|
|
37
|
+
isHistoryLoading: false,
|
|
38
|
+
uiMessages: [],
|
|
39
|
+
isSending: false,
|
|
40
|
+
isAwaitingAssistantOutput: false
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
export const useChatThreadStore = create<ChatThreadStore>((set) => ({
|
|
44
|
+
snapshot: initialSnapshot,
|
|
45
|
+
setSnapshot: (patch) =>
|
|
46
|
+
set((state) => ({
|
|
47
|
+
snapshot: {
|
|
48
|
+
...state.snapshot,
|
|
49
|
+
...patch
|
|
50
|
+
}
|
|
51
|
+
}))
|
|
52
|
+
}));
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
import { useCallback, useEffect, useRef } from 'react';
|
|
2
|
+
import { type AgentChatController, getStopDisabledReason } from '@nextclaw/agent-chat';
|
|
3
|
+
import type { ChatRunView, SessionMessageView } from '@/api/types';
|
|
4
|
+
import type { SendMessageParams, UseChatStreamControllerParams } from '@/components/chat/chat-stream/types';
|
|
5
|
+
import { buildResumeMetadata, buildSendMetadata } from '@/components/chat/chat-stream/nextbot-parsers';
|
|
6
|
+
import { buildUiMessagesFromHistoryMessages, normalizeRequestedSkills } from '@/lib/chat-runtime-utils';
|
|
7
|
+
import { useValueFromBehaviorSubject, useValueFromObservable } from '@/hooks/useObservable';
|
|
8
|
+
|
|
9
|
+
export function useChatRuntimeController(
|
|
10
|
+
params: UseChatStreamControllerParams,
|
|
11
|
+
controller: AgentChatController
|
|
12
|
+
) {
|
|
13
|
+
const paramsRef = useRef(params);
|
|
14
|
+
useEffect(() => {
|
|
15
|
+
paramsRef.current = params;
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
const activeHistorySessionKeyRef = useRef<string | null>(null);
|
|
19
|
+
|
|
20
|
+
// Bind callbacks to controller
|
|
21
|
+
useEffect(() => {
|
|
22
|
+
controller.setCallbacks({
|
|
23
|
+
onRunSettled: async ({ sourceSessionId, resultSessionId }) => {
|
|
24
|
+
const bindings = paramsRef.current;
|
|
25
|
+
await bindings.refetchSessions();
|
|
26
|
+
const activeSessionKey = bindings.selectedSessionKeyRef.current;
|
|
27
|
+
if (!activeSessionKey || activeSessionKey === sourceSessionId || (resultSessionId && activeSessionKey === resultSessionId)) {
|
|
28
|
+
await bindings.refetchHistory();
|
|
29
|
+
}
|
|
30
|
+
},
|
|
31
|
+
onRunError: ({ sourceMessage, restoreDraft }) => {
|
|
32
|
+
if (restoreDraft) {
|
|
33
|
+
paramsRef.current.setDraft((prev) => (prev.trim().length === 0 && sourceMessage ? sourceMessage : prev));
|
|
34
|
+
}
|
|
35
|
+
},
|
|
36
|
+
onSessionChanged: (sessionId) => {
|
|
37
|
+
paramsRef.current.setSelectedSessionKey((prev) => (prev === sessionId ? prev : sessionId));
|
|
38
|
+
}
|
|
39
|
+
});
|
|
40
|
+
}, [controller]);
|
|
41
|
+
|
|
42
|
+
// Reactive state from controller observables
|
|
43
|
+
const uiMessages = useValueFromObservable(controller.messages$, controller.getMessages());
|
|
44
|
+
const isSending = useValueFromBehaviorSubject(controller.isAgentResponding$);
|
|
45
|
+
const isAwaitingAssistantOutput = useValueFromBehaviorSubject(controller.isAwaitingResponse$);
|
|
46
|
+
const activeRun = useValueFromBehaviorSubject(controller.activeRun$);
|
|
47
|
+
const lastSendError = useValueFromBehaviorSubject(controller.lastError$);
|
|
48
|
+
|
|
49
|
+
// Derived state
|
|
50
|
+
const activeBackendRunId = activeRun?.remoteRunId ?? null;
|
|
51
|
+
const stopDisabledReason = getStopDisabledReason(activeRun);
|
|
52
|
+
const canStopCurrentRun = Boolean(
|
|
53
|
+
activeRun && (stopDisabledReason === null || (activeRun.remoteStopCapable && !activeBackendRunId))
|
|
54
|
+
);
|
|
55
|
+
|
|
56
|
+
const sendMessage = useCallback(async (payload: SendMessageParams) => {
|
|
57
|
+
const requestedSkills = normalizeRequestedSkills(payload.requestedSkills);
|
|
58
|
+
const metadata = buildSendMetadata(payload, requestedSkills);
|
|
59
|
+
await controller.send({
|
|
60
|
+
message: payload.message,
|
|
61
|
+
sessionId: payload.sessionKey,
|
|
62
|
+
agentId: payload.agentId,
|
|
63
|
+
metadata,
|
|
64
|
+
restoreDraftOnError: payload.restoreDraftOnError,
|
|
65
|
+
stopCapable: payload.stopSupported,
|
|
66
|
+
stopReason: payload.stopReason
|
|
67
|
+
});
|
|
68
|
+
}, [controller]);
|
|
69
|
+
|
|
70
|
+
const resumeRun = useCallback(async (run: ChatRunView) => {
|
|
71
|
+
const backendRunId = run.runId?.trim();
|
|
72
|
+
const sessionKey = run.sessionKey?.trim();
|
|
73
|
+
if (!backendRunId || !sessionKey) {
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
const metadata = buildResumeMetadata(run);
|
|
77
|
+
await controller.resume({
|
|
78
|
+
remoteRunId: backendRunId,
|
|
79
|
+
sessionId: sessionKey,
|
|
80
|
+
agentId: run.agentId,
|
|
81
|
+
metadata,
|
|
82
|
+
stopCapable: run.stopSupported,
|
|
83
|
+
stopReason: run.stopReason
|
|
84
|
+
});
|
|
85
|
+
}, [controller]);
|
|
86
|
+
|
|
87
|
+
const stopCurrentRun = useCallback(async () => {
|
|
88
|
+
await controller.stop();
|
|
89
|
+
}, [controller]);
|
|
90
|
+
|
|
91
|
+
const resetStreamState = useCallback(() => {
|
|
92
|
+
activeHistorySessionKeyRef.current = null;
|
|
93
|
+
controller.reset();
|
|
94
|
+
}, [controller]);
|
|
95
|
+
|
|
96
|
+
const applyHistoryMessages = useCallback((messages: SessionMessageView[], options?: { isLoading?: boolean }) => {
|
|
97
|
+
const isRunActive = Boolean(controller.activeRun$.getValue() || controller.isAgentResponding$.getValue());
|
|
98
|
+
if (isRunActive) {
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
101
|
+
const selectedSessionKey = paramsRef.current.selectedSessionKeyRef.current;
|
|
102
|
+
if (selectedSessionKey !== activeHistorySessionKeyRef.current) {
|
|
103
|
+
activeHistorySessionKeyRef.current = selectedSessionKey;
|
|
104
|
+
if (controller.getMessages().length > 0) {
|
|
105
|
+
controller.setMessages([]);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
if (!selectedSessionKey) {
|
|
109
|
+
if (controller.getMessages().length > 0) {
|
|
110
|
+
controller.setMessages([]);
|
|
111
|
+
}
|
|
112
|
+
return;
|
|
113
|
+
}
|
|
114
|
+
if (options?.isLoading && messages.length === 0) {
|
|
115
|
+
return;
|
|
116
|
+
}
|
|
117
|
+
controller.setMessages(buildUiMessagesFromHistoryMessages(messages));
|
|
118
|
+
}, [controller]);
|
|
119
|
+
|
|
120
|
+
return {
|
|
121
|
+
uiMessages,
|
|
122
|
+
isSending,
|
|
123
|
+
isAwaitingAssistantOutput,
|
|
124
|
+
canStopCurrentRun,
|
|
125
|
+
stopDisabledReason,
|
|
126
|
+
lastSendError,
|
|
127
|
+
activeBackendRunId,
|
|
128
|
+
sendMessage,
|
|
129
|
+
stopCurrentRun,
|
|
130
|
+
resumeRun,
|
|
131
|
+
resetStreamState,
|
|
132
|
+
applyHistoryMessages
|
|
133
|
+
};
|
|
134
|
+
}
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
import { useEffect, useMemo } from 'react';
|
|
2
|
+
import type { Dispatch, SetStateAction } from 'react';
|
|
3
|
+
import type { SessionEntryView } from '@/api/types';
|
|
4
|
+
import { t } from '@/lib/i18n';
|
|
5
|
+
|
|
6
|
+
export const DEFAULT_SESSION_TYPE = 'native';
|
|
7
|
+
|
|
8
|
+
export type ChatSessionTypeOption = {
|
|
9
|
+
value: string;
|
|
10
|
+
label: string;
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
type UseChatSessionTypeStateParams = {
|
|
14
|
+
selectedSession: SessionEntryView | null;
|
|
15
|
+
selectedSessionKey: string | null;
|
|
16
|
+
pendingSessionType: string;
|
|
17
|
+
setPendingSessionType: Dispatch<SetStateAction<string>>;
|
|
18
|
+
sessionTypesData?: {
|
|
19
|
+
defaultType?: string;
|
|
20
|
+
options?: Array<{ value: string; label: string }>;
|
|
21
|
+
} | null;
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
export function normalizeSessionType(value: unknown): string {
|
|
25
|
+
if (typeof value !== 'string') {
|
|
26
|
+
return DEFAULT_SESSION_TYPE;
|
|
27
|
+
}
|
|
28
|
+
const normalized = value.trim().toLowerCase();
|
|
29
|
+
return normalized || DEFAULT_SESSION_TYPE;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function resolveSessionTypeLabel(sessionType: string, fallbackLabel?: string): string {
|
|
33
|
+
if (sessionType === 'native') {
|
|
34
|
+
return t('chatSessionTypeNative');
|
|
35
|
+
}
|
|
36
|
+
if (sessionType === 'codex-sdk') {
|
|
37
|
+
return t('chatSessionTypeCodex');
|
|
38
|
+
}
|
|
39
|
+
if (sessionType === 'claude-agent-sdk') {
|
|
40
|
+
return t('chatSessionTypeClaude');
|
|
41
|
+
}
|
|
42
|
+
return fallbackLabel?.trim() || sessionType;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function buildSessionTypeOptions(
|
|
46
|
+
options: Array<{ value: string; label: string }>
|
|
47
|
+
): ChatSessionTypeOption[] {
|
|
48
|
+
const deduped = new Map<string, ChatSessionTypeOption>();
|
|
49
|
+
for (const option of options) {
|
|
50
|
+
const value = normalizeSessionType(option.value);
|
|
51
|
+
deduped.set(value, {
|
|
52
|
+
value,
|
|
53
|
+
label: option.label?.trim() || resolveSessionTypeLabel(value)
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
if (!deduped.has(DEFAULT_SESSION_TYPE)) {
|
|
57
|
+
deduped.set(DEFAULT_SESSION_TYPE, {
|
|
58
|
+
value: DEFAULT_SESSION_TYPE,
|
|
59
|
+
label: resolveSessionTypeLabel(DEFAULT_SESSION_TYPE)
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
return Array.from(deduped.values()).sort((left, right) => {
|
|
63
|
+
if (left.value === DEFAULT_SESSION_TYPE) {
|
|
64
|
+
return -1;
|
|
65
|
+
}
|
|
66
|
+
if (right.value === DEFAULT_SESSION_TYPE) {
|
|
67
|
+
return 1;
|
|
68
|
+
}
|
|
69
|
+
return left.value.localeCompare(right.value);
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export function useChatSessionTypeState(params: UseChatSessionTypeStateParams): {
|
|
74
|
+
sessionTypeOptions: ChatSessionTypeOption[];
|
|
75
|
+
defaultSessionType: string;
|
|
76
|
+
selectedSessionType: string;
|
|
77
|
+
canEditSessionType: boolean;
|
|
78
|
+
sessionTypeUnavailable: boolean;
|
|
79
|
+
sessionTypeUnavailableMessage: string | null;
|
|
80
|
+
} {
|
|
81
|
+
const {
|
|
82
|
+
selectedSession,
|
|
83
|
+
selectedSessionKey,
|
|
84
|
+
pendingSessionType,
|
|
85
|
+
setPendingSessionType,
|
|
86
|
+
sessionTypesData
|
|
87
|
+
} = params;
|
|
88
|
+
|
|
89
|
+
const runtimeSessionTypeOptions = useMemo(
|
|
90
|
+
() => buildSessionTypeOptions(sessionTypesData?.options ?? []),
|
|
91
|
+
[sessionTypesData?.options]
|
|
92
|
+
);
|
|
93
|
+
const sessionTypeOptions = useMemo(() => {
|
|
94
|
+
const options = [...runtimeSessionTypeOptions];
|
|
95
|
+
const currentSessionType = normalizeSessionType(selectedSession?.sessionType);
|
|
96
|
+
if (!options.some((option) => option.value === currentSessionType)) {
|
|
97
|
+
options.push({
|
|
98
|
+
value: currentSessionType,
|
|
99
|
+
label: resolveSessionTypeLabel(currentSessionType)
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
return options.sort((left, right) => {
|
|
103
|
+
if (left.value === DEFAULT_SESSION_TYPE) {
|
|
104
|
+
return -1;
|
|
105
|
+
}
|
|
106
|
+
if (right.value === DEFAULT_SESSION_TYPE) {
|
|
107
|
+
return 1;
|
|
108
|
+
}
|
|
109
|
+
return left.value.localeCompare(right.value);
|
|
110
|
+
});
|
|
111
|
+
}, [runtimeSessionTypeOptions, selectedSession?.sessionType]);
|
|
112
|
+
const defaultSessionType = useMemo(
|
|
113
|
+
() => normalizeSessionType(sessionTypesData?.defaultType ?? DEFAULT_SESSION_TYPE),
|
|
114
|
+
[sessionTypesData?.defaultType]
|
|
115
|
+
);
|
|
116
|
+
const selectedSessionType = useMemo(
|
|
117
|
+
() => normalizeSessionType(selectedSession?.sessionType ?? pendingSessionType ?? defaultSessionType),
|
|
118
|
+
[defaultSessionType, pendingSessionType, selectedSession?.sessionType]
|
|
119
|
+
);
|
|
120
|
+
|
|
121
|
+
useEffect(() => {
|
|
122
|
+
if (selectedSessionKey) {
|
|
123
|
+
return;
|
|
124
|
+
}
|
|
125
|
+
setPendingSessionType(defaultSessionType);
|
|
126
|
+
}, [defaultSessionType, selectedSessionKey, setPendingSessionType]);
|
|
127
|
+
|
|
128
|
+
const canEditSessionType = !selectedSessionKey || Boolean(selectedSession?.sessionTypeMutable);
|
|
129
|
+
const availableSessionTypeSet = useMemo(
|
|
130
|
+
() => new Set(runtimeSessionTypeOptions.map((option) => option.value)),
|
|
131
|
+
[runtimeSessionTypeOptions]
|
|
132
|
+
);
|
|
133
|
+
const sessionTypeUnavailable = Boolean(
|
|
134
|
+
selectedSession && !availableSessionTypeSet.has(normalizeSessionType(selectedSession.sessionType))
|
|
135
|
+
);
|
|
136
|
+
const sessionTypeUnavailableMessage = sessionTypeUnavailable
|
|
137
|
+
? `${resolveSessionTypeLabel(selectedSessionType)} ${t('chatSessionTypeUnavailableSuffix')}`
|
|
138
|
+
: null;
|
|
139
|
+
|
|
140
|
+
return {
|
|
141
|
+
sessionTypeOptions,
|
|
142
|
+
defaultSessionType,
|
|
143
|
+
selectedSessionType,
|
|
144
|
+
canEditSessionType,
|
|
145
|
+
sessionTypeUnavailable,
|
|
146
|
+
sessionTypeUnavailableMessage
|
|
147
|
+
};
|
|
148
|
+
}
|
|
@@ -9,7 +9,7 @@ interface MaskedInputProps extends React.InputHTMLAttributes<HTMLInputElement> {
|
|
|
9
9
|
isSet?: boolean;
|
|
10
10
|
}
|
|
11
11
|
|
|
12
|
-
export function MaskedInput({
|
|
12
|
+
export function MaskedInput({ isSet, className, value, onChange, placeholder, ...props }: MaskedInputProps) {
|
|
13
13
|
const [showKey, setShowKey] = useState(false);
|
|
14
14
|
const [isEditing, setIsEditing] = useState(false);
|
|
15
15
|
const hasUserInput = typeof value === 'string' && value.length > 0;
|
|
@@ -32,6 +32,15 @@ type ProviderFormProps = {
|
|
|
32
32
|
onProviderDeleted?: (providerName: string) => void;
|
|
33
33
|
};
|
|
34
34
|
|
|
35
|
+
type ProviderAuthMethodOption = {
|
|
36
|
+
id: string;
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
type PillSelectOption = {
|
|
40
|
+
value: string;
|
|
41
|
+
label: string;
|
|
42
|
+
};
|
|
43
|
+
|
|
35
44
|
const EMPTY_PROVIDER_CONFIG: ProviderConfigView = {
|
|
36
45
|
displayName: '',
|
|
37
46
|
apiKeySet: false,
|
|
@@ -151,6 +160,101 @@ function serializeModelsForSave(models: string[], defaultModels: string[]): stri
|
|
|
151
160
|
return models;
|
|
152
161
|
}
|
|
153
162
|
|
|
163
|
+
function resolvePreferredAuthMethodId(params: {
|
|
164
|
+
providerName?: string;
|
|
165
|
+
methods: ProviderAuthMethodOption[];
|
|
166
|
+
defaultMethodId?: string;
|
|
167
|
+
language: 'zh' | 'en';
|
|
168
|
+
}): string {
|
|
169
|
+
const { providerName, methods, defaultMethodId, language } = params;
|
|
170
|
+
if (methods.length === 0) {
|
|
171
|
+
return '';
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
const methodIdMap = new Map<string, string>();
|
|
175
|
+
for (const method of methods) {
|
|
176
|
+
const methodId = method.id.trim();
|
|
177
|
+
if (methodId) {
|
|
178
|
+
methodIdMap.set(methodId.toLowerCase(), methodId);
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
const pick = (...candidates: string[]): string | undefined => {
|
|
183
|
+
for (const candidate of candidates) {
|
|
184
|
+
const resolved = methodIdMap.get(candidate.toLowerCase());
|
|
185
|
+
if (resolved) {
|
|
186
|
+
return resolved;
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
return undefined;
|
|
190
|
+
};
|
|
191
|
+
|
|
192
|
+
const normalizedDefault = defaultMethodId?.trim();
|
|
193
|
+
if (providerName === 'minimax-portal') {
|
|
194
|
+
if (language === 'zh') {
|
|
195
|
+
return pick('cn', 'china-mainland') ?? pick(normalizedDefault ?? '') ?? methods[0]?.id ?? '';
|
|
196
|
+
}
|
|
197
|
+
if (language === 'en') {
|
|
198
|
+
return pick('global', 'intl', 'international') ?? pick(normalizedDefault ?? '') ?? methods[0]?.id ?? '';
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
if (normalizedDefault) {
|
|
203
|
+
const matchedDefault = pick(normalizedDefault);
|
|
204
|
+
if (matchedDefault) {
|
|
205
|
+
return matchedDefault;
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
if (language === 'zh') {
|
|
210
|
+
return pick('cn') ?? methods[0]?.id ?? '';
|
|
211
|
+
}
|
|
212
|
+
if (language === 'en') {
|
|
213
|
+
return pick('global') ?? methods[0]?.id ?? '';
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
return methods[0]?.id ?? '';
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
function shouldUsePillSelector(params: {
|
|
220
|
+
required: boolean;
|
|
221
|
+
hasDefault: boolean;
|
|
222
|
+
optionCount: number;
|
|
223
|
+
}): boolean {
|
|
224
|
+
return params.required && params.hasDefault && params.optionCount > 1 && params.optionCount <= 3;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
function PillSelector(props: {
|
|
228
|
+
value: string;
|
|
229
|
+
onChange: (value: string) => void;
|
|
230
|
+
options: PillSelectOption[];
|
|
231
|
+
}) {
|
|
232
|
+
const { value, onChange, options } = props;
|
|
233
|
+
|
|
234
|
+
return (
|
|
235
|
+
<div className="flex flex-wrap gap-2">
|
|
236
|
+
{options.map((option) => {
|
|
237
|
+
const selected = option.value === value;
|
|
238
|
+
return (
|
|
239
|
+
<button
|
|
240
|
+
key={option.value}
|
|
241
|
+
type="button"
|
|
242
|
+
onClick={() => onChange(option.value)}
|
|
243
|
+
aria-pressed={selected}
|
|
244
|
+
className={`rounded-full border px-3 py-1.5 text-xs font-medium transition-colors ${
|
|
245
|
+
selected
|
|
246
|
+
? 'border-primary bg-primary text-white shadow-sm'
|
|
247
|
+
: 'border-gray-200 bg-white text-gray-700 hover:border-primary/40 hover:text-primary'
|
|
248
|
+
}`}
|
|
249
|
+
>
|
|
250
|
+
{option.label}
|
|
251
|
+
</button>
|
|
252
|
+
);
|
|
253
|
+
})}
|
|
254
|
+
</div>
|
|
255
|
+
);
|
|
256
|
+
}
|
|
257
|
+
|
|
154
258
|
export function ProviderForm({ providerName, onProviderDeleted }: ProviderFormProps) {
|
|
155
259
|
const queryClient = useQueryClient();
|
|
156
260
|
const { data: config } = useConfig();
|
|
@@ -174,6 +278,7 @@ export function ProviderForm({ providerName, onProviderDeleted }: ProviderFormPr
|
|
|
174
278
|
const [showModelInput, setShowModelInput] = useState(false);
|
|
175
279
|
const [authSessionId, setAuthSessionId] = useState<string | null>(null);
|
|
176
280
|
const [authStatusMessage, setAuthStatusMessage] = useState('');
|
|
281
|
+
const [authMethodId, setAuthMethodId] = useState('');
|
|
177
282
|
const authPollTimerRef = useRef<number | null>(null);
|
|
178
283
|
|
|
179
284
|
const providerSpec = meta?.providers.find((p) => p.name === providerName);
|
|
@@ -225,11 +330,63 @@ export function ProviderForm({ providerName, onProviderDeleted }: ProviderFormPr
|
|
|
225
330
|
apiBaseHint?.help ||
|
|
226
331
|
t('providerApiBaseHelp');
|
|
227
332
|
const providerAuth = providerSpec?.auth;
|
|
333
|
+
const providerAuthMethods = useMemo(
|
|
334
|
+
() => providerAuth?.methods ?? [],
|
|
335
|
+
[providerAuth?.methods]
|
|
336
|
+
);
|
|
337
|
+
const providerAuthMethodOptions = useMemo(
|
|
338
|
+
() =>
|
|
339
|
+
providerAuthMethods.map((method) => ({
|
|
340
|
+
value: method.id,
|
|
341
|
+
label: method.label?.[language] || method.label?.en || method.id
|
|
342
|
+
})),
|
|
343
|
+
[providerAuthMethods, language]
|
|
344
|
+
);
|
|
345
|
+
const preferredAuthMethodId = useMemo(
|
|
346
|
+
() => resolvePreferredAuthMethodId({
|
|
347
|
+
providerName,
|
|
348
|
+
methods: providerAuthMethods,
|
|
349
|
+
defaultMethodId: providerAuth?.defaultMethodId,
|
|
350
|
+
language
|
|
351
|
+
}),
|
|
352
|
+
[providerName, providerAuth?.defaultMethodId, providerAuthMethods, language]
|
|
353
|
+
);
|
|
354
|
+
const resolvedAuthMethodId = useMemo(() => {
|
|
355
|
+
if (!providerAuthMethods.length) {
|
|
356
|
+
return '';
|
|
357
|
+
}
|
|
358
|
+
const normalizedCurrent = authMethodId.trim();
|
|
359
|
+
if (normalizedCurrent && providerAuthMethods.some((method) => method.id === normalizedCurrent)) {
|
|
360
|
+
return normalizedCurrent;
|
|
361
|
+
}
|
|
362
|
+
return preferredAuthMethodId || providerAuthMethods[0]?.id || '';
|
|
363
|
+
}, [authMethodId, preferredAuthMethodId, providerAuthMethods]);
|
|
364
|
+
const selectedAuthMethod = useMemo(
|
|
365
|
+
() => providerAuthMethods.find((method) => method.id === resolvedAuthMethodId),
|
|
366
|
+
[providerAuthMethods, resolvedAuthMethodId]
|
|
367
|
+
);
|
|
368
|
+
const selectedAuthMethodHint =
|
|
369
|
+
selectedAuthMethod?.hint?.[language] || selectedAuthMethod?.hint?.en || '';
|
|
370
|
+
const shouldUseAuthMethodPills = shouldUsePillSelector({
|
|
371
|
+
required: providerAuth?.kind === 'device_code',
|
|
372
|
+
hasDefault: Boolean(providerAuth?.defaultMethodId?.trim()),
|
|
373
|
+
optionCount: providerAuthMethods.length
|
|
374
|
+
});
|
|
228
375
|
const providerAuthNote =
|
|
229
376
|
providerAuth?.note?.[language] ||
|
|
230
377
|
providerAuth?.note?.en ||
|
|
231
378
|
providerAuth?.displayName ||
|
|
232
379
|
'';
|
|
380
|
+
const wireApiOptions = providerSpec?.wireApiOptions || ['auto', 'chat', 'responses'];
|
|
381
|
+
const wireApiSelectOptions: PillSelectOption[] = wireApiOptions.map((option) => ({
|
|
382
|
+
value: option,
|
|
383
|
+
label: option === 'chat' ? t('wireApiChat') : option === 'responses' ? t('wireApiResponses') : t('wireApiAuto')
|
|
384
|
+
}));
|
|
385
|
+
const shouldUseWireApiPills = shouldUsePillSelector({
|
|
386
|
+
required: Boolean(providerSpec?.supportsWireApi),
|
|
387
|
+
hasDefault: typeof providerSpec?.defaultWireApi === 'string' && providerSpec.defaultWireApi.length > 0,
|
|
388
|
+
optionCount: wireApiSelectOptions.length
|
|
389
|
+
});
|
|
233
390
|
|
|
234
391
|
const clearAuthPollTimer = useCallback(() => {
|
|
235
392
|
if (authPollTimerRef.current !== null) {
|
|
@@ -290,6 +447,7 @@ export function ProviderForm({ providerName, onProviderDeleted }: ProviderFormPr
|
|
|
290
447
|
setProviderDisplayName('');
|
|
291
448
|
setAuthSessionId(null);
|
|
292
449
|
setAuthStatusMessage('');
|
|
450
|
+
setAuthMethodId('');
|
|
293
451
|
clearAuthPollTimer();
|
|
294
452
|
return;
|
|
295
453
|
}
|
|
@@ -303,8 +461,18 @@ export function ProviderForm({ providerName, onProviderDeleted }: ProviderFormPr
|
|
|
303
461
|
setProviderDisplayName(effectiveDisplayName);
|
|
304
462
|
setAuthSessionId(null);
|
|
305
463
|
setAuthStatusMessage('');
|
|
464
|
+
setAuthMethodId(preferredAuthMethodId);
|
|
306
465
|
clearAuthPollTimer();
|
|
307
|
-
}, [
|
|
466
|
+
}, [
|
|
467
|
+
providerName,
|
|
468
|
+
currentApiBase,
|
|
469
|
+
resolvedProviderConfig.extraHeaders,
|
|
470
|
+
currentWireApi,
|
|
471
|
+
currentEditableModels,
|
|
472
|
+
effectiveDisplayName,
|
|
473
|
+
preferredAuthMethodId,
|
|
474
|
+
clearAuthPollTimer
|
|
475
|
+
]);
|
|
308
476
|
|
|
309
477
|
useEffect(() => () => clearAuthPollTimer(), [clearAuthPollTimer]);
|
|
310
478
|
|
|
@@ -453,7 +621,10 @@ export function ProviderForm({ providerName, onProviderDeleted }: ProviderFormPr
|
|
|
453
621
|
|
|
454
622
|
try {
|
|
455
623
|
setAuthStatusMessage('');
|
|
456
|
-
const result = await startProviderAuth.mutateAsync({
|
|
624
|
+
const result = await startProviderAuth.mutateAsync({
|
|
625
|
+
provider: providerName,
|
|
626
|
+
data: resolvedAuthMethodId ? { methodId: resolvedAuthMethodId } : {}
|
|
627
|
+
});
|
|
457
628
|
if (!result.sessionId || !result.verificationUri) {
|
|
458
629
|
throw new Error(t('providerAuthStartFailed'));
|
|
459
630
|
}
|
|
@@ -567,6 +738,34 @@ export function ProviderForm({ providerName, onProviderDeleted }: ProviderFormPr
|
|
|
567
738
|
{providerAuthNote ? (
|
|
568
739
|
<p className="text-xs text-gray-600">{providerAuthNote}</p>
|
|
569
740
|
) : null}
|
|
741
|
+
{providerAuthMethods.length > 1 ? (
|
|
742
|
+
<div className="space-y-2">
|
|
743
|
+
<Label className="text-xs font-medium text-gray-700">{t('providerAuthMethodLabel')}</Label>
|
|
744
|
+
{shouldUseAuthMethodPills ? (
|
|
745
|
+
<PillSelector
|
|
746
|
+
value={resolvedAuthMethodId}
|
|
747
|
+
onChange={setAuthMethodId}
|
|
748
|
+
options={providerAuthMethodOptions}
|
|
749
|
+
/>
|
|
750
|
+
) : (
|
|
751
|
+
<Select value={resolvedAuthMethodId} onValueChange={setAuthMethodId}>
|
|
752
|
+
<SelectTrigger className="h-8 rounded-lg bg-white">
|
|
753
|
+
<SelectValue placeholder={t('providerAuthMethodPlaceholder')} />
|
|
754
|
+
</SelectTrigger>
|
|
755
|
+
<SelectContent>
|
|
756
|
+
{providerAuthMethodOptions.map((method) => (
|
|
757
|
+
<SelectItem key={method.value} value={method.value}>
|
|
758
|
+
{method.label}
|
|
759
|
+
</SelectItem>
|
|
760
|
+
))}
|
|
761
|
+
</SelectContent>
|
|
762
|
+
</Select>
|
|
763
|
+
)}
|
|
764
|
+
{selectedAuthMethodHint ? (
|
|
765
|
+
<p className="text-xs text-gray-500">{selectedAuthMethodHint}</p>
|
|
766
|
+
) : null}
|
|
767
|
+
</div>
|
|
768
|
+
) : null}
|
|
570
769
|
<div className="flex flex-wrap items-center gap-2">
|
|
571
770
|
<Button
|
|
572
771
|
type="button"
|
|
@@ -718,18 +917,26 @@ export function ProviderForm({ providerName, onProviderDeleted }: ProviderFormPr
|
|
|
718
917
|
<Label htmlFor="wireApi" className="text-sm font-medium text-gray-900">
|
|
719
918
|
{wireApiHint?.label ?? t('wireApi')}
|
|
720
919
|
</Label>
|
|
721
|
-
|
|
722
|
-
<
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
920
|
+
{shouldUseWireApiPills ? (
|
|
921
|
+
<PillSelector
|
|
922
|
+
value={wireApi}
|
|
923
|
+
onChange={(v) => setWireApi(v as WireApiType)}
|
|
924
|
+
options={wireApiSelectOptions}
|
|
925
|
+
/>
|
|
926
|
+
) : (
|
|
927
|
+
<Select value={wireApi} onValueChange={(v) => setWireApi(v as WireApiType)}>
|
|
928
|
+
<SelectTrigger className="rounded-xl">
|
|
929
|
+
<SelectValue />
|
|
930
|
+
</SelectTrigger>
|
|
931
|
+
<SelectContent>
|
|
932
|
+
{wireApiSelectOptions.map((option) => (
|
|
933
|
+
<SelectItem key={option.value} value={option.value}>
|
|
934
|
+
{option.label}
|
|
935
|
+
</SelectItem>
|
|
936
|
+
))}
|
|
937
|
+
</SelectContent>
|
|
938
|
+
</Select>
|
|
939
|
+
)}
|
|
733
940
|
</div>
|
|
734
941
|
)}
|
|
735
942
|
|