@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,19 +1,37 @@
|
|
|
1
1
|
import { useCallback, useEffect, useRef, useState } from 'react';
|
|
2
2
|
import type { Dispatch, MutableRefObject, SetStateAction } from 'react';
|
|
3
3
|
import type { SessionEventView } from '@/api/types';
|
|
4
|
-
import { sendChatTurnStream } from '@/api/config';
|
|
4
|
+
import { sendChatTurnStream, stopChatTurn } from '@/api/config';
|
|
5
5
|
|
|
6
6
|
type PendingChatMessage = {
|
|
7
7
|
id: number;
|
|
8
8
|
message: string;
|
|
9
9
|
sessionKey: string;
|
|
10
10
|
agentId: string;
|
|
11
|
+
model?: string;
|
|
12
|
+
requestedSkills?: string[];
|
|
13
|
+
stopSupported?: boolean;
|
|
14
|
+
stopReason?: string;
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
type ActiveRunState = {
|
|
18
|
+
localRunId: number;
|
|
19
|
+
sessionKey: string;
|
|
20
|
+
agentId: string;
|
|
21
|
+
requestAbortController: AbortController;
|
|
22
|
+
backendRunId?: string;
|
|
23
|
+
backendStopSupported: boolean;
|
|
24
|
+
backendStopReason?: string;
|
|
11
25
|
};
|
|
12
26
|
|
|
13
27
|
type SendMessageParams = {
|
|
14
28
|
message: string;
|
|
15
29
|
sessionKey: string;
|
|
16
30
|
agentId: string;
|
|
31
|
+
model?: string;
|
|
32
|
+
requestedSkills?: string[];
|
|
33
|
+
stopSupported?: boolean;
|
|
34
|
+
stopReason?: string;
|
|
17
35
|
restoreDraftOnError?: boolean;
|
|
18
36
|
};
|
|
19
37
|
|
|
@@ -26,6 +44,17 @@ type UseChatStreamControllerParams = {
|
|
|
26
44
|
refetchHistory: () => Promise<unknown>;
|
|
27
45
|
};
|
|
28
46
|
|
|
47
|
+
function formatSendError(error: unknown): string {
|
|
48
|
+
if (error instanceof Error) {
|
|
49
|
+
const message = error.message.trim();
|
|
50
|
+
if (message) {
|
|
51
|
+
return message;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
const raw = String(error ?? '').trim();
|
|
55
|
+
return raw || 'Failed to send message';
|
|
56
|
+
}
|
|
57
|
+
|
|
29
58
|
type StreamSetters = {
|
|
30
59
|
setOptimisticUserEvent: Dispatch<SetStateAction<SessionEventView | null>>;
|
|
31
60
|
setStreamingSessionEvents: Dispatch<SetStateAction<SessionEventView[]>>;
|
|
@@ -33,6 +62,9 @@ type StreamSetters = {
|
|
|
33
62
|
setStreamingAssistantTimestamp: Dispatch<SetStateAction<string | null>>;
|
|
34
63
|
setIsSending: Dispatch<SetStateAction<boolean>>;
|
|
35
64
|
setIsAwaitingAssistantOutput: Dispatch<SetStateAction<boolean>>;
|
|
65
|
+
setCanStopCurrentRun: Dispatch<SetStateAction<boolean>>;
|
|
66
|
+
setStopDisabledReason: Dispatch<SetStateAction<string | null>>;
|
|
67
|
+
setLastSendError: Dispatch<SetStateAction<string | null>>;
|
|
36
68
|
};
|
|
37
69
|
|
|
38
70
|
function clearStreamingState(setters: StreamSetters) {
|
|
@@ -42,12 +74,78 @@ function clearStreamingState(setters: StreamSetters) {
|
|
|
42
74
|
setters.setStreamingAssistantText('');
|
|
43
75
|
setters.setStreamingAssistantTimestamp(null);
|
|
44
76
|
setters.setIsAwaitingAssistantOutput(false);
|
|
77
|
+
setters.setCanStopCurrentRun(false);
|
|
78
|
+
setters.setStopDisabledReason(null);
|
|
79
|
+
setters.setLastSendError(null);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function normalizeRequestedSkills(value: string[] | undefined): string[] {
|
|
83
|
+
if (!Array.isArray(value)) {
|
|
84
|
+
return [];
|
|
85
|
+
}
|
|
86
|
+
const deduped = new Set<string>();
|
|
87
|
+
for (const item of value) {
|
|
88
|
+
const trimmed = item.trim();
|
|
89
|
+
if (trimmed) {
|
|
90
|
+
deduped.add(trimmed);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
return [...deduped];
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function isAbortLikeError(error: unknown): boolean {
|
|
97
|
+
if (error instanceof DOMException && error.name === 'AbortError') {
|
|
98
|
+
return true;
|
|
99
|
+
}
|
|
100
|
+
if (error instanceof Error) {
|
|
101
|
+
if (error.name === 'AbortError') {
|
|
102
|
+
return true;
|
|
103
|
+
}
|
|
104
|
+
const lower = error.message.toLowerCase();
|
|
105
|
+
if (lower.includes('aborted') || lower.includes('abort')) {
|
|
106
|
+
return true;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
return false;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function buildLocalAssistantEvent(content: string, eventType = 'message.assistant.local'): SessionEventView {
|
|
113
|
+
const timestamp = new Date().toISOString();
|
|
114
|
+
return {
|
|
115
|
+
seq: Date.now(),
|
|
116
|
+
type: eventType,
|
|
117
|
+
timestamp,
|
|
118
|
+
message: {
|
|
119
|
+
role: 'assistant',
|
|
120
|
+
content,
|
|
121
|
+
timestamp
|
|
122
|
+
}
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
async function refetchIfSessionVisible(params: {
|
|
127
|
+
selectedSessionKeyRef: MutableRefObject<string | null>;
|
|
128
|
+
currentSessionKey: string;
|
|
129
|
+
resultSessionKey?: string;
|
|
130
|
+
refetchSessions: () => Promise<unknown>;
|
|
131
|
+
refetchHistory: () => Promise<unknown>;
|
|
132
|
+
}): Promise<void> {
|
|
133
|
+
await params.refetchSessions();
|
|
134
|
+
const activeSessionKey = params.selectedSessionKeyRef.current;
|
|
135
|
+
if (
|
|
136
|
+
!activeSessionKey ||
|
|
137
|
+
activeSessionKey === params.currentSessionKey ||
|
|
138
|
+
(params.resultSessionKey && activeSessionKey === params.resultSessionKey)
|
|
139
|
+
) {
|
|
140
|
+
await params.refetchHistory();
|
|
141
|
+
}
|
|
45
142
|
}
|
|
46
143
|
|
|
47
144
|
async function executeSendRun(params: {
|
|
48
145
|
item: PendingChatMessage;
|
|
49
146
|
runId: number;
|
|
50
147
|
runIdRef: MutableRefObject<number>;
|
|
148
|
+
activeRunRef: MutableRefObject<ActiveRunState | null>;
|
|
51
149
|
nextOptimisticUserSeq: number;
|
|
52
150
|
selectedSessionKeyRef: MutableRefObject<string | null>;
|
|
53
151
|
setSelectedSessionKey: Dispatch<SetStateAction<string | null>>;
|
|
@@ -61,6 +159,7 @@ async function executeSendRun(params: {
|
|
|
61
159
|
item,
|
|
62
160
|
runId,
|
|
63
161
|
runIdRef,
|
|
162
|
+
activeRunRef,
|
|
64
163
|
nextOptimisticUserSeq,
|
|
65
164
|
selectedSessionKeyRef,
|
|
66
165
|
setSelectedSessionKey,
|
|
@@ -71,6 +170,16 @@ async function executeSendRun(params: {
|
|
|
71
170
|
setters
|
|
72
171
|
} = params;
|
|
73
172
|
|
|
173
|
+
const requestAbortController = new AbortController();
|
|
174
|
+
activeRunRef.current = {
|
|
175
|
+
localRunId: runId,
|
|
176
|
+
sessionKey: item.sessionKey,
|
|
177
|
+
agentId: item.agentId,
|
|
178
|
+
requestAbortController,
|
|
179
|
+
backendStopSupported: Boolean(item.stopSupported),
|
|
180
|
+
...(item.stopReason ? { backendStopReason: item.stopReason } : {})
|
|
181
|
+
};
|
|
182
|
+
|
|
74
183
|
setters.setStreamingSessionEvents([]);
|
|
75
184
|
setters.setStreamingAssistantText('');
|
|
76
185
|
setters.setStreamingAssistantTimestamp(null);
|
|
@@ -86,60 +195,94 @@ async function executeSendRun(params: {
|
|
|
86
195
|
});
|
|
87
196
|
setters.setIsSending(true);
|
|
88
197
|
setters.setIsAwaitingAssistantOutput(true);
|
|
198
|
+
setters.setCanStopCurrentRun(false);
|
|
199
|
+
setters.setStopDisabledReason(item.stopSupported ? '__preparing__' : item.stopReason ?? null);
|
|
200
|
+
setters.setLastSendError(null);
|
|
89
201
|
|
|
202
|
+
let streamText = '';
|
|
90
203
|
try {
|
|
91
|
-
let
|
|
204
|
+
let hasAssistantSessionEvent = false;
|
|
92
205
|
const streamTimestamp = new Date().toISOString();
|
|
93
206
|
setters.setStreamingAssistantTimestamp(streamTimestamp);
|
|
94
207
|
|
|
95
|
-
const
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
208
|
+
const requestedSkills = normalizeRequestedSkills(item.requestedSkills);
|
|
209
|
+
const result = await sendChatTurnStream(
|
|
210
|
+
{
|
|
211
|
+
message: item.message,
|
|
212
|
+
sessionKey: item.sessionKey,
|
|
213
|
+
agentId: item.agentId,
|
|
214
|
+
...(item.model ? { model: item.model } : {}),
|
|
215
|
+
...(requestedSkills.length > 0
|
|
216
|
+
? {
|
|
217
|
+
metadata: {
|
|
218
|
+
requested_skills: requestedSkills
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
: {}),
|
|
222
|
+
channel: 'ui',
|
|
223
|
+
chatId: 'web-ui'
|
|
109
224
|
},
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
setters.setStreamingAssistantText(streamText);
|
|
116
|
-
setters.setIsAwaitingAssistantOutput(false);
|
|
117
|
-
},
|
|
118
|
-
onSessionEvent: (event) => {
|
|
119
|
-
if (runId !== runIdRef.current) {
|
|
120
|
-
return;
|
|
121
|
-
}
|
|
122
|
-
if (event.data.message?.role === 'user') {
|
|
123
|
-
setters.setOptimisticUserEvent(null);
|
|
124
|
-
}
|
|
125
|
-
setters.setStreamingSessionEvents((prev) => {
|
|
126
|
-
const next = [...prev];
|
|
127
|
-
const hit = next.findIndex((streamEvent) => streamEvent.seq === event.data.seq);
|
|
128
|
-
if (hit >= 0) {
|
|
129
|
-
next[hit] = event.data;
|
|
130
|
-
} else {
|
|
131
|
-
next.push(event.data);
|
|
225
|
+
{
|
|
226
|
+
signal: requestAbortController.signal,
|
|
227
|
+
onReady: (event) => {
|
|
228
|
+
if (runId !== runIdRef.current) {
|
|
229
|
+
return;
|
|
132
230
|
}
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
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));
|
|
250
|
+
}
|
|
251
|
+
},
|
|
252
|
+
onDelta: (event) => {
|
|
253
|
+
if (runId !== runIdRef.current) {
|
|
254
|
+
return;
|
|
255
|
+
}
|
|
256
|
+
streamText += event.delta;
|
|
257
|
+
setters.setStreamingAssistantText(streamText);
|
|
139
258
|
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
|
+
}
|
|
140
283
|
}
|
|
141
284
|
}
|
|
142
|
-
|
|
285
|
+
);
|
|
143
286
|
if (runId !== runIdRef.current) {
|
|
144
287
|
return;
|
|
145
288
|
}
|
|
@@ -147,24 +290,60 @@ async function executeSendRun(params: {
|
|
|
147
290
|
if (result.sessionKey !== item.sessionKey) {
|
|
148
291
|
setSelectedSessionKey(result.sessionKey);
|
|
149
292
|
}
|
|
150
|
-
|
|
151
|
-
const
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
293
|
+
|
|
294
|
+
const localAssistantText = !hasAssistantSessionEvent ? streamText.trim() : '';
|
|
295
|
+
await refetchIfSessionVisible({
|
|
296
|
+
selectedSessionKeyRef,
|
|
297
|
+
currentSessionKey: item.sessionKey,
|
|
298
|
+
resultSessionKey: result.sessionKey,
|
|
299
|
+
refetchSessions,
|
|
300
|
+
refetchHistory
|
|
301
|
+
});
|
|
302
|
+
|
|
303
|
+
setters.setStreamingSessionEvents(localAssistantText ? [buildLocalAssistantEvent(localAssistantText)] : []);
|
|
304
|
+
|
|
156
305
|
setters.setStreamingAssistantText('');
|
|
157
306
|
setters.setStreamingAssistantTimestamp(null);
|
|
158
307
|
setters.setIsAwaitingAssistantOutput(false);
|
|
159
308
|
setters.setIsSending(false);
|
|
160
|
-
|
|
309
|
+
setters.setCanStopCurrentRun(false);
|
|
310
|
+
setters.setStopDisabledReason(null);
|
|
311
|
+
setters.setLastSendError(null);
|
|
312
|
+
activeRunRef.current = null;
|
|
313
|
+
} catch (error) {
|
|
161
314
|
if (runId !== runIdRef.current) {
|
|
162
315
|
return;
|
|
163
316
|
}
|
|
317
|
+
const wasAborted = requestAbortController.signal.aborted || isAbortLikeError(error);
|
|
164
318
|
runIdRef.current += 1;
|
|
319
|
+
if (wasAborted) {
|
|
320
|
+
const localAssistantText = streamText.trim();
|
|
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);
|
|
329
|
+
activeRunRef.current = null;
|
|
330
|
+
await refetchIfSessionVisible({
|
|
331
|
+
selectedSessionKeyRef,
|
|
332
|
+
currentSessionKey: item.sessionKey,
|
|
333
|
+
refetchSessions,
|
|
334
|
+
refetchHistory
|
|
335
|
+
});
|
|
336
|
+
setters.setStreamingSessionEvents(localAssistantText ? [buildLocalAssistantEvent(localAssistantText)] : []);
|
|
337
|
+
return;
|
|
338
|
+
}
|
|
339
|
+
|
|
165
340
|
clearStreamingState(setters);
|
|
341
|
+
const sendError = formatSendError(error);
|
|
342
|
+
setters.setLastSendError(sendError);
|
|
343
|
+
setters.setStreamingSessionEvents([buildLocalAssistantEvent(sendError, 'message.assistant.error.local')]);
|
|
344
|
+
activeRunRef.current = null;
|
|
166
345
|
if (restoreDraftOnError) {
|
|
167
|
-
setDraft((prev) => prev.trim().length === 0 ? item.message : prev);
|
|
346
|
+
setDraft((prev) => (prev.trim().length === 0 ? item.message : prev));
|
|
168
347
|
}
|
|
169
348
|
}
|
|
170
349
|
}
|
|
@@ -177,36 +356,49 @@ export function useChatStreamController(params: UseChatStreamControllerParams) {
|
|
|
177
356
|
const [isSending, setIsSending] = useState(false);
|
|
178
357
|
const [isAwaitingAssistantOutput, setIsAwaitingAssistantOutput] = useState(false);
|
|
179
358
|
const [queuedMessages, setQueuedMessages] = useState<PendingChatMessage[]>([]);
|
|
359
|
+
const [canStopCurrentRun, setCanStopCurrentRun] = useState(false);
|
|
360
|
+
const [stopDisabledReason, setStopDisabledReason] = useState<string | null>(null);
|
|
361
|
+
const [lastSendError, setLastSendError] = useState<string | null>(null);
|
|
180
362
|
|
|
181
363
|
const streamRunIdRef = useRef(0);
|
|
182
364
|
const queueIdRef = useRef(0);
|
|
365
|
+
const activeRunRef = useRef<ActiveRunState | null>(null);
|
|
183
366
|
|
|
184
367
|
const resetStreamState = useCallback(() => {
|
|
185
368
|
streamRunIdRef.current += 1;
|
|
186
369
|
setQueuedMessages([]);
|
|
370
|
+
activeRunRef.current?.requestAbortController.abort();
|
|
371
|
+
activeRunRef.current = null;
|
|
187
372
|
clearStreamingState({
|
|
188
373
|
setOptimisticUserEvent,
|
|
189
374
|
setStreamingSessionEvents,
|
|
190
375
|
setStreamingAssistantText,
|
|
191
376
|
setStreamingAssistantTimestamp,
|
|
192
377
|
setIsSending,
|
|
193
|
-
setIsAwaitingAssistantOutput
|
|
378
|
+
setIsAwaitingAssistantOutput,
|
|
379
|
+
setCanStopCurrentRun,
|
|
380
|
+
setStopDisabledReason,
|
|
381
|
+
setLastSendError
|
|
194
382
|
});
|
|
195
383
|
}, []);
|
|
196
384
|
|
|
197
385
|
useEffect(() => {
|
|
198
386
|
return () => {
|
|
199
387
|
streamRunIdRef.current += 1;
|
|
388
|
+
activeRunRef.current?.requestAbortController.abort();
|
|
389
|
+
activeRunRef.current = null;
|
|
200
390
|
};
|
|
201
391
|
}, []);
|
|
202
392
|
|
|
203
393
|
const runSend = useCallback(
|
|
204
394
|
async (item: PendingChatMessage, options?: { restoreDraftOnError?: boolean }) => {
|
|
395
|
+
setLastSendError(null);
|
|
205
396
|
streamRunIdRef.current += 1;
|
|
206
397
|
await executeSendRun({
|
|
207
398
|
item,
|
|
208
399
|
runId: streamRunIdRef.current,
|
|
209
400
|
runIdRef: streamRunIdRef,
|
|
401
|
+
activeRunRef,
|
|
210
402
|
nextOptimisticUserSeq: params.nextOptimisticUserSeq,
|
|
211
403
|
selectedSessionKeyRef: params.selectedSessionKeyRef,
|
|
212
404
|
setSelectedSessionKey: params.setSelectedSessionKey,
|
|
@@ -220,7 +412,10 @@ export function useChatStreamController(params: UseChatStreamControllerParams) {
|
|
|
220
412
|
setStreamingAssistantText,
|
|
221
413
|
setStreamingAssistantTimestamp,
|
|
222
414
|
setIsSending,
|
|
223
|
-
setIsAwaitingAssistantOutput
|
|
415
|
+
setIsAwaitingAssistantOutput,
|
|
416
|
+
setCanStopCurrentRun,
|
|
417
|
+
setStopDisabledReason,
|
|
418
|
+
setLastSendError
|
|
224
419
|
}
|
|
225
420
|
});
|
|
226
421
|
},
|
|
@@ -238,12 +433,19 @@ export function useChatStreamController(params: UseChatStreamControllerParams) {
|
|
|
238
433
|
|
|
239
434
|
const sendMessage = useCallback(
|
|
240
435
|
async (payload: SendMessageParams) => {
|
|
436
|
+
setLastSendError(null);
|
|
241
437
|
queueIdRef.current += 1;
|
|
242
438
|
const item: PendingChatMessage = {
|
|
243
439
|
id: queueIdRef.current,
|
|
244
440
|
message: payload.message,
|
|
245
441
|
sessionKey: payload.sessionKey,
|
|
246
|
-
agentId: payload.agentId
|
|
442
|
+
agentId: payload.agentId,
|
|
443
|
+
...(payload.model ? { model: payload.model } : {}),
|
|
444
|
+
...(payload.requestedSkills && payload.requestedSkills.length > 0
|
|
445
|
+
? { requestedSkills: payload.requestedSkills }
|
|
446
|
+
: {}),
|
|
447
|
+
...(typeof payload.stopSupported === 'boolean' ? { stopSupported: payload.stopSupported } : {}),
|
|
448
|
+
...(payload.stopReason ? { stopReason: payload.stopReason } : {})
|
|
247
449
|
};
|
|
248
450
|
if (isSending) {
|
|
249
451
|
setQueuedMessages((prev) => [...prev, item]);
|
|
@@ -254,6 +456,31 @@ export function useChatStreamController(params: UseChatStreamControllerParams) {
|
|
|
254
456
|
[isSending, runSend]
|
|
255
457
|
);
|
|
256
458
|
|
|
459
|
+
const stopCurrentRun = useCallback(async () => {
|
|
460
|
+
const activeRun = activeRunRef.current;
|
|
461
|
+
if (!activeRun) {
|
|
462
|
+
return;
|
|
463
|
+
}
|
|
464
|
+
if (!activeRun.backendStopSupported) {
|
|
465
|
+
return;
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
setCanStopCurrentRun(false);
|
|
469
|
+
setQueuedMessages([]);
|
|
470
|
+
if (activeRun.backendRunId) {
|
|
471
|
+
try {
|
|
472
|
+
await stopChatTurn({
|
|
473
|
+
runId: activeRun.backendRunId,
|
|
474
|
+
sessionKey: activeRun.sessionKey,
|
|
475
|
+
agentId: activeRun.agentId
|
|
476
|
+
});
|
|
477
|
+
} catch {
|
|
478
|
+
// Keep local abort as fallback even if stop API fails.
|
|
479
|
+
}
|
|
480
|
+
}
|
|
481
|
+
activeRun.requestAbortController.abort();
|
|
482
|
+
}, []);
|
|
483
|
+
|
|
257
484
|
return {
|
|
258
485
|
optimisticUserEvent,
|
|
259
486
|
streamingSessionEvents,
|
|
@@ -262,7 +489,11 @@ export function useChatStreamController(params: UseChatStreamControllerParams) {
|
|
|
262
489
|
isSending,
|
|
263
490
|
isAwaitingAssistantOutput,
|
|
264
491
|
queuedCount: queuedMessages.length,
|
|
492
|
+
canStopCurrentRun,
|
|
493
|
+
stopDisabledReason,
|
|
494
|
+
lastSendError,
|
|
265
495
|
sendMessage,
|
|
496
|
+
stopCurrentRun,
|
|
266
497
|
resetStreamState
|
|
267
498
|
};
|
|
268
499
|
}
|
|
@@ -371,7 +371,7 @@ export function ChannelForm({ channelName }: ChannelFormProps) {
|
|
|
371
371
|
</div>
|
|
372
372
|
|
|
373
373
|
<form onSubmit={handleSubmit} className="flex min-h-0 flex-1 flex-col">
|
|
374
|
-
<div className="min-h-0 flex-1 space-y-6 overflow-y-auto px-6 py-5">
|
|
374
|
+
<div className="min-h-0 flex-1 space-y-6 overflow-y-auto overscroll-contain px-6 py-5">
|
|
375
375
|
{fields.map((field) => {
|
|
376
376
|
const hint = channelName
|
|
377
377
|
? hintForPath(`channels.${channelName}.${field.name}`, uiHints)
|
|
@@ -74,10 +74,10 @@ export function ChannelsList() {
|
|
|
74
74
|
}
|
|
75
75
|
|
|
76
76
|
return (
|
|
77
|
-
<PageLayout>
|
|
77
|
+
<PageLayout className="xl:flex xl:h-full xl:min-h-0 xl:flex-col xl:pb-0">
|
|
78
78
|
<PageHeader title={t('channelsPageTitle')} description={t('channelsPageDescription')} />
|
|
79
79
|
|
|
80
|
-
<div className={CONFIG_SPLIT_GRID_CLASS}>
|
|
80
|
+
<div className={cn(CONFIG_SPLIT_GRID_CLASS, 'xl:min-h-0 xl:flex-1')}>
|
|
81
81
|
<section className={CONFIG_SIDEBAR_CARD_CLASS}>
|
|
82
82
|
<div className="border-b border-gray-100 px-4 pt-4">
|
|
83
83
|
<Tabs tabs={tabs} activeTab={activeTab} onChange={setActiveTab} className="mb-0" />
|
|
@@ -95,7 +95,7 @@ export function ChannelsList() {
|
|
|
95
95
|
</div>
|
|
96
96
|
</div>
|
|
97
97
|
|
|
98
|
-
<div className="min-h-0 flex-1 space-y-2 overflow-y-auto p-3">
|
|
98
|
+
<div className="min-h-0 flex-1 space-y-2 overflow-y-auto overscroll-contain p-3">
|
|
99
99
|
{filteredChannels.map((channel) => {
|
|
100
100
|
const channelConfig = config.channels[channel.name];
|
|
101
101
|
const enabled = channelConfig?.enabled || false;
|
|
@@ -8,74 +8,17 @@ import { SearchableModelInput } from '@/components/common/SearchableModelInput';
|
|
|
8
8
|
import { useConfig, useConfigMeta, useConfigSchema, useUpdateModel } from '@/hooks/useConfig';
|
|
9
9
|
import { hintForPath } from '@/lib/config-hints';
|
|
10
10
|
import { t } from '@/lib/i18n';
|
|
11
|
+
import {
|
|
12
|
+
buildProviderModelCatalog,
|
|
13
|
+
composeProviderModel,
|
|
14
|
+
findProviderByModel,
|
|
15
|
+
toProviderLocalModel
|
|
16
|
+
} from '@/lib/provider-models';
|
|
11
17
|
import { PageLayout, PageHeader } from '@/components/layout/page-layout';
|
|
12
18
|
import { DOCS_DEFAULT_BASE_URL } from '@/components/doc-browser/DocBrowserContext';
|
|
13
19
|
import { BookOpen, Folder, Loader2, Sparkles } from 'lucide-react';
|
|
14
20
|
import { useEffect, useMemo, useState } from 'react';
|
|
15
21
|
|
|
16
|
-
function normalizeStringList(input: string[] | null | undefined): string[] {
|
|
17
|
-
if (!input || input.length === 0) {
|
|
18
|
-
return [];
|
|
19
|
-
}
|
|
20
|
-
const deduped = new Set<string>();
|
|
21
|
-
for (const item of input) {
|
|
22
|
-
const trimmed = item.trim();
|
|
23
|
-
if (trimmed) {
|
|
24
|
-
deduped.add(trimmed);
|
|
25
|
-
}
|
|
26
|
-
}
|
|
27
|
-
return [...deduped];
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
function stripProviderPrefix(model: string, prefix: string): string {
|
|
31
|
-
const trimmed = model.trim();
|
|
32
|
-
const cleanPrefix = prefix.trim();
|
|
33
|
-
if (!trimmed || !cleanPrefix) {
|
|
34
|
-
return trimmed;
|
|
35
|
-
}
|
|
36
|
-
const withSlash = `${cleanPrefix}/`;
|
|
37
|
-
if (trimmed.startsWith(withSlash)) {
|
|
38
|
-
return trimmed.slice(withSlash.length);
|
|
39
|
-
}
|
|
40
|
-
return trimmed;
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
function toProviderLocalModel(model: string, aliases: string[]): string {
|
|
44
|
-
let normalized = model.trim();
|
|
45
|
-
if (!normalized) {
|
|
46
|
-
return '';
|
|
47
|
-
}
|
|
48
|
-
for (const alias of aliases) {
|
|
49
|
-
normalized = stripProviderPrefix(normalized, alias);
|
|
50
|
-
}
|
|
51
|
-
return normalized.trim();
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
function findProviderByModel(
|
|
55
|
-
model: string,
|
|
56
|
-
providerCatalog: Array<{ name: string; aliases: string[] }>
|
|
57
|
-
): string | null {
|
|
58
|
-
const trimmed = model.trim();
|
|
59
|
-
if (!trimmed) {
|
|
60
|
-
return null;
|
|
61
|
-
}
|
|
62
|
-
let bestMatch: { name: string; score: number } | null = null;
|
|
63
|
-
for (const provider of providerCatalog) {
|
|
64
|
-
for (const alias of provider.aliases) {
|
|
65
|
-
const cleanAlias = alias.trim();
|
|
66
|
-
if (!cleanAlias) {
|
|
67
|
-
continue;
|
|
68
|
-
}
|
|
69
|
-
if (trimmed === cleanAlias || trimmed.startsWith(`${cleanAlias}/`)) {
|
|
70
|
-
if (!bestMatch || cleanAlias.length > bestMatch.score) {
|
|
71
|
-
bestMatch = { name: provider.name, score: cleanAlias.length };
|
|
72
|
-
}
|
|
73
|
-
}
|
|
74
|
-
}
|
|
75
|
-
}
|
|
76
|
-
return bestMatch?.name ?? null;
|
|
77
|
-
}
|
|
78
|
-
|
|
79
22
|
export function ModelConfig() {
|
|
80
23
|
const { data: config, isLoading } = useConfig();
|
|
81
24
|
const { data: meta } = useConfigMeta();
|
|
@@ -89,25 +32,10 @@ export function ModelConfig() {
|
|
|
89
32
|
const modelHint = hintForPath('agents.defaults.model', uiHints);
|
|
90
33
|
const workspaceHint = hintForPath('agents.defaults.workspace', uiHints);
|
|
91
34
|
|
|
92
|
-
const providerCatalog = useMemo(
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
const defaultModels = normalizeStringList((provider.defaultModels ?? []).map((model) => toProviderLocalModel(model, aliases)));
|
|
97
|
-
const customModels = normalizeStringList(
|
|
98
|
-
(config?.providers?.[provider.name]?.models ?? []).map((model) => toProviderLocalModel(model, aliases))
|
|
99
|
-
);
|
|
100
|
-
const allModels = normalizeStringList([...defaultModels, ...customModels]);
|
|
101
|
-
const configDisplayName = config?.providers?.[provider.name]?.displayName?.trim();
|
|
102
|
-
return {
|
|
103
|
-
name: provider.name,
|
|
104
|
-
displayName: configDisplayName || provider.displayName || provider.name,
|
|
105
|
-
prefix,
|
|
106
|
-
aliases,
|
|
107
|
-
models: allModels
|
|
108
|
-
};
|
|
109
|
-
});
|
|
110
|
-
}, [meta, config]);
|
|
35
|
+
const providerCatalog = useMemo(
|
|
36
|
+
() => buildProviderModelCatalog({ meta, config }),
|
|
37
|
+
[config, meta]
|
|
38
|
+
);
|
|
111
39
|
|
|
112
40
|
const providerMap = useMemo(() => new Map(providerCatalog.map((provider) => [provider.name, provider])), [providerCatalog]);
|
|
113
41
|
const selectedProvider = providerMap.get(providerName) ?? providerCatalog[0];
|
|
@@ -151,13 +79,7 @@ export function ModelConfig() {
|
|
|
151
79
|
if (!normalizedModelId) {
|
|
152
80
|
return '';
|
|
153
81
|
}
|
|
154
|
-
|
|
155
|
-
return normalizedModelId;
|
|
156
|
-
}
|
|
157
|
-
if (!selectedProvider.prefix) {
|
|
158
|
-
return normalizedModelId;
|
|
159
|
-
}
|
|
160
|
-
return `${selectedProvider.prefix}/${normalizedModelId}`;
|
|
82
|
+
return composeProviderModel(selectedProvider?.prefix ?? '', normalizedModelId);
|
|
161
83
|
}, [modelId, selectedProvider, selectedProviderAliases]);
|
|
162
84
|
|
|
163
85
|
const modelHelpText = t('modelIdentifierHelp') || modelHint?.help || '';
|