@nextclaw/ui 0.6.10 → 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 +9 -0
- package/dist/assets/{ChannelsList-TyMb5Mgz.js → ChannelsList-C49JQ-Zt.js} +1 -1
- package/dist/assets/ChatPage-DIx05c6s.js +36 -0
- package/dist/assets/{DocBrowser-CNtrA0ps.js → DocBrowser-CpOosDEI.js} +1 -1
- package/dist/assets/{LogoBadge-BLqiOM5D.js → LogoBadge-CL_8ZPXU.js} +1 -1
- package/dist/assets/MarketplacePage-BOzko5s9.js +49 -0
- package/dist/assets/{ModelConfig-CCsQ8KFq.js → ModelConfig-BZ4ZfaQB.js} +1 -1
- package/dist/assets/ProvidersList-fPpJ5gl6.js +1 -0
- package/dist/assets/{RuntimeConfig-BO6s-ls-.js → RuntimeConfig-Dt9pLB9P.js} +1 -1
- package/dist/assets/{SecretsConfig-mayFdxpM.js → SecretsConfig-C1PU0Yy8.js} +2 -2
- package/dist/assets/{SessionsConfig-DAIczdBj.js → SessionsConfig-EskBOofQ.js} +2 -2
- package/dist/assets/{card-BP5YnL-G.js → card-C7Gtw2Vs.js} +1 -1
- package/dist/assets/index-Cn6_2To7.js +8 -0
- package/dist/assets/{index-BUiahmWm.css → index-nEYGCJTC.css} +1 -1
- package/dist/assets/{input-B1D2QX0O.js → input-oBvxsnV9.js} +1 -1
- package/dist/assets/{label-DW0j-fXA.js → label-C7F8lMpQ.js} +1 -1
- package/dist/assets/{page-layout-Ch-H9gD-.js → page-layout-DO8BlScF.js} +1 -1
- package/dist/assets/session-run-status-Kg0FwAPn.js +3 -0
- package/dist/assets/{switch-_cZHlGKB.js → switch-C6a5GyZB.js} +1 -1
- package/dist/assets/{tabs-custom-ARxqYYjG.js → tabs-custom-BatFap5k.js} +1 -1
- package/dist/assets/{useConfirmDialog-BaU7nIat.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 +199 -200
- package/src/api/types.ts +36 -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/hooks/useConfig.ts +31 -1
- package/src/hooks/useObservable.ts +20 -0
- package/src/lib/chat-message.ts +2 -202
- package/src/lib/chat-runtime-utils.ts +250 -0
- package/src/lib/i18n.ts +9 -0
- package/tsconfig.json +2 -1
- package/vite.config.ts +2 -1
- package/dist/assets/ChatPage-CQerYqvy.js +0 -34
- package/dist/assets/MarketplacePage-CotZxxNe.js +0 -49
- package/dist/assets/ProvidersList-BYYX5K_g.js +0 -1
- package/dist/assets/index-D6_5HaDl.js +0 -7
- package/dist/assets/session-run-status-BUYsQeWs.js +0 -5
- package/src/components/chat/ChatInputBar.tsx +0 -590
- package/src/components/chat/useChatStreamController.ts +0 -591
|
@@ -1,97 +1,20 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import type {
|
|
3
|
-
import type { SessionEntryView, SessionEventView } from '@/api/types';
|
|
4
|
-
import {
|
|
5
|
-
useChatCapabilities,
|
|
6
|
-
useConfig,
|
|
7
|
-
useConfigMeta,
|
|
8
|
-
useDeleteSession,
|
|
9
|
-
useSessionHistory,
|
|
10
|
-
useSessions,
|
|
11
|
-
useChatRuns,
|
|
12
|
-
} from '@/hooks/useConfig';
|
|
13
|
-
import { useMarketplaceInstalled } from '@/hooks/useMarketplace';
|
|
1
|
+
import { useEffect, useMemo, useRef, useState } from 'react';
|
|
2
|
+
import type { Dispatch, MutableRefObject, SetStateAction } from 'react';
|
|
14
3
|
import { useConfirmDialog } from '@/hooks/useConfirmDialog';
|
|
15
|
-
import type { ChatModelOption } from '@/components/chat/ChatInputBar';
|
|
16
4
|
import { ChatSidebar } from '@/components/chat/ChatSidebar';
|
|
17
5
|
import { ChatConversationPanel } from '@/components/chat/ChatConversationPanel';
|
|
18
6
|
import { CronConfig } from '@/components/config/CronConfig';
|
|
19
7
|
import { MarketplacePage } from '@/components/marketplace/MarketplacePage';
|
|
20
|
-
import {
|
|
21
|
-
import {
|
|
22
|
-
import {
|
|
23
|
-
import {
|
|
24
|
-
import {
|
|
8
|
+
import { useSessionRunStatus } from '@/components/chat/chat-page-runtime';
|
|
9
|
+
import { useChatRuntimeController } from '@/components/chat/useChatRuntimeController';
|
|
10
|
+
import { parseSessionKeyFromRoute, resolveAgentIdFromSessionKey } from '@/components/chat/chat-session-route';
|
|
11
|
+
import { useChatPageData, sessionDisplayName } from '@/components/chat/chat-page-data';
|
|
12
|
+
import { ChatPresenterProvider } from '@/components/chat/presenter/chat-presenter-context';
|
|
13
|
+
import { ChatPresenter } from '@/components/chat/presenter/chat.presenter';
|
|
14
|
+
import { useChatInputStore } from '@/components/chat/stores/chat-input.store';
|
|
15
|
+
import { useChatSessionListStore } from '@/components/chat/stores/chat-session-list.store';
|
|
25
16
|
import { useLocation, useNavigate, useParams } from 'react-router-dom';
|
|
26
17
|
|
|
27
|
-
const SESSION_ROUTE_PREFIX = 'sid_';
|
|
28
|
-
|
|
29
|
-
function resolveAgentIdFromSessionKey(sessionKey: string): string | null {
|
|
30
|
-
const match = /^agent:([^:]+):/i.exec(sessionKey.trim());
|
|
31
|
-
if (!match) {
|
|
32
|
-
return null;
|
|
33
|
-
}
|
|
34
|
-
const value = match[1]?.trim();
|
|
35
|
-
return value ? value : null;
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
function buildNewSessionKey(agentId: string): string {
|
|
39
|
-
const slug = Math.random().toString(36).slice(2, 8);
|
|
40
|
-
return `agent:${agentId}:ui:direct:web-${Date.now().toString(36)}${slug}`;
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
function encodeSessionRouteId(sessionKey: string): string {
|
|
44
|
-
const bytes = new TextEncoder().encode(sessionKey);
|
|
45
|
-
let binary = '';
|
|
46
|
-
for (const byte of bytes) {
|
|
47
|
-
binary += String.fromCharCode(byte);
|
|
48
|
-
}
|
|
49
|
-
const base64 = btoa(binary).replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/g, '');
|
|
50
|
-
return `${SESSION_ROUTE_PREFIX}${base64}`;
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
function decodeSessionRouteId(routeValue: string): string | null {
|
|
54
|
-
if (!routeValue.startsWith(SESSION_ROUTE_PREFIX)) {
|
|
55
|
-
return null;
|
|
56
|
-
}
|
|
57
|
-
const encoded = routeValue.slice(SESSION_ROUTE_PREFIX.length).replace(/-/g, '+').replace(/_/g, '/');
|
|
58
|
-
const padding = encoded.length % 4 === 0 ? '' : '='.repeat(4 - (encoded.length % 4));
|
|
59
|
-
try {
|
|
60
|
-
const binary = atob(encoded + padding);
|
|
61
|
-
const bytes = Uint8Array.from(binary, (char) => char.charCodeAt(0));
|
|
62
|
-
return new TextDecoder().decode(bytes);
|
|
63
|
-
} catch {
|
|
64
|
-
return null;
|
|
65
|
-
}
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
function parseSessionKeyFromRoute(routeValue?: string): string | null {
|
|
69
|
-
if (!routeValue) {
|
|
70
|
-
return null;
|
|
71
|
-
}
|
|
72
|
-
const decodedToken = decodeSessionRouteId(routeValue);
|
|
73
|
-
if (decodedToken) {
|
|
74
|
-
return decodedToken;
|
|
75
|
-
}
|
|
76
|
-
try {
|
|
77
|
-
return decodeURIComponent(routeValue);
|
|
78
|
-
} catch {
|
|
79
|
-
return routeValue;
|
|
80
|
-
}
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
function buildSessionPath(sessionKey: string): string {
|
|
84
|
-
return `/chat/${encodeSessionRouteId(sessionKey)}`;
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
function sessionDisplayName(session: SessionEntryView): string {
|
|
88
|
-
if (session.label && session.label.trim()) {
|
|
89
|
-
return session.label.trim();
|
|
90
|
-
}
|
|
91
|
-
const chunks = session.key.split(':');
|
|
92
|
-
return chunks[chunks.length - 1] || session.key;
|
|
93
|
-
}
|
|
94
|
-
|
|
95
18
|
type MainPanelView = 'chat' | 'cron' | 'skills';
|
|
96
19
|
|
|
97
20
|
type ChatPageProps = {
|
|
@@ -106,7 +29,6 @@ type UseSessionSyncParams = {
|
|
|
106
29
|
setSelectedSessionKey: Dispatch<SetStateAction<string | null>>;
|
|
107
30
|
setSelectedAgentId: Dispatch<SetStateAction<string>>;
|
|
108
31
|
selectedSessionKeyRef: MutableRefObject<string | null>;
|
|
109
|
-
isUserScrollingRef: MutableRefObject<boolean>;
|
|
110
32
|
resetStreamState: () => void;
|
|
111
33
|
};
|
|
112
34
|
|
|
@@ -119,7 +41,6 @@ function useChatSessionSync(params: UseSessionSyncParams): void {
|
|
|
119
41
|
setSelectedSessionKey,
|
|
120
42
|
setSelectedAgentId,
|
|
121
43
|
selectedSessionKeyRef,
|
|
122
|
-
isUserScrollingRef,
|
|
123
44
|
resetStreamState
|
|
124
45
|
} = params;
|
|
125
46
|
|
|
@@ -151,62 +72,21 @@ function useChatSessionSync(params: UseSessionSyncParams): void {
|
|
|
151
72
|
|
|
152
73
|
useEffect(() => {
|
|
153
74
|
selectedSessionKeyRef.current = selectedSessionKey;
|
|
154
|
-
|
|
155
|
-
}, [isUserScrollingRef, selectedSessionKey, selectedSessionKeyRef]);
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
type UseThreadScrollParams = {
|
|
159
|
-
threadRef: MutableRefObject<HTMLDivElement | null>;
|
|
160
|
-
isUserScrollingRef: MutableRefObject<boolean>;
|
|
161
|
-
mergedEvents: SessionEventView[];
|
|
162
|
-
isSending: boolean;
|
|
163
|
-
};
|
|
164
|
-
|
|
165
|
-
function useChatThreadScroll(params: UseThreadScrollParams): { handleScroll: () => void } {
|
|
166
|
-
const { threadRef, isUserScrollingRef, mergedEvents, isSending } = params;
|
|
167
|
-
|
|
168
|
-
const isNearBottom = useCallback(() => {
|
|
169
|
-
const element = threadRef.current;
|
|
170
|
-
if (!element) {
|
|
171
|
-
return true;
|
|
172
|
-
}
|
|
173
|
-
const threshold = 50;
|
|
174
|
-
return element.scrollHeight - element.scrollTop - element.clientHeight < threshold;
|
|
175
|
-
}, [threadRef]);
|
|
176
|
-
|
|
177
|
-
const handleScroll = useCallback(() => {
|
|
178
|
-
if (isNearBottom()) {
|
|
179
|
-
isUserScrollingRef.current = false;
|
|
180
|
-
} else {
|
|
181
|
-
isUserScrollingRef.current = true;
|
|
182
|
-
}
|
|
183
|
-
}, [isNearBottom, isUserScrollingRef]);
|
|
184
|
-
|
|
185
|
-
useEffect(() => {
|
|
186
|
-
const element = threadRef.current;
|
|
187
|
-
if (!element || isUserScrollingRef.current) {
|
|
188
|
-
return;
|
|
189
|
-
}
|
|
190
|
-
element.scrollTop = element.scrollHeight;
|
|
191
|
-
}, [isSending, isUserScrollingRef, mergedEvents, threadRef]);
|
|
192
|
-
|
|
193
|
-
return { handleScroll };
|
|
75
|
+
}, [selectedSessionKey, selectedSessionKeyRef]);
|
|
194
76
|
}
|
|
195
77
|
|
|
196
78
|
type ChatPageLayoutProps = {
|
|
197
79
|
view: MainPanelView;
|
|
198
|
-
sidebarProps: ComponentProps<typeof ChatSidebar>;
|
|
199
|
-
conversationProps: ComponentProps<typeof ChatConversationPanel>;
|
|
200
80
|
confirmDialog: JSX.Element;
|
|
201
81
|
};
|
|
202
82
|
|
|
203
|
-
function ChatPageLayout({ view,
|
|
83
|
+
function ChatPageLayout({ view, confirmDialog }: ChatPageLayoutProps) {
|
|
204
84
|
return (
|
|
205
85
|
<div className="h-full flex">
|
|
206
|
-
<ChatSidebar
|
|
86
|
+
<ChatSidebar />
|
|
207
87
|
|
|
208
88
|
{view === 'chat' ? (
|
|
209
|
-
<ChatConversationPanel
|
|
89
|
+
<ChatConversationPanel />
|
|
210
90
|
) : (
|
|
211
91
|
<section className="flex-1 min-h-0 overflow-hidden bg-gradient-to-b from-gray-50/60 to-white">
|
|
212
92
|
{view === 'cron' ? (
|
|
@@ -231,356 +111,204 @@ function ChatPageLayout({ view, sidebarProps, conversationProps, confirmDialog }
|
|
|
231
111
|
}
|
|
232
112
|
|
|
233
113
|
export function ChatPage({ view }: ChatPageProps) {
|
|
234
|
-
const [
|
|
235
|
-
const
|
|
236
|
-
const
|
|
237
|
-
const
|
|
238
|
-
const
|
|
239
|
-
const [selectedSkills, setSelectedSkills] = useState<string[]>([]);
|
|
240
|
-
|
|
114
|
+
const [presenter] = useState(() => new ChatPresenter());
|
|
115
|
+
const query = useChatSessionListStore((state) => state.snapshot.query);
|
|
116
|
+
const selectedSessionKey = useChatSessionListStore((state) => state.snapshot.selectedSessionKey);
|
|
117
|
+
const selectedAgentId = useChatSessionListStore((state) => state.snapshot.selectedAgentId);
|
|
118
|
+
const pendingSessionType = useChatInputStore((state) => state.snapshot.pendingSessionType);
|
|
241
119
|
const { confirm, ConfirmDialog } = useConfirmDialog();
|
|
242
120
|
const location = useLocation();
|
|
243
121
|
const navigate = useNavigate();
|
|
244
122
|
const { sessionId: routeSessionIdParam } = useParams<{ sessionId?: string }>();
|
|
245
123
|
const threadRef = useRef<HTMLDivElement | null>(null);
|
|
246
|
-
const isUserScrollingRef = useRef(false);
|
|
247
124
|
const selectedSessionKeyRef = useRef<string | null>(selectedSessionKey);
|
|
248
125
|
const routeSessionKey = useMemo(
|
|
249
126
|
() => parseSessionKeyFromRoute(routeSessionIdParam),
|
|
250
127
|
[routeSessionIdParam]
|
|
251
128
|
);
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
129
|
+
const {
|
|
130
|
+
sessionsQuery,
|
|
131
|
+
installedSkillsQuery,
|
|
132
|
+
chatCapabilitiesQuery,
|
|
133
|
+
historyQuery,
|
|
134
|
+
isProviderStateResolved,
|
|
135
|
+
modelOptions,
|
|
136
|
+
sessions,
|
|
137
|
+
skillRecords,
|
|
138
|
+
selectedSession,
|
|
139
|
+
historyMessages,
|
|
140
|
+
sessionTypeOptions,
|
|
141
|
+
defaultSessionType,
|
|
142
|
+
selectedSessionType,
|
|
143
|
+
canEditSessionType,
|
|
144
|
+
sessionTypeUnavailable,
|
|
145
|
+
sessionTypeUnavailableMessage
|
|
146
|
+
} = useChatPageData({
|
|
147
|
+
query,
|
|
148
|
+
selectedSessionKey,
|
|
149
|
+
selectedAgentId,
|
|
150
|
+
pendingSessionType,
|
|
151
|
+
setPendingSessionType: presenter.chatInputManager.setPendingSessionType,
|
|
152
|
+
setSelectedModel: presenter.chatInputManager.setSelectedModel
|
|
263
153
|
});
|
|
264
|
-
const historyQuery = useSessionHistory(selectedSessionKey, 300);
|
|
265
|
-
const deleteSession = useDeleteSession();
|
|
266
|
-
|
|
267
|
-
const modelOptions = useMemo<ChatModelOption[]>(() => {
|
|
268
|
-
const providers = buildProviderModelCatalog({
|
|
269
|
-
meta: configMetaQuery.data,
|
|
270
|
-
config: configQuery.data,
|
|
271
|
-
onlyConfigured: true
|
|
272
|
-
});
|
|
273
|
-
const seen = new Set<string>();
|
|
274
|
-
const options: ChatModelOption[] = [];
|
|
275
|
-
for (const provider of providers) {
|
|
276
|
-
for (const localModel of provider.models) {
|
|
277
|
-
const value = composeProviderModel(provider.prefix, localModel);
|
|
278
|
-
if (!value || seen.has(value)) {
|
|
279
|
-
continue;
|
|
280
|
-
}
|
|
281
|
-
seen.add(value);
|
|
282
|
-
options.push({
|
|
283
|
-
value,
|
|
284
|
-
modelLabel: localModel,
|
|
285
|
-
providerLabel: provider.displayName
|
|
286
|
-
});
|
|
287
|
-
}
|
|
288
|
-
}
|
|
289
|
-
return options.sort((left, right) => {
|
|
290
|
-
const providerCompare = left.providerLabel.localeCompare(right.providerLabel);
|
|
291
|
-
if (providerCompare !== 0) {
|
|
292
|
-
return providerCompare;
|
|
293
|
-
}
|
|
294
|
-
return left.modelLabel.localeCompare(right.modelLabel);
|
|
295
|
-
});
|
|
296
|
-
}, [configMetaQuery.data, configQuery.data]);
|
|
297
|
-
|
|
298
|
-
const sessions = useMemo(() => sessionsQuery.data?.sessions ?? [], [sessionsQuery.data?.sessions]);
|
|
299
|
-
const skillRecords = useMemo(() => installedSkillsQuery.data?.records ?? [], [installedSkillsQuery.data?.records]);
|
|
300
|
-
|
|
301
|
-
const selectedSession = useMemo(
|
|
302
|
-
() => sessions.find((session) => session.key === selectedSessionKey) ?? null,
|
|
303
|
-
[selectedSessionKey, sessions]
|
|
304
|
-
);
|
|
305
|
-
|
|
306
|
-
useEffect(() => {
|
|
307
|
-
if (modelOptions.length === 0) {
|
|
308
|
-
setSelectedModel('');
|
|
309
|
-
return;
|
|
310
|
-
}
|
|
311
|
-
setSelectedModel((prev) => {
|
|
312
|
-
if (modelOptions.some((option) => option.value === prev)) {
|
|
313
|
-
return prev;
|
|
314
|
-
}
|
|
315
|
-
const sessionPreferred = selectedSession?.preferredModel?.trim();
|
|
316
|
-
if (sessionPreferred && modelOptions.some((option) => option.value === sessionPreferred)) {
|
|
317
|
-
return sessionPreferred;
|
|
318
|
-
}
|
|
319
|
-
const fallback = configQuery.data?.agents.defaults.model?.trim();
|
|
320
|
-
if (fallback && modelOptions.some((option) => option.value === fallback)) {
|
|
321
|
-
return fallback;
|
|
322
|
-
}
|
|
323
|
-
return modelOptions[0]?.value ?? '';
|
|
324
|
-
});
|
|
325
|
-
}, [configQuery.data?.agents.defaults.model, modelOptions, selectedSession?.preferredModel]);
|
|
326
|
-
|
|
327
|
-
const historyData = historyQuery.data;
|
|
328
|
-
const historyMessages = historyData?.messages ?? [];
|
|
329
|
-
const historyEvents =
|
|
330
|
-
historyData?.events && historyData.events.length > 0
|
|
331
|
-
? historyData.events
|
|
332
|
-
: buildFallbackEventsFromMessages(historyMessages);
|
|
333
|
-
const nextOptimisticUserSeq = useMemo(
|
|
334
|
-
() => historyEvents.reduce((max, event) => (Number.isFinite(event.seq) ? Math.max(max, event.seq) : max), 0) + 1,
|
|
335
|
-
[historyEvents]
|
|
336
|
-
);
|
|
337
|
-
|
|
338
154
|
const {
|
|
339
|
-
|
|
340
|
-
streamingSessionEvents,
|
|
341
|
-
streamingAssistantText,
|
|
342
|
-
streamingAssistantTimestamp,
|
|
155
|
+
uiMessages,
|
|
343
156
|
isSending,
|
|
344
157
|
isAwaitingAssistantOutput,
|
|
345
|
-
queuedCount,
|
|
346
158
|
canStopCurrentRun,
|
|
347
159
|
stopDisabledReason,
|
|
348
160
|
lastSendError,
|
|
349
|
-
sendMessage,
|
|
350
|
-
resumeRun,
|
|
351
161
|
activeBackendRunId,
|
|
162
|
+
sendMessage,
|
|
352
163
|
stopCurrentRun,
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
? {
|
|
366
|
-
states: ['queued', 'running'],
|
|
367
|
-
limit: 200
|
|
368
|
-
}
|
|
369
|
-
: undefined
|
|
370
|
-
);
|
|
371
|
-
const activeRunBySessionKey = useMemo(
|
|
372
|
-
() => buildActiveRunBySessionKey(sessionStatusRunsQuery.data?.runs ?? []),
|
|
373
|
-
[sessionStatusRunsQuery.data?.runs]
|
|
374
|
-
);
|
|
375
|
-
const sessionRunStatusByKey = useMemo(
|
|
376
|
-
() => buildSessionRunStatusByKey(activeRunBySessionKey),
|
|
377
|
-
[activeRunBySessionKey]
|
|
164
|
+
resumeRun,
|
|
165
|
+
resetStreamState,
|
|
166
|
+
applyHistoryMessages
|
|
167
|
+
} = useChatRuntimeController(
|
|
168
|
+
{
|
|
169
|
+
selectedSessionKeyRef,
|
|
170
|
+
setSelectedSessionKey: presenter.chatSessionListManager.setSelectedSessionKey,
|
|
171
|
+
setDraft: presenter.chatInputManager.setDraft,
|
|
172
|
+
refetchSessions: sessionsQuery.refetch,
|
|
173
|
+
refetchHistory: historyQuery.refetch
|
|
174
|
+
},
|
|
175
|
+
presenter.chatController
|
|
378
176
|
);
|
|
379
|
-
const activeRun = useMemo(() => {
|
|
380
|
-
if (!selectedSessionKey) {
|
|
381
|
-
return null;
|
|
382
|
-
}
|
|
383
|
-
return activeRunBySessionKey.get(selectedSessionKey) ?? null;
|
|
384
|
-
}, [activeRunBySessionKey, selectedSessionKey]);
|
|
385
177
|
|
|
178
|
+
console.log('[ChatPage] uiMessages', { uiMessages, historyMessages });
|
|
386
179
|
useEffect(() => {
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
}, [
|
|
395
|
-
|
|
396
|
-
const mergedEvents = useMemo(() => {
|
|
397
|
-
const bySeq = new Map<number, SessionEventView>();
|
|
398
|
-
const append = (event: SessionEventView) => {
|
|
399
|
-
if (!Number.isFinite(event.seq)) {
|
|
400
|
-
return;
|
|
401
|
-
}
|
|
402
|
-
bySeq.set(event.seq, event);
|
|
403
|
-
};
|
|
404
|
-
|
|
405
|
-
historyEvents.forEach(append);
|
|
406
|
-
if (optimisticUserEvent) {
|
|
407
|
-
append(optimisticUserEvent);
|
|
408
|
-
}
|
|
409
|
-
streamingSessionEvents.forEach(append);
|
|
180
|
+
presenter.chatStreamActionsManager.bind({
|
|
181
|
+
sendMessage,
|
|
182
|
+
stopCurrentRun,
|
|
183
|
+
resumeRun,
|
|
184
|
+
resetStreamState,
|
|
185
|
+
applyHistoryMessages
|
|
186
|
+
});
|
|
187
|
+
}, [applyHistoryMessages, presenter, resetStreamState, resumeRun, sendMessage, stopCurrentRun]);
|
|
410
188
|
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
message: {
|
|
419
|
-
role: 'assistant',
|
|
420
|
-
content: streamingAssistantText,
|
|
421
|
-
timestamp: streamingAssistantTimestamp ?? new Date().toISOString()
|
|
422
|
-
}
|
|
423
|
-
});
|
|
424
|
-
}
|
|
425
|
-
return next;
|
|
426
|
-
}, [historyEvents, optimisticUserEvent, streamingAssistantText, streamingAssistantTimestamp, streamingSessionEvents]);
|
|
189
|
+
const { sessionRunStatusByKey } = useSessionRunStatus({
|
|
190
|
+
view,
|
|
191
|
+
selectedSessionKey,
|
|
192
|
+
activeBackendRunId,
|
|
193
|
+
isLocallyRunning: isSending || Boolean(activeBackendRunId),
|
|
194
|
+
resumeRun: presenter.chatStreamActionsManager.resumeRun
|
|
195
|
+
});
|
|
427
196
|
|
|
428
197
|
useChatSessionSync({
|
|
429
198
|
view,
|
|
430
199
|
routeSessionKey,
|
|
431
200
|
selectedSessionKey,
|
|
432
201
|
selectedAgentId,
|
|
433
|
-
setSelectedSessionKey,
|
|
434
|
-
setSelectedAgentId,
|
|
202
|
+
setSelectedSessionKey: presenter.chatSessionListManager.setSelectedSessionKey,
|
|
203
|
+
setSelectedAgentId: presenter.chatSessionListManager.setSelectedAgentId,
|
|
435
204
|
selectedSessionKeyRef,
|
|
436
|
-
|
|
437
|
-
resetStreamState
|
|
438
|
-
});
|
|
439
|
-
|
|
440
|
-
const { handleScroll } = useChatThreadScroll({
|
|
441
|
-
threadRef,
|
|
442
|
-
isUserScrollingRef,
|
|
443
|
-
mergedEvents,
|
|
444
|
-
isSending
|
|
205
|
+
resetStreamState: presenter.chatStreamActionsManager.resetStreamState
|
|
445
206
|
});
|
|
446
207
|
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
}
|
|
453
|
-
}, [location.pathname, navigate, resetStreamState]);
|
|
454
|
-
|
|
455
|
-
const goToProviders = useCallback(() => {
|
|
456
|
-
if (location.pathname !== '/providers') {
|
|
457
|
-
navigate('/providers');
|
|
458
|
-
}
|
|
459
|
-
}, [location.pathname, navigate]);
|
|
208
|
+
useEffect(() => {
|
|
209
|
+
presenter.chatStreamActionsManager.applyHistoryMessages(historyMessages, {
|
|
210
|
+
isLoading: historyQuery.isLoading
|
|
211
|
+
});
|
|
212
|
+
}, [historyMessages, historyQuery.isLoading, presenter]);
|
|
460
213
|
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
}
|
|
465
|
-
const confirmed = await confirm({
|
|
466
|
-
title: t('chatDeleteSessionConfirm'),
|
|
467
|
-
variant: 'destructive',
|
|
468
|
-
confirmLabel: t('delete')
|
|
214
|
+
useEffect(() => {
|
|
215
|
+
presenter.chatUiManager.syncState({
|
|
216
|
+
pathname: location.pathname
|
|
469
217
|
});
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
onSuccess: async () => {
|
|
477
|
-
resetStreamState();
|
|
478
|
-
setSelectedSessionKey(null);
|
|
479
|
-
navigate('/chat', { replace: true });
|
|
480
|
-
await sessionsQuery.refetch();
|
|
481
|
-
}
|
|
482
|
-
}
|
|
483
|
-
);
|
|
484
|
-
}, [confirm, deleteSession, navigate, resetStreamState, selectedSessionKey, sessionsQuery]);
|
|
218
|
+
presenter.chatUiManager.bindActions({
|
|
219
|
+
navigate,
|
|
220
|
+
confirm
|
|
221
|
+
});
|
|
222
|
+
}, [confirm, location.pathname, navigate, presenter]);
|
|
223
|
+
const currentSessionDisplayName = selectedSession ? sessionDisplayName(selectedSession) : undefined;
|
|
485
224
|
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
225
|
+
useEffect(() => {
|
|
226
|
+
presenter.chatThreadManager.bindActions({
|
|
227
|
+
refetchSessions: sessionsQuery.refetch
|
|
228
|
+
});
|
|
229
|
+
}, [
|
|
230
|
+
presenter,
|
|
231
|
+
sessionsQuery.refetch,
|
|
232
|
+
]);
|
|
492
233
|
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
await sendMessage({
|
|
500
|
-
message,
|
|
501
|
-
sessionKey,
|
|
502
|
-
agentId: selectedAgentId,
|
|
503
|
-
model: selectedModel || undefined,
|
|
234
|
+
useEffect(() => {
|
|
235
|
+
presenter.chatInputManager.syncSnapshot({
|
|
236
|
+
isProviderStateResolved,
|
|
237
|
+
defaultSessionType,
|
|
238
|
+
canStopGeneration: canStopCurrentRun,
|
|
239
|
+
stopDisabledReason,
|
|
504
240
|
stopSupported: chatCapabilitiesQuery.data?.stopSupported ?? false,
|
|
505
241
|
stopReason: chatCapabilitiesQuery.data?.stopReason,
|
|
506
|
-
|
|
507
|
-
|
|
242
|
+
sendError: lastSendError,
|
|
243
|
+
isSending,
|
|
244
|
+
modelOptions,
|
|
245
|
+
sessionTypeOptions,
|
|
246
|
+
selectedSessionType,
|
|
247
|
+
canEditSessionType,
|
|
248
|
+
sessionTypeUnavailable,
|
|
249
|
+
skillRecords,
|
|
250
|
+
isSkillsLoading: installedSkillsQuery.isLoading
|
|
251
|
+
});
|
|
252
|
+
presenter.chatSessionListManager.syncSnapshot({
|
|
253
|
+
sessions,
|
|
254
|
+
query,
|
|
255
|
+
isLoading: sessionsQuery.isLoading
|
|
256
|
+
});
|
|
257
|
+
presenter.chatRunStatusManager.syncSnapshot({
|
|
258
|
+
sessionRunStatusByKey,
|
|
259
|
+
isLocallyRunning: isSending || Boolean(activeBackendRunId),
|
|
260
|
+
activeBackendRunId
|
|
261
|
+
});
|
|
262
|
+
presenter.chatThreadManager.syncSnapshot({
|
|
263
|
+
isProviderStateResolved,
|
|
264
|
+
modelOptions,
|
|
265
|
+
sessionTypeUnavailable,
|
|
266
|
+
sessionTypeUnavailableMessage,
|
|
267
|
+
selectedSessionKey,
|
|
268
|
+
sessionDisplayName: currentSessionDisplayName,
|
|
269
|
+
canDeleteSession: Boolean(selectedSession),
|
|
270
|
+
threadRef,
|
|
271
|
+
isHistoryLoading: historyQuery.isLoading,
|
|
272
|
+
uiMessages,
|
|
273
|
+
isSending,
|
|
274
|
+
isAwaitingAssistantOutput
|
|
508
275
|
});
|
|
509
276
|
}, [
|
|
277
|
+
activeBackendRunId,
|
|
278
|
+
canEditSessionType,
|
|
279
|
+
canStopCurrentRun,
|
|
280
|
+
currentSessionDisplayName,
|
|
510
281
|
chatCapabilitiesQuery.data?.stopReason,
|
|
511
282
|
chatCapabilitiesQuery.data?.stopSupported,
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
selectedSessionKey,
|
|
517
|
-
selectedSkills,
|
|
518
|
-
sendMessage
|
|
519
|
-
]);
|
|
520
|
-
|
|
521
|
-
const currentSessionDisplayName = selectedSession ? sessionDisplayName(selectedSession) : undefined;
|
|
522
|
-
const handleSelectSession = useCallback((nextSessionKey: string) => {
|
|
523
|
-
const target = buildSessionPath(nextSessionKey);
|
|
524
|
-
if (location.pathname !== target) {
|
|
525
|
-
navigate(target);
|
|
526
|
-
}
|
|
527
|
-
}, [location.pathname, navigate]);
|
|
528
|
-
|
|
529
|
-
const sidebarProps: ComponentProps<typeof ChatSidebar> = {
|
|
530
|
-
sessions,
|
|
531
|
-
sessionRunStatusByKey,
|
|
532
|
-
selectedSessionKey,
|
|
533
|
-
onSelectSession: handleSelectSession,
|
|
534
|
-
onCreateSession: createNewSession,
|
|
535
|
-
sessionTitle: sessionDisplayName,
|
|
536
|
-
isLoading: sessionsQuery.isLoading,
|
|
537
|
-
query,
|
|
538
|
-
onQueryChange: setQuery
|
|
539
|
-
};
|
|
540
|
-
|
|
541
|
-
const conversationProps: ComponentProps<typeof ChatConversationPanel> = {
|
|
283
|
+
defaultSessionType,
|
|
284
|
+
historyQuery.isLoading,
|
|
285
|
+
installedSkillsQuery.isLoading,
|
|
286
|
+
isAwaitingAssistantOutput,
|
|
542
287
|
isProviderStateResolved,
|
|
288
|
+
isSending,
|
|
289
|
+
lastSendError,
|
|
290
|
+
uiMessages,
|
|
543
291
|
modelOptions,
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
skillRecords,
|
|
548
|
-
isSkillsLoading: installedSkillsQuery.isLoading,
|
|
549
|
-
selectedSkills,
|
|
550
|
-
onSelectedSkillsChange: setSelectedSkills,
|
|
292
|
+
presenter,
|
|
293
|
+
query,
|
|
294
|
+
selectedSession,
|
|
551
295
|
selectedSessionKey,
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
onThreadScroll: handleScroll,
|
|
561
|
-
isHistoryLoading: historyQuery.isLoading,
|
|
562
|
-
mergedEvents,
|
|
563
|
-
isSending,
|
|
564
|
-
isAwaitingAssistantOutput,
|
|
565
|
-
streamingAssistantText,
|
|
566
|
-
draft,
|
|
567
|
-
onDraftChange: setDraft,
|
|
568
|
-
onSend: handleSend,
|
|
569
|
-
onStop: () => {
|
|
570
|
-
void stopCurrentRun();
|
|
571
|
-
},
|
|
572
|
-
canStopGeneration: canStopCurrentRun,
|
|
296
|
+
selectedAgentId,
|
|
297
|
+
selectedSessionType,
|
|
298
|
+
sessionRunStatusByKey,
|
|
299
|
+
sessionTypeOptions,
|
|
300
|
+
sessionTypeUnavailable,
|
|
301
|
+
sessionTypeUnavailableMessage,
|
|
302
|
+
sessions,
|
|
303
|
+
sessionsQuery.isLoading,
|
|
573
304
|
stopDisabledReason,
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
305
|
+
threadRef,
|
|
306
|
+
skillRecords
|
|
307
|
+
]);
|
|
577
308
|
|
|
578
309
|
return (
|
|
579
|
-
<
|
|
580
|
-
view={view}
|
|
581
|
-
|
|
582
|
-
conversationProps={conversationProps}
|
|
583
|
-
confirmDialog={<ConfirmDialog />}
|
|
584
|
-
/>
|
|
310
|
+
<ChatPresenterProvider presenter={presenter}>
|
|
311
|
+
<ChatPageLayout view={view} confirmDialog={<ConfirmDialog />} />
|
|
312
|
+
</ChatPresenterProvider>
|
|
585
313
|
);
|
|
586
314
|
}
|