@nextclaw/ui 0.6.0 → 0.6.2
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 → ChannelsList-Bga6n85j.js} +1 -1
- package/dist/assets/ChatPage-B-Yk3kkv.js +32 -0
- package/dist/assets/{DocBrowser-D4pXQDKt.js → DocBrowser-dv57PRp5.js} +1 -1
- package/dist/assets/{MarketplacePage-Cj1HGbGe.js → MarketplacePage-j6p73Hjo.js} +1 -1
- package/dist/assets/{ModelConfig-C2f3h7yq.js → ModelConfig-BiKSDp5h.js} +1 -1
- package/dist/assets/{ProvidersList-DUdQEMNV.js → ProvidersList-B7ZfRUkD.js} +1 -1
- package/dist/assets/{RuntimeConfig-BnR60m9J.js → RuntimeConfig-Bpt9UNb6.js} +1 -1
- package/dist/assets/{SecretsConfig-CXV017VN.js → SecretsConfig-Ds00G-_O.js} +2 -2
- package/dist/assets/{SessionsConfig-DsgHhuYe.js → SessionsConfig-Mjet4opU.js} +1 -1
- package/dist/assets/{card-B7d3Z9Y7.js → card-C7JJ5BGA.js} +1 -1
- package/dist/assets/index-BiJ2xs5X.css +1 -0
- package/dist/assets/index-Cb9xiqC5.js +2 -0
- package/dist/assets/{label-Dlq0AZXx.js → label-DHJKdaUl.js} +1 -1
- package/dist/assets/{logos-CSTJsbua.js → logos-fPO_amyL.js} +1 -1
- package/dist/assets/{page-layout-DeBYaT_B.js → page-layout-CF0JQsWW.js} +1 -1
- package/dist/assets/{switch-DwDE9PLr.js → switch-C1hgy-fE.js} +1 -1
- package/dist/assets/{tabs-custom-DqY_ht59.js → tabs-custom-OyoLf5ZM.js} +1 -1
- package/dist/assets/useConfig-D_G46zbo.js +6 -0
- package/dist/assets/{useConfirmDialog-BEFIWczY.js → useConfirmDialog-_0u6i3cI.js} +1 -1
- package/dist/index.html +2 -2
- package/package.json +1 -1
- package/src/api/config.ts +77 -12
- package/src/api/types.ts +24 -0
- package/src/components/chat/ChatConversationPanel.tsx +20 -0
- package/src/components/chat/ChatInputBar.tsx +23 -4
- package/src/components/chat/ChatPage.tsx +56 -8
- package/src/components/chat/useChatStreamController.ts +192 -115
- package/src/hooks/useConfig.ts +29 -0
- package/src/hooks/useWebSocket.ts +21 -0
- package/src/lib/i18n.ts +7 -1
- package/dist/assets/ChatPage-DsIuF-TC.js +0 -32
- package/dist/assets/index-Dp6x_DHf.js +0 -2
- package/dist/assets/index-DsQL2mtx.css +0 -1
- package/dist/assets/useConfig-BiM-oO9i.js +0 -6
|
@@ -17,6 +17,7 @@ type ChatInputBarProps = {
|
|
|
17
17
|
onDraftChange: (value: string) => void;
|
|
18
18
|
onSend: () => Promise<void> | void;
|
|
19
19
|
onStop: () => Promise<void> | void;
|
|
20
|
+
onGoToProviders: () => void;
|
|
20
21
|
canStopGeneration: boolean;
|
|
21
22
|
stopDisabledReason?: string | null;
|
|
22
23
|
sendError?: string | null;
|
|
@@ -36,6 +37,7 @@ export function ChatInputBar({
|
|
|
36
37
|
onDraftChange,
|
|
37
38
|
onSend,
|
|
38
39
|
onStop,
|
|
40
|
+
onGoToProviders,
|
|
39
41
|
canStopGeneration,
|
|
40
42
|
stopDisabledReason = null,
|
|
41
43
|
sendError = null,
|
|
@@ -49,6 +51,8 @@ export function ChatInputBar({
|
|
|
49
51
|
selectedSkills,
|
|
50
52
|
onSelectedSkillsChange
|
|
51
53
|
}: ChatInputBarProps) {
|
|
54
|
+
const hasModelOptions = modelOptions.length > 0;
|
|
55
|
+
const inputDisabled = !hasModelOptions && !isSending;
|
|
52
56
|
const selectedModelOption = modelOptions.find((option) => option.value === selectedModel);
|
|
53
57
|
const resolvedStopHint =
|
|
54
58
|
stopDisabledReason === '__preparing__'
|
|
@@ -70,6 +74,7 @@ export function ChatInputBar({
|
|
|
70
74
|
<textarea
|
|
71
75
|
value={draft}
|
|
72
76
|
onChange={(e) => onDraftChange(e.target.value)}
|
|
77
|
+
disabled={inputDisabled}
|
|
73
78
|
onKeyDown={(e) => {
|
|
74
79
|
if (e.key === 'Escape' && isSending && canStopGeneration) {
|
|
75
80
|
e.preventDefault();
|
|
@@ -81,9 +86,23 @@ export function ChatInputBar({
|
|
|
81
86
|
void onSend();
|
|
82
87
|
}
|
|
83
88
|
}}
|
|
84
|
-
placeholder={t('chatInputPlaceholder')}
|
|
89
|
+
placeholder={hasModelOptions ? t('chatInputPlaceholder') : t('chatModelNoOptions')}
|
|
85
90
|
className="w-full min-h-[68px] max-h-[220px] resize-y bg-transparent outline-none text-sm px-4 py-3 text-gray-800 placeholder:text-gray-400"
|
|
86
91
|
/>
|
|
92
|
+
{!hasModelOptions && (
|
|
93
|
+
<div className="px-4 pb-2">
|
|
94
|
+
<div className="inline-flex items-center gap-2 rounded-lg border border-amber-200 bg-amber-50 px-3 py-1.5 text-xs text-amber-800">
|
|
95
|
+
<span>{t('chatModelNoOptions')}</span>
|
|
96
|
+
<button
|
|
97
|
+
type="button"
|
|
98
|
+
onClick={onGoToProviders}
|
|
99
|
+
className="font-semibold text-amber-900 underline-offset-2 hover:underline"
|
|
100
|
+
>
|
|
101
|
+
{t('chatGoConfigureProvider')}
|
|
102
|
+
</button>
|
|
103
|
+
</div>
|
|
104
|
+
</div>
|
|
105
|
+
)}
|
|
87
106
|
{selectedSkillRecords.length > 0 && (
|
|
88
107
|
<div className="px-4 pb-2">
|
|
89
108
|
<div className="flex flex-wrap items-center gap-2">
|
|
@@ -116,9 +135,9 @@ export function ChatInputBar({
|
|
|
116
135
|
|
|
117
136
|
{/* Model selector */}
|
|
118
137
|
<Select
|
|
119
|
-
value={
|
|
138
|
+
value={hasModelOptions ? selectedModel : undefined}
|
|
120
139
|
onValueChange={onSelectedModelChange}
|
|
121
|
-
disabled={
|
|
140
|
+
disabled={!hasModelOptions}
|
|
122
141
|
>
|
|
123
142
|
<SelectTrigger className="h-8 w-auto min-w-[220px] rounded-lg border-0 bg-transparent shadow-none text-xs font-medium text-gray-600 hover:bg-gray-100 focus:ring-0 px-3">
|
|
124
143
|
{selectedModelOption ? (
|
|
@@ -210,7 +229,7 @@ export function ChatInputBar({
|
|
|
210
229
|
size="sm"
|
|
211
230
|
className="rounded-lg"
|
|
212
231
|
onClick={() => void onSend()}
|
|
213
|
-
disabled={draft.trim().length === 0}
|
|
232
|
+
disabled={draft.trim().length === 0 || !hasModelOptions}
|
|
214
233
|
>
|
|
215
234
|
<Send className="h-3.5 w-3.5 mr-1.5" />
|
|
216
235
|
{t('chatSend')}
|
|
@@ -7,7 +7,8 @@ import {
|
|
|
7
7
|
useConfigMeta,
|
|
8
8
|
useDeleteSession,
|
|
9
9
|
useSessionHistory,
|
|
10
|
-
useSessions
|
|
10
|
+
useSessions,
|
|
11
|
+
useChatRuns
|
|
11
12
|
} from '@/hooks/useConfig';
|
|
12
13
|
import { useMarketplaceInstalled } from '@/hooks/useMarketplace';
|
|
13
14
|
import { useConfirmDialog } from '@/hooks/useConfirmDialog';
|
|
@@ -242,6 +243,9 @@ export function ChatPage({ view }: ChatPageProps) {
|
|
|
242
243
|
|
|
243
244
|
const configQuery = useConfig();
|
|
244
245
|
const configMetaQuery = useConfigMeta();
|
|
246
|
+
const isProviderStateResolved =
|
|
247
|
+
(configQuery.isFetched || configQuery.isSuccess) &&
|
|
248
|
+
(configMetaQuery.isFetched || configMetaQuery.isSuccess);
|
|
245
249
|
const sessionsQuery = useSessions({ q: query.trim() || undefined, limit: 120, activeMinutes: 0 });
|
|
246
250
|
const installedSkillsQuery = useMarketplaceInstalled('skill');
|
|
247
251
|
const chatCapabilitiesQuery = useChatCapabilities({
|
|
@@ -334,6 +338,8 @@ export function ChatPage({ view }: ChatPageProps) {
|
|
|
334
338
|
stopDisabledReason,
|
|
335
339
|
lastSendError,
|
|
336
340
|
sendMessage,
|
|
341
|
+
resumeRun,
|
|
342
|
+
activeBackendRunId,
|
|
337
343
|
stopCurrentRun,
|
|
338
344
|
resetStreamState
|
|
339
345
|
} = useChatStreamController({
|
|
@@ -345,17 +351,51 @@ export function ChatPage({ view }: ChatPageProps) {
|
|
|
345
351
|
refetchHistory: historyQuery.refetch
|
|
346
352
|
});
|
|
347
353
|
|
|
354
|
+
const activeRunsQuery = useChatRuns(
|
|
355
|
+
selectedSessionKey
|
|
356
|
+
? {
|
|
357
|
+
sessionKey: selectedSessionKey,
|
|
358
|
+
states: ['queued', 'running'],
|
|
359
|
+
limit: 5
|
|
360
|
+
}
|
|
361
|
+
: undefined
|
|
362
|
+
);
|
|
363
|
+
const activeRun = useMemo(() => {
|
|
364
|
+
const candidates = activeRunsQuery.data?.runs ?? [];
|
|
365
|
+
if (!selectedSessionKey) {
|
|
366
|
+
return null;
|
|
367
|
+
}
|
|
368
|
+
return candidates.find((entry) => entry.sessionKey === selectedSessionKey) ?? null;
|
|
369
|
+
}, [activeRunsQuery.data?.runs, selectedSessionKey]);
|
|
370
|
+
|
|
371
|
+
useEffect(() => {
|
|
372
|
+
if (view !== 'chat' || !selectedSessionKey || !activeRun) {
|
|
373
|
+
return;
|
|
374
|
+
}
|
|
375
|
+
if (activeBackendRunId === activeRun.runId) {
|
|
376
|
+
return;
|
|
377
|
+
}
|
|
378
|
+
void resumeRun(activeRun);
|
|
379
|
+
}, [activeBackendRunId, activeRun, resumeRun, selectedSessionKey, view]);
|
|
380
|
+
|
|
348
381
|
const mergedEvents = useMemo(() => {
|
|
349
|
-
const
|
|
382
|
+
const bySeq = new Map<number, SessionEventView>();
|
|
383
|
+
const append = (event: SessionEventView) => {
|
|
384
|
+
if (!Number.isFinite(event.seq)) {
|
|
385
|
+
return;
|
|
386
|
+
}
|
|
387
|
+
bySeq.set(event.seq, event);
|
|
388
|
+
};
|
|
389
|
+
|
|
390
|
+
historyEvents.forEach(append);
|
|
350
391
|
if (optimisticUserEvent) {
|
|
351
|
-
|
|
392
|
+
append(optimisticUserEvent);
|
|
352
393
|
}
|
|
353
|
-
|
|
394
|
+
streamingSessionEvents.forEach(append);
|
|
395
|
+
|
|
396
|
+
const next = [...bySeq.values()].sort((left, right) => left.seq - right.seq);
|
|
354
397
|
if (streamingAssistantText.trim()) {
|
|
355
|
-
const maxSeq = next.reduce((max, event) =>
|
|
356
|
-
const seq = Number.isFinite(event.seq) ? event.seq : 0;
|
|
357
|
-
return seq > max ? seq : max;
|
|
358
|
-
}, 0);
|
|
398
|
+
const maxSeq = next.reduce((max, event) => (event.seq > max ? event.seq : max), 0);
|
|
359
399
|
next.push({
|
|
360
400
|
seq: maxSeq + 1,
|
|
361
401
|
type: 'stream.assistant_delta',
|
|
@@ -397,6 +437,12 @@ export function ChatPage({ view }: ChatPageProps) {
|
|
|
397
437
|
}
|
|
398
438
|
}, [location.pathname, navigate, resetStreamState]);
|
|
399
439
|
|
|
440
|
+
const goToProviders = useCallback(() => {
|
|
441
|
+
if (location.pathname !== '/providers') {
|
|
442
|
+
navigate('/providers');
|
|
443
|
+
}
|
|
444
|
+
}, [location.pathname, navigate]);
|
|
445
|
+
|
|
400
446
|
const handleDeleteSession = useCallback(async () => {
|
|
401
447
|
if (!selectedSessionKey) {
|
|
402
448
|
return;
|
|
@@ -477,9 +523,11 @@ export function ChatPage({ view }: ChatPageProps) {
|
|
|
477
523
|
};
|
|
478
524
|
|
|
479
525
|
const conversationProps: ComponentProps<typeof ChatConversationPanel> = {
|
|
526
|
+
isProviderStateResolved,
|
|
480
527
|
modelOptions,
|
|
481
528
|
selectedModel,
|
|
482
529
|
onSelectedModelChange: setSelectedModel,
|
|
530
|
+
onGoToProviders: goToProviders,
|
|
483
531
|
skillRecords,
|
|
484
532
|
isSkillsLoading: installedSkillsQuery.isLoading,
|
|
485
533
|
selectedSkills,
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { useCallback, useEffect, useRef, useState } from 'react';
|
|
2
2
|
import type { Dispatch, MutableRefObject, SetStateAction } from 'react';
|
|
3
|
-
import type { SessionEventView } from '@/api/types';
|
|
4
|
-
import { sendChatTurnStream, stopChatTurn } from '@/api/config';
|
|
3
|
+
import type { ChatRunView, SessionEventView } from '@/api/types';
|
|
4
|
+
import { sendChatTurnStream, stopChatTurn, streamChatRun } from '@/api/config';
|
|
5
5
|
|
|
6
6
|
type PendingChatMessage = {
|
|
7
7
|
id: number;
|
|
@@ -17,7 +17,7 @@ type PendingChatMessage = {
|
|
|
17
17
|
type ActiveRunState = {
|
|
18
18
|
localRunId: number;
|
|
19
19
|
sessionKey: string;
|
|
20
|
-
agentId
|
|
20
|
+
agentId?: string;
|
|
21
21
|
requestAbortController: AbortController;
|
|
22
22
|
backendRunId?: string;
|
|
23
23
|
backendStopSupported: boolean;
|
|
@@ -141,62 +141,86 @@ async function refetchIfSessionVisible(params: {
|
|
|
141
141
|
}
|
|
142
142
|
}
|
|
143
143
|
|
|
144
|
-
|
|
145
|
-
|
|
144
|
+
function upsertStreamingEvent(
|
|
145
|
+
setStreamingSessionEvents: Dispatch<SetStateAction<SessionEventView[]>>,
|
|
146
|
+
event: SessionEventView
|
|
147
|
+
) {
|
|
148
|
+
setStreamingSessionEvents((prev) => {
|
|
149
|
+
const next = [...prev];
|
|
150
|
+
const hit = next.findIndex((streamEvent) => streamEvent.seq === event.seq);
|
|
151
|
+
if (hit >= 0) {
|
|
152
|
+
next[hit] = event;
|
|
153
|
+
} else {
|
|
154
|
+
next.push(event);
|
|
155
|
+
}
|
|
156
|
+
return next;
|
|
157
|
+
});
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
type ExecuteStreamRunParams = {
|
|
146
161
|
runId: number;
|
|
147
162
|
runIdRef: MutableRefObject<number>;
|
|
148
163
|
activeRunRef: MutableRefObject<ActiveRunState | null>;
|
|
149
|
-
nextOptimisticUserSeq: number;
|
|
150
164
|
selectedSessionKeyRef: MutableRefObject<string | null>;
|
|
151
165
|
setSelectedSessionKey: Dispatch<SetStateAction<string | null>>;
|
|
152
166
|
setDraft: Dispatch<SetStateAction<string>>;
|
|
153
167
|
refetchSessions: () => Promise<unknown>;
|
|
154
168
|
refetchHistory: () => Promise<unknown>;
|
|
155
169
|
restoreDraftOnError?: boolean;
|
|
170
|
+
sourceSessionKey: string;
|
|
171
|
+
sourceAgentId?: string;
|
|
172
|
+
sourceMessage?: string;
|
|
173
|
+
sourceStopSupported?: boolean;
|
|
174
|
+
sourceStopReason?: string;
|
|
175
|
+
optimisticUserEvent: SessionEventView | null;
|
|
176
|
+
openStream: (params: {
|
|
177
|
+
signal: AbortSignal;
|
|
178
|
+
onReady: (event: { runId?: string; stopSupported?: boolean; stopReason?: string; sessionKey: string }) => void;
|
|
179
|
+
onDelta: (event: { delta: string }) => void;
|
|
180
|
+
onSessionEvent: (event: { data: SessionEventView }) => void;
|
|
181
|
+
}) => Promise<{ sessionKey: string }>;
|
|
156
182
|
setters: StreamSetters;
|
|
157
|
-
}
|
|
183
|
+
};
|
|
184
|
+
|
|
185
|
+
async function executeStreamRun(params: ExecuteStreamRunParams): Promise<void> {
|
|
158
186
|
const {
|
|
159
|
-
item,
|
|
160
187
|
runId,
|
|
161
188
|
runIdRef,
|
|
162
189
|
activeRunRef,
|
|
163
|
-
nextOptimisticUserSeq,
|
|
164
190
|
selectedSessionKeyRef,
|
|
165
191
|
setSelectedSessionKey,
|
|
166
192
|
setDraft,
|
|
167
193
|
refetchSessions,
|
|
168
194
|
refetchHistory,
|
|
169
195
|
restoreDraftOnError,
|
|
196
|
+
sourceSessionKey,
|
|
197
|
+
sourceAgentId,
|
|
198
|
+
sourceMessage,
|
|
199
|
+
sourceStopSupported,
|
|
200
|
+
sourceStopReason,
|
|
201
|
+
optimisticUserEvent,
|
|
202
|
+
openStream,
|
|
170
203
|
setters
|
|
171
204
|
} = params;
|
|
172
205
|
|
|
173
206
|
const requestAbortController = new AbortController();
|
|
174
207
|
activeRunRef.current = {
|
|
175
208
|
localRunId: runId,
|
|
176
|
-
sessionKey:
|
|
177
|
-
agentId:
|
|
209
|
+
sessionKey: sourceSessionKey,
|
|
210
|
+
...(sourceAgentId ? { agentId: sourceAgentId } : {}),
|
|
178
211
|
requestAbortController,
|
|
179
|
-
backendStopSupported: Boolean(
|
|
180
|
-
...(
|
|
212
|
+
backendStopSupported: Boolean(sourceStopSupported),
|
|
213
|
+
...(sourceStopReason ? { backendStopReason: sourceStopReason } : {})
|
|
181
214
|
};
|
|
182
215
|
|
|
183
216
|
setters.setStreamingSessionEvents([]);
|
|
184
217
|
setters.setStreamingAssistantText('');
|
|
185
218
|
setters.setStreamingAssistantTimestamp(null);
|
|
186
|
-
setters.setOptimisticUserEvent(
|
|
187
|
-
seq: nextOptimisticUserSeq,
|
|
188
|
-
type: 'message.user.optimistic',
|
|
189
|
-
timestamp: new Date().toISOString(),
|
|
190
|
-
message: {
|
|
191
|
-
role: 'user',
|
|
192
|
-
content: item.message,
|
|
193
|
-
timestamp: new Date().toISOString()
|
|
194
|
-
}
|
|
195
|
-
});
|
|
219
|
+
setters.setOptimisticUserEvent(optimisticUserEvent);
|
|
196
220
|
setters.setIsSending(true);
|
|
197
221
|
setters.setIsAwaitingAssistantOutput(true);
|
|
198
222
|
setters.setCanStopCurrentRun(false);
|
|
199
|
-
setters.setStopDisabledReason(
|
|
223
|
+
setters.setStopDisabledReason(sourceStopSupported ? '__preparing__' : sourceStopReason ?? null);
|
|
200
224
|
setters.setLastSendError(null);
|
|
201
225
|
|
|
202
226
|
let streamText = '';
|
|
@@ -205,96 +229,69 @@ async function executeSendRun(params: {
|
|
|
205
229
|
const streamTimestamp = new Date().toISOString();
|
|
206
230
|
setters.setStreamingAssistantTimestamp(streamTimestamp);
|
|
207
231
|
|
|
208
|
-
const
|
|
209
|
-
|
|
210
|
-
{
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
}
|
|
220
|
-
}
|
|
221
|
-
: {}),
|
|
222
|
-
channel: 'ui',
|
|
223
|
-
chatId: 'web-ui'
|
|
224
|
-
},
|
|
225
|
-
{
|
|
226
|
-
signal: requestAbortController.signal,
|
|
227
|
-
onReady: (event) => {
|
|
228
|
-
if (runId !== runIdRef.current) {
|
|
229
|
-
return;
|
|
230
|
-
}
|
|
231
|
-
const activeRun = activeRunRef.current;
|
|
232
|
-
if (activeRun && activeRun.localRunId === runId) {
|
|
233
|
-
activeRun.backendRunId = event.runId?.trim() || undefined;
|
|
234
|
-
if (typeof event.stopSupported === 'boolean') {
|
|
235
|
-
activeRun.backendStopSupported = event.stopSupported;
|
|
236
|
-
}
|
|
237
|
-
if (typeof event.stopReason === 'string' && event.stopReason.trim().length > 0) {
|
|
238
|
-
activeRun.backendStopReason = event.stopReason.trim();
|
|
239
|
-
}
|
|
240
|
-
const canStopNow = Boolean(activeRun.backendStopSupported && activeRun.backendRunId);
|
|
241
|
-
setters.setCanStopCurrentRun(canStopNow);
|
|
242
|
-
setters.setStopDisabledReason(
|
|
243
|
-
canStopNow
|
|
244
|
-
? null
|
|
245
|
-
: activeRun.backendStopReason ?? (activeRun.backendStopSupported ? '__preparing__' : null)
|
|
246
|
-
);
|
|
247
|
-
}
|
|
248
|
-
if (event.sessionKey) {
|
|
249
|
-
setSelectedSessionKey((prev) => (prev === event.sessionKey ? prev : event.sessionKey));
|
|
232
|
+
const result = await openStream({
|
|
233
|
+
signal: requestAbortController.signal,
|
|
234
|
+
onReady: (event) => {
|
|
235
|
+
if (runId !== runIdRef.current) {
|
|
236
|
+
return;
|
|
237
|
+
}
|
|
238
|
+
const activeRun = activeRunRef.current;
|
|
239
|
+
if (activeRun && activeRun.localRunId === runId) {
|
|
240
|
+
activeRun.backendRunId = event.runId?.trim() || undefined;
|
|
241
|
+
if (typeof event.stopSupported === 'boolean') {
|
|
242
|
+
activeRun.backendStopSupported = event.stopSupported;
|
|
250
243
|
}
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
if (runId !== runIdRef.current) {
|
|
254
|
-
return;
|
|
244
|
+
if (typeof event.stopReason === 'string' && event.stopReason.trim().length > 0) {
|
|
245
|
+
activeRun.backendStopReason = event.stopReason.trim();
|
|
255
246
|
}
|
|
256
|
-
|
|
257
|
-
setters.
|
|
247
|
+
const canStopNow = Boolean(activeRun.backendStopSupported && activeRun.backendRunId);
|
|
248
|
+
setters.setCanStopCurrentRun(canStopNow);
|
|
249
|
+
setters.setStopDisabledReason(
|
|
250
|
+
canStopNow
|
|
251
|
+
? null
|
|
252
|
+
: activeRun.backendStopReason ?? (activeRun.backendStopSupported ? '__preparing__' : null)
|
|
253
|
+
);
|
|
254
|
+
}
|
|
255
|
+
if (event.sessionKey) {
|
|
256
|
+
setSelectedSessionKey((prev) => (prev === event.sessionKey ? prev : event.sessionKey));
|
|
257
|
+
}
|
|
258
|
+
},
|
|
259
|
+
onDelta: (event) => {
|
|
260
|
+
if (runId !== runIdRef.current) {
|
|
261
|
+
return;
|
|
262
|
+
}
|
|
263
|
+
streamText += event.delta;
|
|
264
|
+
setters.setStreamingAssistantText(streamText);
|
|
265
|
+
setters.setIsAwaitingAssistantOutput(false);
|
|
266
|
+
},
|
|
267
|
+
onSessionEvent: (event) => {
|
|
268
|
+
if (runId !== runIdRef.current) {
|
|
269
|
+
return;
|
|
270
|
+
}
|
|
271
|
+
if (event.data.message?.role === 'user') {
|
|
272
|
+
setters.setOptimisticUserEvent(null);
|
|
273
|
+
}
|
|
274
|
+
upsertStreamingEvent(setters.setStreamingSessionEvents, event.data);
|
|
275
|
+
if (event.data.message?.role === 'assistant') {
|
|
276
|
+
hasAssistantSessionEvent = true;
|
|
277
|
+
streamText = '';
|
|
278
|
+
setters.setStreamingAssistantText('');
|
|
258
279
|
setters.setIsAwaitingAssistantOutput(false);
|
|
259
|
-
},
|
|
260
|
-
onSessionEvent: (event) => {
|
|
261
|
-
if (runId !== runIdRef.current) {
|
|
262
|
-
return;
|
|
263
|
-
}
|
|
264
|
-
if (event.data.message?.role === 'user') {
|
|
265
|
-
setters.setOptimisticUserEvent(null);
|
|
266
|
-
}
|
|
267
|
-
setters.setStreamingSessionEvents((prev) => {
|
|
268
|
-
const next = [...prev];
|
|
269
|
-
const hit = next.findIndex((streamEvent) => streamEvent.seq === event.data.seq);
|
|
270
|
-
if (hit >= 0) {
|
|
271
|
-
next[hit] = event.data;
|
|
272
|
-
} else {
|
|
273
|
-
next.push(event.data);
|
|
274
|
-
}
|
|
275
|
-
return next;
|
|
276
|
-
});
|
|
277
|
-
if (event.data.message?.role === 'assistant') {
|
|
278
|
-
hasAssistantSessionEvent = true;
|
|
279
|
-
streamText = '';
|
|
280
|
-
setters.setStreamingAssistantText('');
|
|
281
|
-
setters.setIsAwaitingAssistantOutput(false);
|
|
282
|
-
}
|
|
283
280
|
}
|
|
284
281
|
}
|
|
285
|
-
);
|
|
282
|
+
});
|
|
286
283
|
if (runId !== runIdRef.current) {
|
|
287
284
|
return;
|
|
288
285
|
}
|
|
289
286
|
setters.setOptimisticUserEvent(null);
|
|
290
|
-
if (result.sessionKey !==
|
|
287
|
+
if (result.sessionKey !== sourceSessionKey) {
|
|
291
288
|
setSelectedSessionKey(result.sessionKey);
|
|
292
289
|
}
|
|
293
290
|
|
|
294
291
|
const localAssistantText = !hasAssistantSessionEvent ? streamText.trim() : '';
|
|
295
292
|
await refetchIfSessionVisible({
|
|
296
293
|
selectedSessionKeyRef,
|
|
297
|
-
currentSessionKey:
|
|
294
|
+
currentSessionKey: sourceSessionKey,
|
|
298
295
|
resultSessionKey: result.sessionKey,
|
|
299
296
|
refetchSessions,
|
|
300
297
|
refetchHistory
|
|
@@ -317,23 +314,14 @@ async function executeSendRun(params: {
|
|
|
317
314
|
const wasAborted = requestAbortController.signal.aborted || isAbortLikeError(error);
|
|
318
315
|
runIdRef.current += 1;
|
|
319
316
|
if (wasAborted) {
|
|
320
|
-
|
|
321
|
-
setters.setOptimisticUserEvent(null);
|
|
322
|
-
setters.setStreamingAssistantText('');
|
|
323
|
-
setters.setStreamingAssistantTimestamp(null);
|
|
324
|
-
setters.setIsSending(false);
|
|
325
|
-
setters.setIsAwaitingAssistantOutput(false);
|
|
326
|
-
setters.setCanStopCurrentRun(false);
|
|
327
|
-
setters.setStopDisabledReason(null);
|
|
328
|
-
setters.setLastSendError(null);
|
|
317
|
+
clearStreamingState(setters);
|
|
329
318
|
activeRunRef.current = null;
|
|
330
319
|
await refetchIfSessionVisible({
|
|
331
320
|
selectedSessionKeyRef,
|
|
332
|
-
currentSessionKey:
|
|
321
|
+
currentSessionKey: sourceSessionKey,
|
|
333
322
|
refetchSessions,
|
|
334
323
|
refetchHistory
|
|
335
324
|
});
|
|
336
|
-
setters.setStreamingSessionEvents(localAssistantText ? [buildLocalAssistantEvent(localAssistantText)] : []);
|
|
337
325
|
return;
|
|
338
326
|
}
|
|
339
327
|
|
|
@@ -343,7 +331,7 @@ async function executeSendRun(params: {
|
|
|
343
331
|
setters.setStreamingSessionEvents([buildLocalAssistantEvent(sendError, 'message.assistant.error.local')]);
|
|
344
332
|
activeRunRef.current = null;
|
|
345
333
|
if (restoreDraftOnError) {
|
|
346
|
-
setDraft((prev) => (prev.trim().length === 0 ?
|
|
334
|
+
setDraft((prev) => (prev.trim().length === 0 && sourceMessage ? sourceMessage : prev));
|
|
347
335
|
}
|
|
348
336
|
}
|
|
349
337
|
}
|
|
@@ -394,18 +382,51 @@ export function useChatStreamController(params: UseChatStreamControllerParams) {
|
|
|
394
382
|
async (item: PendingChatMessage, options?: { restoreDraftOnError?: boolean }) => {
|
|
395
383
|
setLastSendError(null);
|
|
396
384
|
streamRunIdRef.current += 1;
|
|
397
|
-
|
|
398
|
-
|
|
385
|
+
const requestedSkills = normalizeRequestedSkills(item.requestedSkills);
|
|
386
|
+
await executeStreamRun({
|
|
399
387
|
runId: streamRunIdRef.current,
|
|
400
388
|
runIdRef: streamRunIdRef,
|
|
401
389
|
activeRunRef,
|
|
402
|
-
nextOptimisticUserSeq: params.nextOptimisticUserSeq,
|
|
403
390
|
selectedSessionKeyRef: params.selectedSessionKeyRef,
|
|
404
391
|
setSelectedSessionKey: params.setSelectedSessionKey,
|
|
405
392
|
setDraft: params.setDraft,
|
|
406
393
|
refetchSessions: params.refetchSessions,
|
|
407
394
|
refetchHistory: params.refetchHistory,
|
|
408
395
|
restoreDraftOnError: options?.restoreDraftOnError,
|
|
396
|
+
sourceSessionKey: item.sessionKey,
|
|
397
|
+
sourceAgentId: item.agentId,
|
|
398
|
+
sourceMessage: item.message,
|
|
399
|
+
sourceStopSupported: item.stopSupported,
|
|
400
|
+
sourceStopReason: item.stopReason,
|
|
401
|
+
optimisticUserEvent: {
|
|
402
|
+
seq: params.nextOptimisticUserSeq,
|
|
403
|
+
type: 'message.user.optimistic',
|
|
404
|
+
timestamp: new Date().toISOString(),
|
|
405
|
+
message: {
|
|
406
|
+
role: 'user',
|
|
407
|
+
content: item.message,
|
|
408
|
+
timestamp: new Date().toISOString()
|
|
409
|
+
}
|
|
410
|
+
},
|
|
411
|
+
openStream: ({ signal, onReady, onDelta, onSessionEvent }) =>
|
|
412
|
+
sendChatTurnStream(
|
|
413
|
+
{
|
|
414
|
+
message: item.message,
|
|
415
|
+
sessionKey: item.sessionKey,
|
|
416
|
+
agentId: item.agentId,
|
|
417
|
+
...(item.model ? { model: item.model } : {}),
|
|
418
|
+
...(requestedSkills.length > 0
|
|
419
|
+
? {
|
|
420
|
+
metadata: {
|
|
421
|
+
requested_skills: requestedSkills
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
: {}),
|
|
425
|
+
channel: 'ui',
|
|
426
|
+
chatId: 'web-ui'
|
|
427
|
+
},
|
|
428
|
+
{ signal, onReady, onDelta, onSessionEvent }
|
|
429
|
+
),
|
|
409
430
|
setters: {
|
|
410
431
|
setOptimisticUserEvent,
|
|
411
432
|
setStreamingSessionEvents,
|
|
@@ -422,6 +443,60 @@ export function useChatStreamController(params: UseChatStreamControllerParams) {
|
|
|
422
443
|
[params]
|
|
423
444
|
);
|
|
424
445
|
|
|
446
|
+
const resumeRun = useCallback(
|
|
447
|
+
async (run: ChatRunView) => {
|
|
448
|
+
const runId = run.runId?.trim();
|
|
449
|
+
const sessionKey = run.sessionKey?.trim();
|
|
450
|
+
if (!runId || !sessionKey) {
|
|
451
|
+
return;
|
|
452
|
+
}
|
|
453
|
+
const active = activeRunRef.current;
|
|
454
|
+
if (active?.backendRunId === runId) {
|
|
455
|
+
return;
|
|
456
|
+
}
|
|
457
|
+
if (isSending && active) {
|
|
458
|
+
return;
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
setLastSendError(null);
|
|
462
|
+
streamRunIdRef.current += 1;
|
|
463
|
+
await executeStreamRun({
|
|
464
|
+
runId: streamRunIdRef.current,
|
|
465
|
+
runIdRef: streamRunIdRef,
|
|
466
|
+
activeRunRef,
|
|
467
|
+
selectedSessionKeyRef: params.selectedSessionKeyRef,
|
|
468
|
+
setSelectedSessionKey: params.setSelectedSessionKey,
|
|
469
|
+
setDraft: params.setDraft,
|
|
470
|
+
refetchSessions: params.refetchSessions,
|
|
471
|
+
refetchHistory: params.refetchHistory,
|
|
472
|
+
sourceSessionKey: sessionKey,
|
|
473
|
+
sourceAgentId: run.agentId,
|
|
474
|
+
sourceStopSupported: run.stopSupported,
|
|
475
|
+
sourceStopReason: run.stopReason,
|
|
476
|
+
optimisticUserEvent: null,
|
|
477
|
+
openStream: ({ signal, onReady, onDelta, onSessionEvent }) =>
|
|
478
|
+
streamChatRun(
|
|
479
|
+
{
|
|
480
|
+
runId
|
|
481
|
+
},
|
|
482
|
+
{ signal, onReady, onDelta, onSessionEvent }
|
|
483
|
+
),
|
|
484
|
+
setters: {
|
|
485
|
+
setOptimisticUserEvent,
|
|
486
|
+
setStreamingSessionEvents,
|
|
487
|
+
setStreamingAssistantText,
|
|
488
|
+
setStreamingAssistantTimestamp,
|
|
489
|
+
setIsSending,
|
|
490
|
+
setIsAwaitingAssistantOutput,
|
|
491
|
+
setCanStopCurrentRun,
|
|
492
|
+
setStopDisabledReason,
|
|
493
|
+
setLastSendError
|
|
494
|
+
}
|
|
495
|
+
});
|
|
496
|
+
},
|
|
497
|
+
[isSending, params]
|
|
498
|
+
);
|
|
499
|
+
|
|
425
500
|
useEffect(() => {
|
|
426
501
|
if (isSending || queuedMessages.length === 0) {
|
|
427
502
|
return;
|
|
@@ -472,7 +547,7 @@ export function useChatStreamController(params: UseChatStreamControllerParams) {
|
|
|
472
547
|
await stopChatTurn({
|
|
473
548
|
runId: activeRun.backendRunId,
|
|
474
549
|
sessionKey: activeRun.sessionKey,
|
|
475
|
-
agentId: activeRun.agentId
|
|
550
|
+
...(activeRun.agentId ? { agentId: activeRun.agentId } : {})
|
|
476
551
|
});
|
|
477
552
|
} catch {
|
|
478
553
|
// Keep local abort as fallback even if stop API fails.
|
|
@@ -492,7 +567,9 @@ export function useChatStreamController(params: UseChatStreamControllerParams) {
|
|
|
492
567
|
canStopCurrentRun,
|
|
493
568
|
stopDisabledReason,
|
|
494
569
|
lastSendError,
|
|
570
|
+
activeBackendRunId: activeRunRef.current?.backendRunId ?? null,
|
|
495
571
|
sendMessage,
|
|
572
|
+
resumeRun,
|
|
496
573
|
stopCurrentRun,
|
|
497
574
|
resetStreamState
|
|
498
575
|
};
|
package/src/hooks/useConfig.ts
CHANGED
|
@@ -17,6 +17,8 @@ import {
|
|
|
17
17
|
updateSession,
|
|
18
18
|
deleteSession,
|
|
19
19
|
sendChatTurn,
|
|
20
|
+
fetchChatRun,
|
|
21
|
+
fetchChatRuns,
|
|
20
22
|
fetchChatCapabilities,
|
|
21
23
|
fetchCronJobs,
|
|
22
24
|
deleteCronJob,
|
|
@@ -260,6 +262,33 @@ export function useChatCapabilities(params?: { sessionKey?: string | null; agent
|
|
|
260
262
|
});
|
|
261
263
|
}
|
|
262
264
|
|
|
265
|
+
export function useChatRuns(params?: { sessionKey?: string | null; states?: Array<'queued' | 'running' | 'completed' | 'failed' | 'aborted'>; limit?: number }) {
|
|
266
|
+
const sessionKey = params?.sessionKey?.trim() || undefined;
|
|
267
|
+
const states = Array.isArray(params?.states) && params.states.length > 0 ? params.states : undefined;
|
|
268
|
+
return useQuery({
|
|
269
|
+
queryKey: ['chat-runs', sessionKey ?? null, states ?? null, params?.limit ?? null],
|
|
270
|
+
queryFn: () => fetchChatRuns({
|
|
271
|
+
...(sessionKey ? { sessionKey } : {}),
|
|
272
|
+
...(states ? { states } : {}),
|
|
273
|
+
...(typeof params?.limit === 'number' ? { limit: params.limit } : {})
|
|
274
|
+
}),
|
|
275
|
+
enabled: Boolean(sessionKey) || Boolean(states),
|
|
276
|
+
staleTime: 5_000,
|
|
277
|
+
retry: false
|
|
278
|
+
});
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
export function useChatRun(runId: string | null) {
|
|
282
|
+
const normalizedRunId = runId?.trim() || null;
|
|
283
|
+
return useQuery({
|
|
284
|
+
queryKey: ['chat-run', normalizedRunId],
|
|
285
|
+
queryFn: () => fetchChatRun(normalizedRunId as string),
|
|
286
|
+
enabled: Boolean(normalizedRunId),
|
|
287
|
+
staleTime: 5_000,
|
|
288
|
+
retry: false
|
|
289
|
+
});
|
|
290
|
+
}
|
|
291
|
+
|
|
263
292
|
export function useCronJobs(params: { all?: boolean } = { all: true }) {
|
|
264
293
|
return useQuery({
|
|
265
294
|
queryKey: ['cron', params],
|