@nextclaw/ui 0.5.48 → 0.6.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +18 -0
- package/dist/assets/ChannelsList-CkCpHSto.js +1 -0
- package/dist/assets/ChatPage-DM4XNsrW.js +32 -0
- package/dist/assets/DocBrowser-B5Aqiz6W.js +1 -0
- package/dist/assets/MarketplacePage-BIi0bBdW.js +49 -0
- package/dist/assets/ModelConfig-BTFiEAxQ.js +1 -0
- package/dist/assets/{ProvidersList-BXHpjVtO.js → ProvidersList-cdk1d-G_.js} +1 -1
- package/dist/assets/RuntimeConfig-CFqFsXmR.js +1 -0
- package/dist/assets/{SecretsConfig-KkgMzdt1.js → SecretsConfig-CIKasCek.js} +2 -2
- package/dist/assets/SessionsConfig-mnCLFtbo.js +2 -0
- package/dist/assets/{card-D7NY0Szf.js → card-C1BUfR85.js} +1 -1
- package/dist/assets/index-Dxas8MJ9.js +2 -0
- package/dist/assets/index-P4YzN9iS.css +1 -0
- package/dist/assets/{label-Ojs7Al6B.js → label-CwWfYbuj.js} +1 -1
- package/dist/assets/{logos-B1qBsCSi.js → logos-DDyjHSEU.js} +1 -1
- package/dist/assets/{page-layout-CUMMO0nN.js → page-layout-DKTRKcHL.js} +1 -1
- package/dist/assets/provider-models-y4mUDcGF.js +1 -0
- package/dist/assets/{switch-BdhS_16-.js → switch-Bi3yeYiC.js} +1 -1
- package/dist/assets/{tabs-custom-D261E5EA.js → tabs-custom-HZFNZrc0.js} +1 -1
- package/dist/assets/useConfig-CgzVQTZl.js +6 -0
- package/dist/assets/{useConfirmDialog-BUKGHDL6.js → useConfirmDialog-DwD21HlD.js} +2 -2
- package/dist/assets/{vendor-Dh04PGww.js → vendor-Ylg6Wdt_.js} +84 -69
- package/dist/index.html +3 -3
- package/package.json +2 -1
- package/src/App.tsx +10 -6
- package/src/api/config.ts +42 -1
- package/src/api/types.ts +29 -0
- package/src/components/chat/ChatConversationPanel.tsx +109 -85
- package/src/components/chat/ChatInputBar.tsx +245 -0
- package/src/components/chat/ChatPage.tsx +365 -187
- package/src/components/chat/ChatSidebar.tsx +242 -0
- package/src/components/chat/ChatThread.tsx +92 -25
- package/src/components/chat/ChatWelcome.tsx +61 -0
- package/src/components/chat/SkillsPicker.tsx +137 -0
- package/src/components/chat/useChatStreamController.ts +287 -56
- package/src/components/config/ChannelForm.tsx +1 -1
- package/src/components/config/ChannelsList.tsx +3 -3
- package/src/components/config/ModelConfig.tsx +11 -89
- package/src/components/config/RuntimeConfig.tsx +29 -1
- package/src/components/layout/AppLayout.tsx +42 -6
- package/src/components/layout/Sidebar.tsx +68 -62
- package/src/components/marketplace/MarketplacePage.tsx +13 -3
- package/src/components/ui/popover.tsx +31 -0
- package/src/hooks/useConfig.ts +18 -0
- package/src/lib/i18n.ts +53 -0
- package/src/lib/provider-models.ts +129 -0
- package/dist/assets/ChannelsList-C8cguFLc.js +0 -1
- package/dist/assets/ChatPage-BkHWNUNR.js +0 -32
- package/dist/assets/CronConfig-D-ESQlvk.js +0 -1
- package/dist/assets/DocBrowser-B9ZD6pAk.js +0 -1
- package/dist/assets/MarketplacePage-Ds_l9KTF.js +0 -49
- package/dist/assets/ModelConfig-N1tbLv9b.js +0 -1
- package/dist/assets/RuntimeConfig-KsKfkjgv.js +0 -1
- package/dist/assets/SessionsConfig-CWBp8IPf.js +0 -2
- package/dist/assets/index-BRBYYgR_.js +0 -2
- package/dist/assets/index-C5cdRzpO.css +0 -1
- package/dist/assets/useConfig-txxbxXnT.js +0 -6
|
@@ -1,59 +1,85 @@
|
|
|
1
1
|
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
|
2
|
-
import type {
|
|
3
|
-
import {
|
|
2
|
+
import type { ComponentProps, Dispatch, MutableRefObject, SetStateAction } from 'react';
|
|
3
|
+
import type { SessionEntryView, SessionEventView } from '@/api/types';
|
|
4
|
+
import {
|
|
5
|
+
useChatCapabilities,
|
|
6
|
+
useConfig,
|
|
7
|
+
useConfigMeta,
|
|
8
|
+
useDeleteSession,
|
|
9
|
+
useSessionHistory,
|
|
10
|
+
useSessions
|
|
11
|
+
} from '@/hooks/useConfig';
|
|
12
|
+
import { useMarketplaceInstalled } from '@/hooks/useMarketplace';
|
|
4
13
|
import { useConfirmDialog } from '@/hooks/useConfirmDialog';
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
7
|
-
import { ChatSessionsSidebar } from '@/components/chat/ChatSessionsSidebar';
|
|
14
|
+
import type { ChatModelOption } from '@/components/chat/ChatInputBar';
|
|
15
|
+
import { ChatSidebar } from '@/components/chat/ChatSidebar';
|
|
8
16
|
import { ChatConversationPanel } from '@/components/chat/ChatConversationPanel';
|
|
17
|
+
import { CronConfig } from '@/components/config/CronConfig';
|
|
18
|
+
import { MarketplacePage } from '@/components/marketplace/MarketplacePage';
|
|
9
19
|
import { useChatStreamController } from '@/components/chat/useChatStreamController';
|
|
10
|
-
import { cn } from '@/lib/utils';
|
|
11
20
|
import { buildFallbackEventsFromMessages } from '@/lib/chat-message';
|
|
21
|
+
import { buildProviderModelCatalog, composeProviderModel } from '@/lib/provider-models';
|
|
12
22
|
import { t } from '@/lib/i18n';
|
|
13
|
-
import {
|
|
23
|
+
import { useLocation, useNavigate, useParams } from 'react-router-dom';
|
|
14
24
|
|
|
15
|
-
const
|
|
16
|
-
const UNKNOWN_CHAT_CHANNEL_KEY = '__unknown_channel__';
|
|
25
|
+
const SESSION_ROUTE_PREFIX = 'sid_';
|
|
17
26
|
|
|
18
|
-
function
|
|
19
|
-
|
|
27
|
+
function resolveAgentIdFromSessionKey(sessionKey: string): string | null {
|
|
28
|
+
const match = /^agent:([^:]+):/i.exec(sessionKey.trim());
|
|
29
|
+
if (!match) {
|
|
20
30
|
return null;
|
|
21
31
|
}
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
32
|
+
const value = match[1]?.trim();
|
|
33
|
+
return value ? value : null;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function buildNewSessionKey(agentId: string): string {
|
|
37
|
+
const slug = Math.random().toString(36).slice(2, 8);
|
|
38
|
+
return `agent:${agentId}:ui:direct:web-${Date.now().toString(36)}${slug}`;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function encodeSessionRouteId(sessionKey: string): string {
|
|
42
|
+
const bytes = new TextEncoder().encode(sessionKey);
|
|
43
|
+
let binary = '';
|
|
44
|
+
for (const byte of bytes) {
|
|
45
|
+
binary += String.fromCharCode(byte);
|
|
27
46
|
}
|
|
47
|
+
const base64 = btoa(binary).replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/g, '');
|
|
48
|
+
return `${SESSION_ROUTE_PREFIX}${base64}`;
|
|
28
49
|
}
|
|
29
50
|
|
|
30
|
-
function
|
|
31
|
-
if (
|
|
32
|
-
return;
|
|
51
|
+
function decodeSessionRouteId(routeValue: string): string | null {
|
|
52
|
+
if (!routeValue.startsWith(SESSION_ROUTE_PREFIX)) {
|
|
53
|
+
return null;
|
|
33
54
|
}
|
|
55
|
+
const encoded = routeValue.slice(SESSION_ROUTE_PREFIX.length).replace(/-/g, '+').replace(/_/g, '/');
|
|
56
|
+
const padding = encoded.length % 4 === 0 ? '' : '='.repeat(4 - (encoded.length % 4));
|
|
34
57
|
try {
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
}
|
|
39
|
-
window.localStorage.setItem(CHAT_SESSION_STORAGE_KEY, value);
|
|
58
|
+
const binary = atob(encoded + padding);
|
|
59
|
+
const bytes = Uint8Array.from(binary, (char) => char.charCodeAt(0));
|
|
60
|
+
return new TextDecoder().decode(bytes);
|
|
40
61
|
} catch {
|
|
41
|
-
|
|
62
|
+
return null;
|
|
42
63
|
}
|
|
43
64
|
}
|
|
44
65
|
|
|
45
|
-
function
|
|
46
|
-
|
|
47
|
-
if (!match) {
|
|
66
|
+
function parseSessionKeyFromRoute(routeValue?: string): string | null {
|
|
67
|
+
if (!routeValue) {
|
|
48
68
|
return null;
|
|
49
69
|
}
|
|
50
|
-
const
|
|
51
|
-
|
|
70
|
+
const decodedToken = decodeSessionRouteId(routeValue);
|
|
71
|
+
if (decodedToken) {
|
|
72
|
+
return decodedToken;
|
|
73
|
+
}
|
|
74
|
+
try {
|
|
75
|
+
return decodeURIComponent(routeValue);
|
|
76
|
+
} catch {
|
|
77
|
+
return routeValue;
|
|
78
|
+
}
|
|
52
79
|
}
|
|
53
80
|
|
|
54
|
-
function
|
|
55
|
-
|
|
56
|
-
return `agent:${agentId}:ui:direct:web-${Date.now().toString(36)}${slug}`;
|
|
81
|
+
function buildSessionPath(sessionKey: string): string {
|
|
82
|
+
return `/chat/${encodeSessionRouteId(sessionKey)}`;
|
|
57
83
|
}
|
|
58
84
|
|
|
59
85
|
function sessionDisplayName(session: SessionEntryView): string {
|
|
@@ -64,73 +90,227 @@ function sessionDisplayName(session: SessionEntryView): string {
|
|
|
64
90
|
return chunks[chunks.length - 1] || session.key;
|
|
65
91
|
}
|
|
66
92
|
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
93
|
+
type MainPanelView = 'chat' | 'cron' | 'skills';
|
|
94
|
+
|
|
95
|
+
type ChatPageProps = {
|
|
96
|
+
view: MainPanelView;
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
type UseSessionSyncParams = {
|
|
100
|
+
view: MainPanelView;
|
|
101
|
+
routeSessionKey: string | null;
|
|
102
|
+
selectedSessionKey: string | null;
|
|
103
|
+
selectedAgentId: string;
|
|
104
|
+
setSelectedSessionKey: Dispatch<SetStateAction<string | null>>;
|
|
105
|
+
setSelectedAgentId: Dispatch<SetStateAction<string>>;
|
|
106
|
+
selectedSessionKeyRef: MutableRefObject<string | null>;
|
|
107
|
+
isUserScrollingRef: MutableRefObject<boolean>;
|
|
108
|
+
resetStreamState: () => void;
|
|
109
|
+
};
|
|
110
|
+
|
|
111
|
+
function useChatSessionSync(params: UseSessionSyncParams): void {
|
|
112
|
+
const {
|
|
113
|
+
view,
|
|
114
|
+
routeSessionKey,
|
|
115
|
+
selectedSessionKey,
|
|
116
|
+
selectedAgentId,
|
|
117
|
+
setSelectedSessionKey,
|
|
118
|
+
setSelectedAgentId,
|
|
119
|
+
selectedSessionKeyRef,
|
|
120
|
+
isUserScrollingRef,
|
|
121
|
+
resetStreamState
|
|
122
|
+
} = params;
|
|
123
|
+
|
|
124
|
+
useEffect(() => {
|
|
125
|
+
if (view !== 'chat') {
|
|
126
|
+
return;
|
|
127
|
+
}
|
|
128
|
+
if (routeSessionKey) {
|
|
129
|
+
if (selectedSessionKey !== routeSessionKey) {
|
|
130
|
+
setSelectedSessionKey(routeSessionKey);
|
|
131
|
+
}
|
|
132
|
+
return;
|
|
133
|
+
}
|
|
134
|
+
if (selectedSessionKey !== null) {
|
|
135
|
+
setSelectedSessionKey(null);
|
|
136
|
+
resetStreamState();
|
|
137
|
+
}
|
|
138
|
+
}, [resetStreamState, routeSessionKey, selectedSessionKey, setSelectedSessionKey, view]);
|
|
139
|
+
|
|
140
|
+
useEffect(() => {
|
|
141
|
+
const inferred = selectedSessionKey ? resolveAgentIdFromSessionKey(selectedSessionKey) : null;
|
|
142
|
+
if (!inferred) {
|
|
143
|
+
return;
|
|
144
|
+
}
|
|
145
|
+
if (selectedAgentId !== inferred) {
|
|
146
|
+
setSelectedAgentId(inferred);
|
|
147
|
+
}
|
|
148
|
+
}, [selectedAgentId, selectedSessionKey, setSelectedAgentId]);
|
|
149
|
+
|
|
150
|
+
useEffect(() => {
|
|
151
|
+
selectedSessionKeyRef.current = selectedSessionKey;
|
|
152
|
+
isUserScrollingRef.current = false;
|
|
153
|
+
}, [isUserScrollingRef, selectedSessionKey, selectedSessionKeyRef]);
|
|
74
154
|
}
|
|
75
155
|
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
156
|
+
type UseThreadScrollParams = {
|
|
157
|
+
threadRef: MutableRefObject<HTMLDivElement | null>;
|
|
158
|
+
isUserScrollingRef: MutableRefObject<boolean>;
|
|
159
|
+
mergedEvents: SessionEventView[];
|
|
160
|
+
isSending: boolean;
|
|
161
|
+
};
|
|
162
|
+
|
|
163
|
+
function useChatThreadScroll(params: UseThreadScrollParams): { handleScroll: () => void } {
|
|
164
|
+
const { threadRef, isUserScrollingRef, mergedEvents, isSending } = params;
|
|
165
|
+
|
|
166
|
+
const isNearBottom = useCallback(() => {
|
|
167
|
+
const element = threadRef.current;
|
|
168
|
+
if (!element) {
|
|
169
|
+
return true;
|
|
170
|
+
}
|
|
171
|
+
const threshold = 50;
|
|
172
|
+
return element.scrollHeight - element.scrollTop - element.clientHeight < threshold;
|
|
173
|
+
}, [threadRef]);
|
|
174
|
+
|
|
175
|
+
const handleScroll = useCallback(() => {
|
|
176
|
+
if (isNearBottom()) {
|
|
177
|
+
isUserScrollingRef.current = false;
|
|
178
|
+
} else {
|
|
179
|
+
isUserScrollingRef.current = true;
|
|
180
|
+
}
|
|
181
|
+
}, [isNearBottom, isUserScrollingRef]);
|
|
182
|
+
|
|
183
|
+
useEffect(() => {
|
|
184
|
+
const element = threadRef.current;
|
|
185
|
+
if (!element || isUserScrollingRef.current) {
|
|
186
|
+
return;
|
|
187
|
+
}
|
|
188
|
+
element.scrollTop = element.scrollHeight;
|
|
189
|
+
}, [isSending, isUserScrollingRef, mergedEvents, threadRef]);
|
|
190
|
+
|
|
191
|
+
return { handleScroll };
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
type ChatPageLayoutProps = {
|
|
195
|
+
view: MainPanelView;
|
|
196
|
+
sidebarProps: ComponentProps<typeof ChatSidebar>;
|
|
197
|
+
conversationProps: ComponentProps<typeof ChatConversationPanel>;
|
|
198
|
+
confirmDialog: JSX.Element;
|
|
199
|
+
};
|
|
200
|
+
|
|
201
|
+
function ChatPageLayout({ view, sidebarProps, conversationProps, confirmDialog }: ChatPageLayoutProps) {
|
|
202
|
+
return (
|
|
203
|
+
<div className="h-full flex">
|
|
204
|
+
<ChatSidebar {...sidebarProps} />
|
|
205
|
+
|
|
206
|
+
{view === 'chat' ? (
|
|
207
|
+
<ChatConversationPanel {...conversationProps} />
|
|
208
|
+
) : (
|
|
209
|
+
<section className="flex-1 min-h-0 overflow-hidden bg-gradient-to-b from-gray-50/60 to-white">
|
|
210
|
+
<div className="h-full overflow-auto custom-scrollbar">
|
|
211
|
+
<div className="mx-auto w-full max-w-[min(1120px,100%)] px-6 py-5">
|
|
212
|
+
{view === 'cron' ? <CronConfig /> : <MarketplacePage forcedType="skills" />}
|
|
213
|
+
</div>
|
|
214
|
+
</div>
|
|
215
|
+
</section>
|
|
216
|
+
)}
|
|
217
|
+
|
|
218
|
+
{confirmDialog}
|
|
219
|
+
</div>
|
|
220
|
+
);
|
|
81
221
|
}
|
|
82
222
|
|
|
83
|
-
export function ChatPage() {
|
|
223
|
+
export function ChatPage({ view }: ChatPageProps) {
|
|
84
224
|
const [query, setQuery] = useState('');
|
|
85
|
-
const [selectedChannel, setSelectedChannel] = useState('all');
|
|
86
225
|
const [draft, setDraft] = useState('');
|
|
87
|
-
const [selectedSessionKey, setSelectedSessionKey] = useState<string | null>(
|
|
226
|
+
const [selectedSessionKey, setSelectedSessionKey] = useState<string | null>(null);
|
|
88
227
|
const [selectedAgentId, setSelectedAgentId] = useState('main');
|
|
228
|
+
const [selectedModel, setSelectedModel] = useState('');
|
|
229
|
+
const [selectedSkills, setSelectedSkills] = useState<string[]>([]);
|
|
89
230
|
|
|
90
231
|
const { confirm, ConfirmDialog } = useConfirmDialog();
|
|
232
|
+
const location = useLocation();
|
|
233
|
+
const navigate = useNavigate();
|
|
234
|
+
const { sessionId: routeSessionIdParam } = useParams<{ sessionId?: string }>();
|
|
91
235
|
const threadRef = useRef<HTMLDivElement | null>(null);
|
|
92
236
|
const isUserScrollingRef = useRef(false);
|
|
93
237
|
const selectedSessionKeyRef = useRef<string | null>(selectedSessionKey);
|
|
238
|
+
const routeSessionKey = useMemo(
|
|
239
|
+
() => parseSessionKeyFromRoute(routeSessionIdParam),
|
|
240
|
+
[routeSessionIdParam]
|
|
241
|
+
);
|
|
94
242
|
|
|
95
243
|
const configQuery = useConfig();
|
|
244
|
+
const configMetaQuery = useConfigMeta();
|
|
96
245
|
const sessionsQuery = useSessions({ q: query.trim() || undefined, limit: 120, activeMinutes: 0 });
|
|
246
|
+
const installedSkillsQuery = useMarketplaceInstalled('skill');
|
|
247
|
+
const chatCapabilitiesQuery = useChatCapabilities({
|
|
248
|
+
sessionKey: selectedSessionKey,
|
|
249
|
+
agentId: selectedAgentId
|
|
250
|
+
});
|
|
97
251
|
const historyQuery = useSessionHistory(selectedSessionKey, 300);
|
|
98
252
|
const deleteSession = useDeleteSession();
|
|
99
253
|
|
|
100
|
-
const
|
|
101
|
-
const
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
254
|
+
const modelOptions = useMemo<ChatModelOption[]>(() => {
|
|
255
|
+
const providers = buildProviderModelCatalog({
|
|
256
|
+
meta: configMetaQuery.data,
|
|
257
|
+
config: configQuery.data,
|
|
258
|
+
onlyConfigured: true
|
|
259
|
+
});
|
|
260
|
+
const seen = new Set<string>();
|
|
261
|
+
const options: ChatModelOption[] = [];
|
|
262
|
+
for (const provider of providers) {
|
|
263
|
+
for (const localModel of provider.models) {
|
|
264
|
+
const value = composeProviderModel(provider.prefix, localModel);
|
|
265
|
+
if (!value || seen.has(value)) {
|
|
266
|
+
continue;
|
|
267
|
+
}
|
|
268
|
+
seen.add(value);
|
|
269
|
+
options.push({
|
|
270
|
+
value,
|
|
271
|
+
modelLabel: localModel,
|
|
272
|
+
providerLabel: provider.displayName
|
|
273
|
+
});
|
|
106
274
|
}
|
|
107
275
|
}
|
|
108
|
-
return
|
|
109
|
-
|
|
276
|
+
return options.sort((left, right) => {
|
|
277
|
+
const providerCompare = left.providerLabel.localeCompare(right.providerLabel);
|
|
278
|
+
if (providerCompare !== 0) {
|
|
279
|
+
return providerCompare;
|
|
280
|
+
}
|
|
281
|
+
return left.modelLabel.localeCompare(right.modelLabel);
|
|
282
|
+
});
|
|
283
|
+
}, [configMetaQuery.data, configQuery.data]);
|
|
110
284
|
|
|
111
285
|
const sessions = useMemo(() => sessionsQuery.data?.sessions ?? [], [sessionsQuery.data?.sessions]);
|
|
112
|
-
const
|
|
113
|
-
|
|
114
|
-
for (const session of sessions) {
|
|
115
|
-
unique.add(resolveChannelFromSessionKey(session.key));
|
|
116
|
-
}
|
|
117
|
-
return Array.from(unique).sort((a, b) => {
|
|
118
|
-
if (a === UNKNOWN_CHAT_CHANNEL_KEY) return 1;
|
|
119
|
-
if (b === UNKNOWN_CHAT_CHANNEL_KEY) return -1;
|
|
120
|
-
return a.localeCompare(b);
|
|
121
|
-
});
|
|
122
|
-
}, [sessions]);
|
|
123
|
-
const filteredSessions = useMemo(() => {
|
|
124
|
-
if (selectedChannel === 'all') {
|
|
125
|
-
return sessions;
|
|
126
|
-
}
|
|
127
|
-
return sessions.filter((session) => resolveChannelFromSessionKey(session.key) === selectedChannel);
|
|
128
|
-
}, [selectedChannel, sessions]);
|
|
286
|
+
const skillRecords = useMemo(() => installedSkillsQuery.data?.records ?? [], [installedSkillsQuery.data?.records]);
|
|
287
|
+
|
|
129
288
|
const selectedSession = useMemo(
|
|
130
289
|
() => sessions.find((session) => session.key === selectedSessionKey) ?? null,
|
|
131
290
|
[selectedSessionKey, sessions]
|
|
132
291
|
);
|
|
133
292
|
|
|
293
|
+
useEffect(() => {
|
|
294
|
+
if (modelOptions.length === 0) {
|
|
295
|
+
setSelectedModel('');
|
|
296
|
+
return;
|
|
297
|
+
}
|
|
298
|
+
setSelectedModel((prev) => {
|
|
299
|
+
if (modelOptions.some((option) => option.value === prev)) {
|
|
300
|
+
return prev;
|
|
301
|
+
}
|
|
302
|
+
const sessionPreferred = selectedSession?.preferredModel?.trim();
|
|
303
|
+
if (sessionPreferred && modelOptions.some((option) => option.value === sessionPreferred)) {
|
|
304
|
+
return sessionPreferred;
|
|
305
|
+
}
|
|
306
|
+
const fallback = configQuery.data?.agents.defaults.model?.trim();
|
|
307
|
+
if (fallback && modelOptions.some((option) => option.value === fallback)) {
|
|
308
|
+
return fallback;
|
|
309
|
+
}
|
|
310
|
+
return modelOptions[0]?.value ?? '';
|
|
311
|
+
});
|
|
312
|
+
}, [configQuery.data?.agents.defaults.model, modelOptions, selectedSession?.preferredModel]);
|
|
313
|
+
|
|
134
314
|
const historyData = historyQuery.data;
|
|
135
315
|
const historyMessages = historyData?.messages ?? [];
|
|
136
316
|
const historyEvents =
|
|
@@ -150,7 +330,11 @@ export function ChatPage() {
|
|
|
150
330
|
isSending,
|
|
151
331
|
isAwaitingAssistantOutput,
|
|
152
332
|
queuedCount,
|
|
333
|
+
canStopCurrentRun,
|
|
334
|
+
stopDisabledReason,
|
|
335
|
+
lastSendError,
|
|
153
336
|
sendMessage,
|
|
337
|
+
stopCurrentRun,
|
|
154
338
|
resetStreamState
|
|
155
339
|
} = useChatStreamController({
|
|
156
340
|
nextOptimisticUserSeq,
|
|
@@ -186,59 +370,38 @@ export function ChatPage() {
|
|
|
186
370
|
return next;
|
|
187
371
|
}, [historyEvents, optimisticUserEvent, streamingAssistantText, streamingAssistantTimestamp, streamingSessionEvents]);
|
|
188
372
|
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
const inferred = selectedSessionKey ? resolveAgentIdFromSessionKey(selectedSessionKey) : null;
|
|
201
|
-
if (!inferred) {
|
|
202
|
-
return;
|
|
203
|
-
}
|
|
204
|
-
if (selectedAgentId !== inferred) {
|
|
205
|
-
setSelectedAgentId(inferred);
|
|
206
|
-
}
|
|
207
|
-
}, [selectedAgentId, selectedSessionKey]);
|
|
208
|
-
|
|
209
|
-
useEffect(() => {
|
|
210
|
-
selectedSessionKeyRef.current = selectedSessionKey;
|
|
211
|
-
isUserScrollingRef.current = false;
|
|
212
|
-
}, [selectedSessionKey]);
|
|
373
|
+
useChatSessionSync({
|
|
374
|
+
view,
|
|
375
|
+
routeSessionKey,
|
|
376
|
+
selectedSessionKey,
|
|
377
|
+
selectedAgentId,
|
|
378
|
+
setSelectedSessionKey,
|
|
379
|
+
setSelectedAgentId,
|
|
380
|
+
selectedSessionKeyRef,
|
|
381
|
+
isUserScrollingRef,
|
|
382
|
+
resetStreamState
|
|
383
|
+
});
|
|
213
384
|
|
|
214
|
-
const
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
}
|
|
385
|
+
const { handleScroll } = useChatThreadScroll({
|
|
386
|
+
threadRef,
|
|
387
|
+
isUserScrollingRef,
|
|
388
|
+
mergedEvents,
|
|
389
|
+
isSending
|
|
390
|
+
});
|
|
220
391
|
|
|
221
|
-
const
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
392
|
+
const createNewSession = useCallback(() => {
|
|
393
|
+
resetStreamState();
|
|
394
|
+
setSelectedSessionKey(null);
|
|
395
|
+
if (location.pathname !== '/chat') {
|
|
396
|
+
navigate('/chat');
|
|
226
397
|
}
|
|
227
|
-
}, [
|
|
398
|
+
}, [location.pathname, navigate, resetStreamState]);
|
|
228
399
|
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
return;
|
|
400
|
+
const goToProviders = useCallback(() => {
|
|
401
|
+
if (location.pathname !== '/providers') {
|
|
402
|
+
navigate('/providers');
|
|
233
403
|
}
|
|
234
|
-
|
|
235
|
-
}, [mergedEvents, isSending]);
|
|
236
|
-
|
|
237
|
-
const createNewSession = useCallback(() => {
|
|
238
|
-
resetStreamState();
|
|
239
|
-
const next = buildNewSessionKey(selectedAgentId);
|
|
240
|
-
setSelectedSessionKey(next);
|
|
241
|
-
}, [resetStreamState, selectedAgentId]);
|
|
404
|
+
}, [location.pathname, navigate]);
|
|
242
405
|
|
|
243
406
|
const handleDeleteSession = useCallback(async () => {
|
|
244
407
|
if (!selectedSessionKey) {
|
|
@@ -258,94 +421,109 @@ export function ChatPage() {
|
|
|
258
421
|
onSuccess: async () => {
|
|
259
422
|
resetStreamState();
|
|
260
423
|
setSelectedSessionKey(null);
|
|
424
|
+
navigate('/chat', { replace: true });
|
|
261
425
|
await sessionsQuery.refetch();
|
|
262
426
|
}
|
|
263
427
|
}
|
|
264
428
|
);
|
|
265
|
-
}, [confirm, deleteSession, resetStreamState, selectedSessionKey, sessionsQuery]);
|
|
429
|
+
}, [confirm, deleteSession, navigate, resetStreamState, selectedSessionKey, sessionsQuery]);
|
|
266
430
|
|
|
267
431
|
const handleSend = useCallback(async () => {
|
|
268
432
|
const message = draft.trim();
|
|
269
433
|
if (!message) {
|
|
270
434
|
return;
|
|
271
435
|
}
|
|
436
|
+
const requestedSkills = selectedSkills;
|
|
272
437
|
|
|
273
438
|
const sessionKey = selectedSessionKey ?? buildNewSessionKey(selectedAgentId);
|
|
274
439
|
if (!selectedSessionKey) {
|
|
275
|
-
|
|
440
|
+
navigate(buildSessionPath(sessionKey), { replace: true });
|
|
276
441
|
}
|
|
277
442
|
setDraft('');
|
|
443
|
+
setSelectedSkills([]);
|
|
278
444
|
await sendMessage({
|
|
279
445
|
message,
|
|
280
446
|
sessionKey,
|
|
281
447
|
agentId: selectedAgentId,
|
|
448
|
+
model: selectedModel || undefined,
|
|
449
|
+
stopSupported: chatCapabilitiesQuery.data?.stopSupported ?? false,
|
|
450
|
+
stopReason: chatCapabilitiesQuery.data?.stopReason,
|
|
451
|
+
requestedSkills,
|
|
282
452
|
restoreDraftOnError: true
|
|
283
453
|
});
|
|
284
|
-
}, [
|
|
454
|
+
}, [
|
|
455
|
+
chatCapabilitiesQuery.data?.stopReason,
|
|
456
|
+
chatCapabilitiesQuery.data?.stopSupported,
|
|
457
|
+
draft,
|
|
458
|
+
selectedAgentId,
|
|
459
|
+
selectedModel,
|
|
460
|
+
navigate,
|
|
461
|
+
selectedSessionKey,
|
|
462
|
+
selectedSkills,
|
|
463
|
+
sendMessage
|
|
464
|
+
]);
|
|
465
|
+
|
|
466
|
+
const currentSessionDisplayName = selectedSession ? sessionDisplayName(selectedSession) : undefined;
|
|
467
|
+
const handleSelectSession = useCallback((nextSessionKey: string) => {
|
|
468
|
+
const target = buildSessionPath(nextSessionKey);
|
|
469
|
+
if (location.pathname !== target) {
|
|
470
|
+
navigate(target);
|
|
471
|
+
}
|
|
472
|
+
}, [location.pathname, navigate]);
|
|
473
|
+
|
|
474
|
+
const sidebarProps: ComponentProps<typeof ChatSidebar> = {
|
|
475
|
+
sessions,
|
|
476
|
+
selectedSessionKey,
|
|
477
|
+
onSelectSession: handleSelectSession,
|
|
478
|
+
onCreateSession: createNewSession,
|
|
479
|
+
sessionTitle: sessionDisplayName,
|
|
480
|
+
isLoading: sessionsQuery.isLoading,
|
|
481
|
+
query,
|
|
482
|
+
onQueryChange: setQuery
|
|
483
|
+
};
|
|
484
|
+
|
|
485
|
+
const conversationProps: ComponentProps<typeof ChatConversationPanel> = {
|
|
486
|
+
modelOptions,
|
|
487
|
+
selectedModel,
|
|
488
|
+
onSelectedModelChange: setSelectedModel,
|
|
489
|
+
onGoToProviders: goToProviders,
|
|
490
|
+
skillRecords,
|
|
491
|
+
isSkillsLoading: installedSkillsQuery.isLoading,
|
|
492
|
+
selectedSkills,
|
|
493
|
+
onSelectedSkillsChange: setSelectedSkills,
|
|
494
|
+
selectedSessionKey,
|
|
495
|
+
sessionDisplayName: currentSessionDisplayName,
|
|
496
|
+
canDeleteSession: Boolean(selectedSession),
|
|
497
|
+
isDeletePending: deleteSession.isPending,
|
|
498
|
+
onDeleteSession: () => {
|
|
499
|
+
void handleDeleteSession();
|
|
500
|
+
},
|
|
501
|
+
onCreateSession: createNewSession,
|
|
502
|
+
threadRef,
|
|
503
|
+
onThreadScroll: handleScroll,
|
|
504
|
+
isHistoryLoading: historyQuery.isLoading,
|
|
505
|
+
mergedEvents,
|
|
506
|
+
isSending,
|
|
507
|
+
isAwaitingAssistantOutput,
|
|
508
|
+
streamingAssistantText,
|
|
509
|
+
draft,
|
|
510
|
+
onDraftChange: setDraft,
|
|
511
|
+
onSend: handleSend,
|
|
512
|
+
onStop: () => {
|
|
513
|
+
void stopCurrentRun();
|
|
514
|
+
},
|
|
515
|
+
canStopGeneration: canStopCurrentRun,
|
|
516
|
+
stopDisabledReason,
|
|
517
|
+
sendError: lastSendError,
|
|
518
|
+
queuedCount
|
|
519
|
+
};
|
|
285
520
|
|
|
286
521
|
return (
|
|
287
|
-
<
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
<Button variant="outline" size="sm" onClick={() => historyQuery.refetch()} className="rounded-lg">
|
|
294
|
-
<RefreshCw className={cn('h-3.5 w-3.5 mr-1.5', historyQuery.isFetching && 'animate-spin')} />
|
|
295
|
-
{t('chatRefresh')}
|
|
296
|
-
</Button>
|
|
297
|
-
<Button variant="primary" size="sm" onClick={createNewSession} className="rounded-lg">
|
|
298
|
-
<Plus className="h-3.5 w-3.5 mr-1.5" />
|
|
299
|
-
{t('chatNewSession')}
|
|
300
|
-
</Button>
|
|
301
|
-
</div>
|
|
302
|
-
}
|
|
303
|
-
/>
|
|
304
|
-
|
|
305
|
-
<div className="flex-1 min-h-0 flex gap-4 max-lg:flex-col">
|
|
306
|
-
<ChatSessionsSidebar
|
|
307
|
-
query={query}
|
|
308
|
-
onQueryChange={setQuery}
|
|
309
|
-
selectedChannel={selectedChannel}
|
|
310
|
-
onSelectedChannelChange={setSelectedChannel}
|
|
311
|
-
channelOptions={channelOptions}
|
|
312
|
-
channelLabel={displayChannelName}
|
|
313
|
-
isLoading={sessionsQuery.isLoading}
|
|
314
|
-
isRefreshing={sessionsQuery.isFetching}
|
|
315
|
-
sessions={filteredSessions}
|
|
316
|
-
selectedSessionKey={selectedSessionKey}
|
|
317
|
-
onSelectSession={setSelectedSessionKey}
|
|
318
|
-
sessionTitle={sessionDisplayName}
|
|
319
|
-
onRefresh={() => {
|
|
320
|
-
void sessionsQuery.refetch();
|
|
321
|
-
}}
|
|
322
|
-
onCreateSession={createNewSession}
|
|
323
|
-
/>
|
|
324
|
-
|
|
325
|
-
<ChatConversationPanel
|
|
326
|
-
agentOptions={agentOptions}
|
|
327
|
-
selectedAgentId={selectedAgentId}
|
|
328
|
-
onSelectedAgentIdChange={setSelectedAgentId}
|
|
329
|
-
selectedSessionKey={selectedSessionKey}
|
|
330
|
-
canDeleteSession={Boolean(selectedSession)}
|
|
331
|
-
isDeletePending={deleteSession.isPending}
|
|
332
|
-
onDeleteSession={() => {
|
|
333
|
-
void handleDeleteSession();
|
|
334
|
-
}}
|
|
335
|
-
threadRef={threadRef}
|
|
336
|
-
onThreadScroll={handleScroll}
|
|
337
|
-
isHistoryLoading={historyQuery.isLoading}
|
|
338
|
-
mergedEvents={mergedEvents}
|
|
339
|
-
isSending={isSending}
|
|
340
|
-
isAwaitingAssistantOutput={isAwaitingAssistantOutput}
|
|
341
|
-
streamingAssistantText={streamingAssistantText}
|
|
342
|
-
draft={draft}
|
|
343
|
-
onDraftChange={setDraft}
|
|
344
|
-
onSend={handleSend}
|
|
345
|
-
queuedCount={queuedCount}
|
|
346
|
-
/>
|
|
347
|
-
</div>
|
|
348
|
-
<ConfirmDialog />
|
|
349
|
-
</PageLayout>
|
|
522
|
+
<ChatPageLayout
|
|
523
|
+
view={view}
|
|
524
|
+
sidebarProps={sidebarProps}
|
|
525
|
+
conversationProps={conversationProps}
|
|
526
|
+
confirmDialog={<ConfirmDialog />}
|
|
527
|
+
/>
|
|
350
528
|
);
|
|
351
529
|
}
|