@nextclaw/ui 0.5.22 → 0.5.24
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-dmUfRDVE.js → ChannelsList-CAuBEcOr.js} +1 -1
- package/dist/assets/ChatPage-Bxs3X5OC.js +32 -0
- package/dist/assets/{CronConfig-B15BA--M.js → CronConfig-jOEPCnpf.js} +1 -1
- package/dist/assets/{DocBrowser-CBwMA2wK.js → DocBrowser-wov_cBSN.js} +1 -1
- package/dist/assets/{MarketplacePage-BZRz4tCA.js → MarketplacePage-Cob6DGoO.js} +1 -1
- package/dist/assets/{ModelConfig-CByjZNvf.js → ModelConfig-C_Y3UDYr.js} +1 -1
- package/dist/assets/{ProvidersList-BbsQEnoZ.js → ProvidersList-6A6N2eDT.js} +1 -1
- package/dist/assets/{RuntimeConfig-CrQsFzP7.js → RuntimeConfig-B3k_dMdJ.js} +1 -1
- package/dist/assets/{SecretsConfig-DPZqWmry.js → SecretsConfig-BeCRCCEW.js} +2 -2
- package/dist/assets/{SessionsConfig-Bk7RCsWw.js → SessionsConfig-M32Qm7cL.js} +1 -1
- package/dist/assets/{action-link-CKSHFT5k.js → action-link-DsAjsb68.js} +1 -1
- package/dist/assets/{card-6hv7Kf0F.js → card-uTj7-9XS.js} +1 -1
- package/dist/assets/chat-message-DZV2Z5oc.js +5 -0
- package/dist/assets/{dialog-Bmy_bApp.js → dialog-FRtXcCmk.js} +1 -1
- package/dist/assets/{index-y6creQ7S.js → index-Dw8Ss2WH.js} +2 -2
- package/dist/assets/{label-Btp6gGxV.js → label-DAhEgM6-.js} +1 -1
- package/dist/assets/{page-layout-DWVKbt_g.js → page-layout-DMpNQawS.js} +1 -1
- package/dist/assets/{switch-AeXayTRS.js → switch-D8lSFzq4.js} +1 -1
- package/dist/assets/{tabs-custom-DEZwNpXo.js → tabs-custom-CsFrOXUS.js} +1 -1
- package/dist/assets/useConfig-Cjlx5C-1.js +6 -0
- package/dist/assets/{useConfirmDialog-BY0hni-H.js → useConfirmDialog-D3eI0Hfj.js} +1 -1
- package/dist/index.html +1 -1
- package/package.json +1 -1
- package/src/api/config.ts +159 -2
- package/src/api/types.ts +16 -0
- package/src/components/chat/ChatPage.tsx +74 -18
- package/src/components/chat/ChatThread.tsx +12 -3
- package/src/lib/chat-message.ts +80 -2
- package/dist/assets/ChatPage-CmRkhiCX.js +0 -32
- package/dist/assets/chat-message-B7oqvJ2d.js +0 -3
- package/dist/assets/useConfig-BiQH98MD.js +0 -1
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { useEffect, useMemo, useRef, useState } from 'react';
|
|
2
2
|
import type { SessionEntryView, SessionMessageView } from '@/api/types';
|
|
3
|
-
import {
|
|
3
|
+
import { sendChatTurnStream } from '@/api/config';
|
|
4
|
+
import { useConfig, useDeleteSession, useSessionHistory, useSessions } from '@/hooks/useConfig';
|
|
4
5
|
import { useConfirmDialog } from '@/hooks/useConfirmDialog';
|
|
5
6
|
import { Button } from '@/components/ui/button';
|
|
6
7
|
import { Input } from '@/components/ui/input';
|
|
@@ -68,15 +69,17 @@ export function ChatPage() {
|
|
|
68
69
|
const [selectedSessionKey, setSelectedSessionKey] = useState<string | null>(() => readStoredSessionKey());
|
|
69
70
|
const [selectedAgentId, setSelectedAgentId] = useState('main');
|
|
70
71
|
const [optimisticUserMessage, setOptimisticUserMessage] = useState<SessionMessageView | null>(null);
|
|
72
|
+
const [streamingAssistantMessage, setStreamingAssistantMessage] = useState<SessionMessageView | null>(null);
|
|
73
|
+
const [isSending, setIsSending] = useState(false);
|
|
71
74
|
|
|
72
75
|
const { confirm, ConfirmDialog } = useConfirmDialog();
|
|
73
76
|
const threadRef = useRef<HTMLDivElement | null>(null);
|
|
77
|
+
const streamRunIdRef = useRef(0);
|
|
74
78
|
|
|
75
79
|
const configQuery = useConfig();
|
|
76
80
|
const sessionsQuery = useSessions({ q: query.trim() || undefined, limit: 120, activeMinutes: 0 });
|
|
77
81
|
const historyQuery = useSessionHistory(selectedSessionKey, 300);
|
|
78
82
|
const deleteSession = useDeleteSession();
|
|
79
|
-
const sendChatTurn = useSendChatTurn();
|
|
80
83
|
|
|
81
84
|
const agentOptions = useMemo(() => {
|
|
82
85
|
const list = configQuery.data?.agents.list ?? [];
|
|
@@ -96,12 +99,20 @@ export function ChatPage() {
|
|
|
96
99
|
);
|
|
97
100
|
|
|
98
101
|
const historyMessages = useMemo(() => historyQuery.data?.messages ?? [], [historyQuery.data?.messages]);
|
|
102
|
+
const isGenerating = isSending;
|
|
99
103
|
const mergedMessages = useMemo(() => {
|
|
100
|
-
if (!optimisticUserMessage) {
|
|
104
|
+
if (!optimisticUserMessage && !streamingAssistantMessage) {
|
|
101
105
|
return historyMessages;
|
|
102
106
|
}
|
|
103
|
-
|
|
104
|
-
|
|
107
|
+
const next = [...historyMessages];
|
|
108
|
+
if (optimisticUserMessage) {
|
|
109
|
+
next.push(optimisticUserMessage);
|
|
110
|
+
}
|
|
111
|
+
if (streamingAssistantMessage) {
|
|
112
|
+
next.push(streamingAssistantMessage);
|
|
113
|
+
}
|
|
114
|
+
return next;
|
|
115
|
+
}, [historyMessages, optimisticUserMessage, streamingAssistantMessage]);
|
|
105
116
|
|
|
106
117
|
useEffect(() => {
|
|
107
118
|
if (!selectedSessionKey && sessions.length > 0) {
|
|
@@ -129,9 +140,18 @@ export function ChatPage() {
|
|
|
129
140
|
return;
|
|
130
141
|
}
|
|
131
142
|
element.scrollTop = element.scrollHeight;
|
|
132
|
-
}, [mergedMessages
|
|
143
|
+
}, [mergedMessages, isSending, selectedSessionKey]);
|
|
144
|
+
|
|
145
|
+
useEffect(() => {
|
|
146
|
+
return () => {
|
|
147
|
+
streamRunIdRef.current += 1;
|
|
148
|
+
};
|
|
149
|
+
}, []);
|
|
133
150
|
|
|
134
151
|
const createNewSession = () => {
|
|
152
|
+
streamRunIdRef.current += 1;
|
|
153
|
+
setIsSending(false);
|
|
154
|
+
setStreamingAssistantMessage(null);
|
|
135
155
|
const next = buildNewSessionKey(selectedAgentId);
|
|
136
156
|
setSelectedSessionKey(next);
|
|
137
157
|
setOptimisticUserMessage(null);
|
|
@@ -153,6 +173,9 @@ export function ChatPage() {
|
|
|
153
173
|
{ key: selectedSessionKey },
|
|
154
174
|
{
|
|
155
175
|
onSuccess: async () => {
|
|
176
|
+
streamRunIdRef.current += 1;
|
|
177
|
+
setIsSending(false);
|
|
178
|
+
setStreamingAssistantMessage(null);
|
|
156
179
|
setSelectedSessionKey(null);
|
|
157
180
|
setOptimisticUserMessage(null);
|
|
158
181
|
await sessionsQuery.refetch();
|
|
@@ -163,10 +186,12 @@ export function ChatPage() {
|
|
|
163
186
|
|
|
164
187
|
const handleSend = async () => {
|
|
165
188
|
const message = draft.trim();
|
|
166
|
-
if (!message ||
|
|
189
|
+
if (!message || isGenerating) {
|
|
167
190
|
return;
|
|
168
191
|
}
|
|
169
192
|
|
|
193
|
+
streamRunIdRef.current += 1;
|
|
194
|
+
setStreamingAssistantMessage(null);
|
|
170
195
|
const hadActiveSession = Boolean(selectedSessionKey);
|
|
171
196
|
const sessionKey = selectedSessionKey ?? buildNewSessionKey(selectedAgentId);
|
|
172
197
|
if (!selectedSessionKey) {
|
|
@@ -178,17 +203,43 @@ export function ChatPage() {
|
|
|
178
203
|
content: message,
|
|
179
204
|
timestamp: new Date().toISOString()
|
|
180
205
|
});
|
|
206
|
+
setIsSending(true);
|
|
181
207
|
|
|
182
208
|
try {
|
|
183
|
-
const
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
209
|
+
const runId = streamRunIdRef.current;
|
|
210
|
+
let streamText = '';
|
|
211
|
+
const streamTimestamp = new Date().toISOString();
|
|
212
|
+
|
|
213
|
+
const result = await sendChatTurnStream({
|
|
214
|
+
message,
|
|
215
|
+
sessionKey,
|
|
216
|
+
agentId: selectedAgentId,
|
|
217
|
+
channel: 'ui',
|
|
218
|
+
chatId: 'web-ui'
|
|
219
|
+
}, {
|
|
220
|
+
onReady: (event) => {
|
|
221
|
+
if (runId !== streamRunIdRef.current) {
|
|
222
|
+
return;
|
|
223
|
+
}
|
|
224
|
+
if (event.sessionKey && event.sessionKey !== selectedSessionKey) {
|
|
225
|
+
setSelectedSessionKey(event.sessionKey);
|
|
226
|
+
}
|
|
227
|
+
},
|
|
228
|
+
onDelta: (event) => {
|
|
229
|
+
if (runId !== streamRunIdRef.current) {
|
|
230
|
+
return;
|
|
231
|
+
}
|
|
232
|
+
streamText += event.delta;
|
|
233
|
+
setStreamingAssistantMessage({
|
|
234
|
+
role: 'assistant',
|
|
235
|
+
content: streamText,
|
|
236
|
+
timestamp: streamTimestamp
|
|
237
|
+
});
|
|
190
238
|
}
|
|
191
239
|
});
|
|
240
|
+
if (runId !== streamRunIdRef.current) {
|
|
241
|
+
return;
|
|
242
|
+
}
|
|
192
243
|
setOptimisticUserMessage(null);
|
|
193
244
|
if (result.sessionKey !== sessionKey) {
|
|
194
245
|
setSelectedSessionKey(result.sessionKey);
|
|
@@ -197,7 +248,12 @@ export function ChatPage() {
|
|
|
197
248
|
if (hadActiveSession) {
|
|
198
249
|
await historyQuery.refetch();
|
|
199
250
|
}
|
|
251
|
+
setStreamingAssistantMessage(null);
|
|
252
|
+
setIsSending(false);
|
|
200
253
|
} catch {
|
|
254
|
+
streamRunIdRef.current += 1;
|
|
255
|
+
setIsSending(false);
|
|
256
|
+
setStreamingAssistantMessage(null);
|
|
201
257
|
setOptimisticUserMessage(null);
|
|
202
258
|
setDraft(message);
|
|
203
259
|
}
|
|
@@ -335,7 +391,7 @@ export function ChatPage() {
|
|
|
335
391
|
{mergedMessages.length === 0 ? (
|
|
336
392
|
<div className="text-sm text-gray-500">{t('chatNoMessages')}</div>
|
|
337
393
|
) : (
|
|
338
|
-
<ChatThread messages={mergedMessages} isSending={
|
|
394
|
+
<ChatThread messages={mergedMessages} isSending={isSending && !streamingAssistantMessage} />
|
|
339
395
|
)}
|
|
340
396
|
</>
|
|
341
397
|
)}
|
|
@@ -354,7 +410,7 @@ export function ChatPage() {
|
|
|
354
410
|
}}
|
|
355
411
|
placeholder={t('chatInputPlaceholder')}
|
|
356
412
|
className="w-full min-h-[68px] max-h-[220px] resize-y bg-transparent outline-none text-sm px-2 py-1.5 text-gray-800 placeholder:text-gray-400"
|
|
357
|
-
disabled={
|
|
413
|
+
disabled={isGenerating}
|
|
358
414
|
/>
|
|
359
415
|
<div className="flex items-center justify-between px-2 pb-1">
|
|
360
416
|
<div className="text-[11px] text-gray-400">{t('chatInputHint')}</div>
|
|
@@ -362,10 +418,10 @@ export function ChatPage() {
|
|
|
362
418
|
size="sm"
|
|
363
419
|
className="rounded-lg"
|
|
364
420
|
onClick={() => void handleSend()}
|
|
365
|
-
disabled={
|
|
421
|
+
disabled={isGenerating || draft.trim().length === 0}
|
|
366
422
|
>
|
|
367
423
|
<Send className="h-3.5 w-3.5 mr-1.5" />
|
|
368
|
-
{
|
|
424
|
+
{isGenerating ? t('chatSending') : t('chatSend')}
|
|
369
425
|
</Button>
|
|
370
426
|
</div>
|
|
371
427
|
</div>
|
|
@@ -1,7 +1,14 @@
|
|
|
1
1
|
import { useMemo } from 'react';
|
|
2
2
|
import type { SessionMessageView } from '@/api/types';
|
|
3
3
|
import { cn } from '@/lib/utils';
|
|
4
|
-
import {
|
|
4
|
+
import {
|
|
5
|
+
combineToolCallAndResults,
|
|
6
|
+
extractMessageText,
|
|
7
|
+
extractToolCards,
|
|
8
|
+
groupChatMessages,
|
|
9
|
+
type ChatRole,
|
|
10
|
+
type ToolCard
|
|
11
|
+
} from '@/lib/chat-message';
|
|
5
12
|
import { formatDateTime, t } from '@/lib/i18n';
|
|
6
13
|
import ReactMarkdown from 'react-markdown';
|
|
7
14
|
import rehypeSanitize from 'rehype-sanitize';
|
|
@@ -101,6 +108,7 @@ function ToolCardView({ card }: { card: ToolCard }) {
|
|
|
101
108
|
const output = card.text?.trim() ?? '';
|
|
102
109
|
const showDetails = output.length > TOOL_OUTPUT_PREVIEW_MAX || output.includes('\n');
|
|
103
110
|
const preview = showDetails ? `${output.slice(0, TOOL_OUTPUT_PREVIEW_MAX)}…` : output;
|
|
111
|
+
const showOutputSection = card.kind === 'result' || card.hasResult;
|
|
104
112
|
|
|
105
113
|
return (
|
|
106
114
|
<div className="rounded-xl border border-amber-200/80 bg-amber-50/60 px-3 py-2.5">
|
|
@@ -112,7 +120,7 @@ function ToolCardView({ card }: { card: ToolCard }) {
|
|
|
112
120
|
{card.detail && (
|
|
113
121
|
<div className="mt-1 text-[11px] text-amber-800/90 font-mono break-words">{card.detail}</div>
|
|
114
122
|
)}
|
|
115
|
-
{
|
|
123
|
+
{showOutputSection && (
|
|
116
124
|
<div className="mt-2">
|
|
117
125
|
{!output ? (
|
|
118
126
|
<div className="text-[11px] text-amber-700/80">{t('chatToolNoOutput')}</div>
|
|
@@ -175,7 +183,8 @@ function MessageCard({ message, role }: { message: SessionMessageView; role: Cha
|
|
|
175
183
|
}
|
|
176
184
|
|
|
177
185
|
export function ChatThread({ messages, isSending, className }: ChatThreadProps) {
|
|
178
|
-
const
|
|
186
|
+
const preparedMessages = useMemo(() => combineToolCallAndResults(messages), [messages]);
|
|
187
|
+
const groups = useMemo(() => groupChatMessages(preparedMessages), [preparedMessages]);
|
|
179
188
|
|
|
180
189
|
return (
|
|
181
190
|
<div className={cn('space-y-5', className)}>
|
package/src/lib/chat-message.ts
CHANGED
|
@@ -8,6 +8,7 @@ export type ToolCard = {
|
|
|
8
8
|
detail?: string;
|
|
9
9
|
text?: string;
|
|
10
10
|
callId?: string;
|
|
11
|
+
hasResult?: boolean;
|
|
11
12
|
};
|
|
12
13
|
|
|
13
14
|
export type GroupedChatMessage = {
|
|
@@ -158,11 +159,15 @@ export function extractToolCards(message: SessionMessageView): ToolCard[] {
|
|
|
158
159
|
const fn = isRecord(call.function) ? call.function : null;
|
|
159
160
|
const name = toToolName(fn?.name ?? call.name);
|
|
160
161
|
const args = fn?.arguments ?? call.arguments;
|
|
162
|
+
const resultText = typeof call.result_text === 'string' ? call.result_text.trim() : '';
|
|
163
|
+
const hasResult = call.has_result === true || typeof call.result_text === 'string';
|
|
161
164
|
cards.push({
|
|
162
165
|
kind: 'call',
|
|
163
166
|
name,
|
|
164
167
|
detail: summarizeToolArgs(args),
|
|
165
|
-
callId: typeof call.id === 'string' ? call.id : undefined
|
|
168
|
+
callId: typeof call.id === 'string' ? call.id : undefined,
|
|
169
|
+
text: resultText,
|
|
170
|
+
hasResult
|
|
166
171
|
});
|
|
167
172
|
}
|
|
168
173
|
|
|
@@ -173,13 +178,86 @@ export function extractToolCards(message: SessionMessageView): ToolCard[] {
|
|
|
173
178
|
kind: 'result',
|
|
174
179
|
name: toToolName(message.name ?? cards[0]?.name),
|
|
175
180
|
text,
|
|
176
|
-
callId: typeof message.tool_call_id === 'string' ? message.tool_call_id : undefined
|
|
181
|
+
callId: typeof message.tool_call_id === 'string' ? message.tool_call_id : undefined,
|
|
182
|
+
hasResult: true
|
|
177
183
|
});
|
|
178
184
|
}
|
|
179
185
|
|
|
180
186
|
return cards;
|
|
181
187
|
}
|
|
182
188
|
|
|
189
|
+
type ToolResultBucket = {
|
|
190
|
+
name?: string;
|
|
191
|
+
texts: string[];
|
|
192
|
+
};
|
|
193
|
+
|
|
194
|
+
function cloneMessageForMerge(message: SessionMessageView): SessionMessageView {
|
|
195
|
+
return {
|
|
196
|
+
...message,
|
|
197
|
+
tool_calls: Array.isArray(message.tool_calls)
|
|
198
|
+
? message.tool_calls.map((call) => (isRecord(call) ? { ...call } : call))
|
|
199
|
+
: message.tool_calls
|
|
200
|
+
};
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
export function combineToolCallAndResults(messages: SessionMessageView[]): SessionMessageView[] {
|
|
204
|
+
const cloned = messages.map(cloneMessageForMerge);
|
|
205
|
+
const resultByCallId = new Map<string, ToolResultBucket>();
|
|
206
|
+
|
|
207
|
+
for (const message of cloned) {
|
|
208
|
+
if (normalizeChatRole(message) !== 'tool') {
|
|
209
|
+
continue;
|
|
210
|
+
}
|
|
211
|
+
if (typeof message.tool_call_id !== 'string' || !message.tool_call_id.trim()) {
|
|
212
|
+
continue;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
const callId = message.tool_call_id.trim();
|
|
216
|
+
const text = extractMessageText(message.content).trim();
|
|
217
|
+
const existing = resultByCallId.get(callId) ?? { texts: [] };
|
|
218
|
+
if (typeof message.name === 'string' && message.name.trim()) {
|
|
219
|
+
existing.name = message.name.trim();
|
|
220
|
+
}
|
|
221
|
+
existing.texts.push(text);
|
|
222
|
+
resultByCallId.set(callId, existing);
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
const consumedCallIds = new Set<string>();
|
|
226
|
+
|
|
227
|
+
for (const message of cloned) {
|
|
228
|
+
if (normalizeChatRole(message) !== 'assistant' || !Array.isArray(message.tool_calls)) {
|
|
229
|
+
continue;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
message.tool_calls = message.tool_calls.map((call) => {
|
|
233
|
+
if (!isRecord(call) || typeof call.id !== 'string') {
|
|
234
|
+
return call;
|
|
235
|
+
}
|
|
236
|
+
const result = resultByCallId.get(call.id);
|
|
237
|
+
if (!result) {
|
|
238
|
+
return call;
|
|
239
|
+
}
|
|
240
|
+
consumedCallIds.add(call.id);
|
|
241
|
+
return {
|
|
242
|
+
...call,
|
|
243
|
+
result_text: result.texts.filter(Boolean).join('\n\n'),
|
|
244
|
+
has_result: true,
|
|
245
|
+
result_name: result.name
|
|
246
|
+
};
|
|
247
|
+
}) as Array<Record<string, unknown>>;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
return cloned.filter((message) => {
|
|
251
|
+
if (normalizeChatRole(message) !== 'tool') {
|
|
252
|
+
return true;
|
|
253
|
+
}
|
|
254
|
+
if (typeof message.tool_call_id !== 'string' || !message.tool_call_id.trim()) {
|
|
255
|
+
return true;
|
|
256
|
+
}
|
|
257
|
+
return !consumedCallIds.has(message.tool_call_id.trim());
|
|
258
|
+
});
|
|
259
|
+
}
|
|
260
|
+
|
|
183
261
|
export function groupChatMessages(messages: SessionMessageView[]): GroupedChatMessage[] {
|
|
184
262
|
const groups: GroupedChatMessage[] = [];
|
|
185
263
|
let lastTs = 0;
|