@nextclaw/ui 0.5.48 → 0.6.0
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-BWQYaOuz.js +1 -0
- package/dist/assets/ChatPage-DsIuF-TC.js +32 -0
- package/dist/assets/DocBrowser-D4pXQDKt.js +1 -0
- package/dist/assets/MarketplacePage-Cj1HGbGe.js +49 -0
- package/dist/assets/ModelConfig-C2f3h7yq.js +1 -0
- package/dist/assets/{ProvidersList-BXHpjVtO.js → ProvidersList-DUdQEMNV.js} +1 -1
- package/dist/assets/RuntimeConfig-BnR60m9J.js +1 -0
- package/dist/assets/{SecretsConfig-KkgMzdt1.js → SecretsConfig-CXV017VN.js} +2 -2
- package/dist/assets/SessionsConfig-DsgHhuYe.js +2 -0
- package/dist/assets/{card-D7NY0Szf.js → card-B7d3Z9Y7.js} +1 -1
- package/dist/assets/index-Dp6x_DHf.js +2 -0
- package/dist/assets/index-DsQL2mtx.css +1 -0
- package/dist/assets/{label-Ojs7Al6B.js → label-Dlq0AZXx.js} +1 -1
- package/dist/assets/{logos-B1qBsCSi.js → logos-CSTJsbua.js} +1 -1
- package/dist/assets/{page-layout-CUMMO0nN.js → page-layout-DeBYaT_B.js} +1 -1
- package/dist/assets/provider-models-y4mUDcGF.js +1 -0
- package/dist/assets/{switch-BdhS_16-.js → switch-DwDE9PLr.js} +1 -1
- package/dist/assets/{tabs-custom-D261E5EA.js → tabs-custom-DqY_ht59.js} +1 -1
- package/dist/assets/useConfig-BiM-oO9i.js +6 -0
- package/dist/assets/{useConfirmDialog-BUKGHDL6.js → useConfirmDialog-BEFIWczY.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 +75 -86
- package/src/components/chat/ChatInputBar.tsx +226 -0
- package/src/components/chat/ChatPage.tsx +359 -188
- 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 +47 -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,32 @@ 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]);
|
|
213
|
-
|
|
214
|
-
const isNearBottom = useCallback(() => {
|
|
215
|
-
const element = threadRef.current;
|
|
216
|
-
if (!element) return true;
|
|
217
|
-
const threshold = 50;
|
|
218
|
-
return element.scrollHeight - element.scrollTop - element.clientHeight < threshold;
|
|
219
|
-
}, []);
|
|
220
|
-
|
|
221
|
-
const handleScroll = useCallback(() => {
|
|
222
|
-
if (isNearBottom()) {
|
|
223
|
-
isUserScrollingRef.current = false;
|
|
224
|
-
} else {
|
|
225
|
-
isUserScrollingRef.current = true;
|
|
226
|
-
}
|
|
227
|
-
}, [isNearBottom]);
|
|
373
|
+
useChatSessionSync({
|
|
374
|
+
view,
|
|
375
|
+
routeSessionKey,
|
|
376
|
+
selectedSessionKey,
|
|
377
|
+
selectedAgentId,
|
|
378
|
+
setSelectedSessionKey,
|
|
379
|
+
setSelectedAgentId,
|
|
380
|
+
selectedSessionKeyRef,
|
|
381
|
+
isUserScrollingRef,
|
|
382
|
+
resetStreamState
|
|
383
|
+
});
|
|
228
384
|
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
}, [mergedEvents, isSending]);
|
|
385
|
+
const { handleScroll } = useChatThreadScroll({
|
|
386
|
+
threadRef,
|
|
387
|
+
isUserScrollingRef,
|
|
388
|
+
mergedEvents,
|
|
389
|
+
isSending
|
|
390
|
+
});
|
|
236
391
|
|
|
237
392
|
const createNewSession = useCallback(() => {
|
|
238
393
|
resetStreamState();
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
394
|
+
setSelectedSessionKey(null);
|
|
395
|
+
if (location.pathname !== '/chat') {
|
|
396
|
+
navigate('/chat');
|
|
397
|
+
}
|
|
398
|
+
}, [location.pathname, navigate, resetStreamState]);
|
|
242
399
|
|
|
243
400
|
const handleDeleteSession = useCallback(async () => {
|
|
244
401
|
if (!selectedSessionKey) {
|
|
@@ -258,94 +415,108 @@ export function ChatPage() {
|
|
|
258
415
|
onSuccess: async () => {
|
|
259
416
|
resetStreamState();
|
|
260
417
|
setSelectedSessionKey(null);
|
|
418
|
+
navigate('/chat', { replace: true });
|
|
261
419
|
await sessionsQuery.refetch();
|
|
262
420
|
}
|
|
263
421
|
}
|
|
264
422
|
);
|
|
265
|
-
}, [confirm, deleteSession, resetStreamState, selectedSessionKey, sessionsQuery]);
|
|
423
|
+
}, [confirm, deleteSession, navigate, resetStreamState, selectedSessionKey, sessionsQuery]);
|
|
266
424
|
|
|
267
425
|
const handleSend = useCallback(async () => {
|
|
268
426
|
const message = draft.trim();
|
|
269
427
|
if (!message) {
|
|
270
428
|
return;
|
|
271
429
|
}
|
|
430
|
+
const requestedSkills = selectedSkills;
|
|
272
431
|
|
|
273
432
|
const sessionKey = selectedSessionKey ?? buildNewSessionKey(selectedAgentId);
|
|
274
433
|
if (!selectedSessionKey) {
|
|
275
|
-
|
|
434
|
+
navigate(buildSessionPath(sessionKey), { replace: true });
|
|
276
435
|
}
|
|
277
436
|
setDraft('');
|
|
437
|
+
setSelectedSkills([]);
|
|
278
438
|
await sendMessage({
|
|
279
439
|
message,
|
|
280
440
|
sessionKey,
|
|
281
441
|
agentId: selectedAgentId,
|
|
442
|
+
model: selectedModel || undefined,
|
|
443
|
+
stopSupported: chatCapabilitiesQuery.data?.stopSupported ?? false,
|
|
444
|
+
stopReason: chatCapabilitiesQuery.data?.stopReason,
|
|
445
|
+
requestedSkills,
|
|
282
446
|
restoreDraftOnError: true
|
|
283
447
|
});
|
|
284
|
-
}, [
|
|
448
|
+
}, [
|
|
449
|
+
chatCapabilitiesQuery.data?.stopReason,
|
|
450
|
+
chatCapabilitiesQuery.data?.stopSupported,
|
|
451
|
+
draft,
|
|
452
|
+
selectedAgentId,
|
|
453
|
+
selectedModel,
|
|
454
|
+
navigate,
|
|
455
|
+
selectedSessionKey,
|
|
456
|
+
selectedSkills,
|
|
457
|
+
sendMessage
|
|
458
|
+
]);
|
|
459
|
+
|
|
460
|
+
const currentSessionDisplayName = selectedSession ? sessionDisplayName(selectedSession) : undefined;
|
|
461
|
+
const handleSelectSession = useCallback((nextSessionKey: string) => {
|
|
462
|
+
const target = buildSessionPath(nextSessionKey);
|
|
463
|
+
if (location.pathname !== target) {
|
|
464
|
+
navigate(target);
|
|
465
|
+
}
|
|
466
|
+
}, [location.pathname, navigate]);
|
|
467
|
+
|
|
468
|
+
const sidebarProps: ComponentProps<typeof ChatSidebar> = {
|
|
469
|
+
sessions,
|
|
470
|
+
selectedSessionKey,
|
|
471
|
+
onSelectSession: handleSelectSession,
|
|
472
|
+
onCreateSession: createNewSession,
|
|
473
|
+
sessionTitle: sessionDisplayName,
|
|
474
|
+
isLoading: sessionsQuery.isLoading,
|
|
475
|
+
query,
|
|
476
|
+
onQueryChange: setQuery
|
|
477
|
+
};
|
|
478
|
+
|
|
479
|
+
const conversationProps: ComponentProps<typeof ChatConversationPanel> = {
|
|
480
|
+
modelOptions,
|
|
481
|
+
selectedModel,
|
|
482
|
+
onSelectedModelChange: setSelectedModel,
|
|
483
|
+
skillRecords,
|
|
484
|
+
isSkillsLoading: installedSkillsQuery.isLoading,
|
|
485
|
+
selectedSkills,
|
|
486
|
+
onSelectedSkillsChange: setSelectedSkills,
|
|
487
|
+
selectedSessionKey,
|
|
488
|
+
sessionDisplayName: currentSessionDisplayName,
|
|
489
|
+
canDeleteSession: Boolean(selectedSession),
|
|
490
|
+
isDeletePending: deleteSession.isPending,
|
|
491
|
+
onDeleteSession: () => {
|
|
492
|
+
void handleDeleteSession();
|
|
493
|
+
},
|
|
494
|
+
onCreateSession: createNewSession,
|
|
495
|
+
threadRef,
|
|
496
|
+
onThreadScroll: handleScroll,
|
|
497
|
+
isHistoryLoading: historyQuery.isLoading,
|
|
498
|
+
mergedEvents,
|
|
499
|
+
isSending,
|
|
500
|
+
isAwaitingAssistantOutput,
|
|
501
|
+
streamingAssistantText,
|
|
502
|
+
draft,
|
|
503
|
+
onDraftChange: setDraft,
|
|
504
|
+
onSend: handleSend,
|
|
505
|
+
onStop: () => {
|
|
506
|
+
void stopCurrentRun();
|
|
507
|
+
},
|
|
508
|
+
canStopGeneration: canStopCurrentRun,
|
|
509
|
+
stopDisabledReason,
|
|
510
|
+
sendError: lastSendError,
|
|
511
|
+
queuedCount
|
|
512
|
+
};
|
|
285
513
|
|
|
286
514
|
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>
|
|
515
|
+
<ChatPageLayout
|
|
516
|
+
view={view}
|
|
517
|
+
sidebarProps={sidebarProps}
|
|
518
|
+
conversationProps={conversationProps}
|
|
519
|
+
confirmDialog={<ConfirmDialog />}
|
|
520
|
+
/>
|
|
350
521
|
);
|
|
351
522
|
}
|