@nextclaw/ui 0.5.26 → 0.5.28
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 +16 -0
- package/dist/assets/{ChannelsList-D0Wk08Ki.js → ChannelsList-DgkIu7_t.js} +1 -1
- package/dist/assets/ChatPage-CjzR-76f.js +32 -0
- package/dist/assets/{CronConfig-D-3Y8kWb.js → CronConfig-DREWLzKD.js} +1 -1
- package/dist/assets/{DocBrowser-BSPKhqrK.js → DocBrowser-fXdmzwRi.js} +1 -1
- package/dist/assets/{MarketplacePage-Dkm2FTtN.js → MarketplacePage-zKC-b_O_.js} +1 -1
- package/dist/assets/{ModelConfig-2cpAmvGq.js → ModelConfig-BdogzFBq.js} +1 -1
- package/dist/assets/{ProvidersList-Dot21pAy.js → ProvidersList-BLScOe9j.js} +1 -1
- package/dist/assets/{RuntimeConfig-BNw_Ms_Y.js → RuntimeConfig-CGsoLtsV.js} +1 -1
- package/dist/assets/{SecretsConfig-z8M3PDJP.js → SecretsConfig-B8ZDpRgB.js} +1 -1
- package/dist/assets/{SessionsConfig-XVHZ-FG5.js → SessionsConfig-CcpfGazP.js} +1 -1
- package/dist/assets/{action-link-CpPJJN-z.js → action-link-RjYHzlQk.js} +1 -1
- package/dist/assets/{card-DsZ2Am92.js → card-Bt8T3JA3.js} +1 -1
- package/dist/assets/chat-message-D0s61C4e.js +5 -0
- package/dist/assets/{dialog-BysNu5hM.js → dialog-BNi5ymWD.js} +1 -1
- package/dist/assets/{index-Bny21Br0.js → index-CtNWUrVR.js} +2 -2
- package/dist/assets/{label-q6RASlER.js → label-BDCnjFl3.js} +1 -1
- package/dist/assets/{page-layout-WiVrFc8t.js → page-layout-DWVnm0X8.js} +1 -1
- package/dist/assets/{switch-DM_YYUgB.js → switch-CrmdAOBw.js} +1 -1
- package/dist/assets/{tabs-custom-mlgm-IGH.js → tabs-custom-WJxQwDQy.js} +1 -1
- package/dist/assets/useConfig-COoN7EVf.js +6 -0
- package/dist/assets/{useConfirmDialog-DamaA60g.js → useConfirmDialog-C5hEiEeb.js} +1 -1
- package/dist/index.html +1 -1
- package/package.json +1 -1
- package/src/api/config.ts +14 -1
- package/src/api/types.ts +14 -0
- package/src/components/chat/ChatPage.tsx +66 -36
- package/src/components/chat/ChatThread.tsx +58 -32
- package/src/lib/chat-message.ts +169 -153
- package/dist/assets/ChatPage-Deg2lBH4.js +0 -32
- package/dist/assets/chat-message-Jxa8JFA_.js +0 -9
- package/dist/assets/useConfig-BOn-kp8G.js +0 -6
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
|
2
|
-
import type { SessionEntryView,
|
|
2
|
+
import type { SessionEntryView, SessionEventView } from '@/api/types';
|
|
3
3
|
import { sendChatTurnStream } from '@/api/config';
|
|
4
4
|
import { useConfig, useDeleteSession, useSessionHistory, useSessions } from '@/hooks/useConfig';
|
|
5
5
|
import { useConfirmDialog } from '@/hooks/useConfirmDialog';
|
|
@@ -9,6 +9,7 @@ import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@
|
|
|
9
9
|
import { PageHeader, PageLayout } from '@/components/layout/page-layout';
|
|
10
10
|
import { ChatThread } from '@/components/chat/ChatThread';
|
|
11
11
|
import { cn } from '@/lib/utils';
|
|
12
|
+
import { buildFallbackEventsFromMessages } from '@/lib/chat-message';
|
|
12
13
|
import { formatDateTime, t } from '@/lib/i18n';
|
|
13
14
|
import { MessageSquareText, Plus, RefreshCw, Search, Send, Trash2 } from 'lucide-react';
|
|
14
15
|
|
|
@@ -93,8 +94,9 @@ export function ChatPage() {
|
|
|
93
94
|
const [draft, setDraft] = useState('');
|
|
94
95
|
const [selectedSessionKey, setSelectedSessionKey] = useState<string | null>(() => readStoredSessionKey());
|
|
95
96
|
const [selectedAgentId, setSelectedAgentId] = useState('main');
|
|
96
|
-
const [
|
|
97
|
-
const [
|
|
97
|
+
const [streamingSessionEvents, setStreamingSessionEvents] = useState<SessionEventView[]>([]);
|
|
98
|
+
const [streamingAssistantText, setStreamingAssistantText] = useState('');
|
|
99
|
+
const [streamingAssistantTimestamp, setStreamingAssistantTimestamp] = useState<string | null>(null);
|
|
98
100
|
const [isSending, setIsSending] = useState(false);
|
|
99
101
|
const [queuedMessages, setQueuedMessages] = useState<PendingChatMessage[]>([]);
|
|
100
102
|
|
|
@@ -143,20 +145,32 @@ export function ChatPage() {
|
|
|
143
145
|
[selectedSessionKey, sessions]
|
|
144
146
|
);
|
|
145
147
|
|
|
146
|
-
const
|
|
147
|
-
const
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
148
|
+
const historyData = historyQuery.data;
|
|
149
|
+
const historyMessages = historyData?.messages ?? [];
|
|
150
|
+
const historyEvents =
|
|
151
|
+
historyData?.events && historyData.events.length > 0
|
|
152
|
+
? historyData.events
|
|
153
|
+
: buildFallbackEventsFromMessages(historyMessages);
|
|
154
|
+
const mergedEvents = useMemo(() => {
|
|
155
|
+
const next = [...historyEvents, ...streamingSessionEvents];
|
|
156
|
+
if (streamingAssistantText.trim()) {
|
|
157
|
+
const maxSeq = next.reduce((max, event) => {
|
|
158
|
+
const seq = Number.isFinite(event.seq) ? event.seq : 0;
|
|
159
|
+
return seq > max ? seq : max;
|
|
160
|
+
}, 0);
|
|
161
|
+
next.push({
|
|
162
|
+
seq: maxSeq + 1,
|
|
163
|
+
type: 'stream.assistant_delta',
|
|
164
|
+
timestamp: streamingAssistantTimestamp ?? new Date().toISOString(),
|
|
165
|
+
message: {
|
|
166
|
+
role: 'assistant',
|
|
167
|
+
content: streamingAssistantText,
|
|
168
|
+
timestamp: streamingAssistantTimestamp ?? new Date().toISOString()
|
|
169
|
+
}
|
|
170
|
+
});
|
|
157
171
|
}
|
|
158
172
|
return next;
|
|
159
|
-
}, [
|
|
173
|
+
}, [historyEvents, streamingAssistantText, streamingAssistantTimestamp, streamingSessionEvents]);
|
|
160
174
|
|
|
161
175
|
useEffect(() => {
|
|
162
176
|
if (!selectedSessionKey && filteredSessions.length > 0) {
|
|
@@ -188,7 +202,7 @@ export function ChatPage() {
|
|
|
188
202
|
return;
|
|
189
203
|
}
|
|
190
204
|
element.scrollTop = element.scrollHeight;
|
|
191
|
-
}, [
|
|
205
|
+
}, [mergedEvents, isSending, selectedSessionKey]);
|
|
192
206
|
|
|
193
207
|
useEffect(() => {
|
|
194
208
|
return () => {
|
|
@@ -200,10 +214,11 @@ export function ChatPage() {
|
|
|
200
214
|
streamRunIdRef.current += 1;
|
|
201
215
|
setIsSending(false);
|
|
202
216
|
setQueuedMessages([]);
|
|
203
|
-
|
|
217
|
+
setStreamingSessionEvents([]);
|
|
218
|
+
setStreamingAssistantText('');
|
|
219
|
+
setStreamingAssistantTimestamp(null);
|
|
204
220
|
const next = buildNewSessionKey(selectedAgentId);
|
|
205
221
|
setSelectedSessionKey(next);
|
|
206
|
-
setOptimisticUserMessage(null);
|
|
207
222
|
};
|
|
208
223
|
|
|
209
224
|
const handleDeleteSession = async () => {
|
|
@@ -225,9 +240,10 @@ export function ChatPage() {
|
|
|
225
240
|
streamRunIdRef.current += 1;
|
|
226
241
|
setIsSending(false);
|
|
227
242
|
setQueuedMessages([]);
|
|
228
|
-
|
|
243
|
+
setStreamingSessionEvents([]);
|
|
244
|
+
setStreamingAssistantText('');
|
|
245
|
+
setStreamingAssistantTimestamp(null);
|
|
229
246
|
setSelectedSessionKey(null);
|
|
230
|
-
setOptimisticUserMessage(null);
|
|
231
247
|
await sessionsQuery.refetch();
|
|
232
248
|
}
|
|
233
249
|
}
|
|
@@ -238,17 +254,15 @@ export function ChatPage() {
|
|
|
238
254
|
streamRunIdRef.current += 1;
|
|
239
255
|
const runId = streamRunIdRef.current;
|
|
240
256
|
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
content: item.message,
|
|
245
|
-
timestamp: new Date().toISOString()
|
|
246
|
-
});
|
|
257
|
+
setStreamingSessionEvents([]);
|
|
258
|
+
setStreamingAssistantText('');
|
|
259
|
+
setStreamingAssistantTimestamp(null);
|
|
247
260
|
setIsSending(true);
|
|
248
261
|
|
|
249
262
|
try {
|
|
250
263
|
let streamText = '';
|
|
251
264
|
const streamTimestamp = new Date().toISOString();
|
|
265
|
+
setStreamingAssistantTimestamp(streamTimestamp);
|
|
252
266
|
|
|
253
267
|
const result = await sendChatTurnStream({
|
|
254
268
|
message: item.message,
|
|
@@ -270,17 +284,30 @@ export function ChatPage() {
|
|
|
270
284
|
return;
|
|
271
285
|
}
|
|
272
286
|
streamText += event.delta;
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
287
|
+
setStreamingAssistantText(streamText);
|
|
288
|
+
},
|
|
289
|
+
onSessionEvent: (event) => {
|
|
290
|
+
if (runId !== streamRunIdRef.current) {
|
|
291
|
+
return;
|
|
292
|
+
}
|
|
293
|
+
setStreamingSessionEvents((prev) => {
|
|
294
|
+
const next = [...prev];
|
|
295
|
+
const hit = next.findIndex((item) => item.seq === event.data.seq);
|
|
296
|
+
if (hit >= 0) {
|
|
297
|
+
next[hit] = event.data;
|
|
298
|
+
} else {
|
|
299
|
+
next.push(event.data);
|
|
300
|
+
}
|
|
301
|
+
return next;
|
|
277
302
|
});
|
|
303
|
+
if (event.data.message?.role === 'assistant') {
|
|
304
|
+
setStreamingAssistantText('');
|
|
305
|
+
}
|
|
278
306
|
}
|
|
279
307
|
});
|
|
280
308
|
if (runId !== streamRunIdRef.current) {
|
|
281
309
|
return;
|
|
282
310
|
}
|
|
283
|
-
setOptimisticUserMessage(null);
|
|
284
311
|
if (result.sessionKey !== item.sessionKey) {
|
|
285
312
|
setSelectedSessionKey(result.sessionKey);
|
|
286
313
|
}
|
|
@@ -289,7 +316,9 @@ export function ChatPage() {
|
|
|
289
316
|
if (!activeSessionKey || activeSessionKey === item.sessionKey || activeSessionKey === result.sessionKey) {
|
|
290
317
|
await historyQuery.refetch();
|
|
291
318
|
}
|
|
292
|
-
|
|
319
|
+
setStreamingSessionEvents([]);
|
|
320
|
+
setStreamingAssistantText('');
|
|
321
|
+
setStreamingAssistantTimestamp(null);
|
|
293
322
|
setIsSending(false);
|
|
294
323
|
} catch {
|
|
295
324
|
if (runId !== streamRunIdRef.current) {
|
|
@@ -297,8 +326,9 @@ export function ChatPage() {
|
|
|
297
326
|
}
|
|
298
327
|
streamRunIdRef.current += 1;
|
|
299
328
|
setIsSending(false);
|
|
300
|
-
|
|
301
|
-
|
|
329
|
+
setStreamingSessionEvents([]);
|
|
330
|
+
setStreamingAssistantText('');
|
|
331
|
+
setStreamingAssistantTimestamp(null);
|
|
302
332
|
if (options?.restoreDraftOnError) {
|
|
303
333
|
setDraft((prev) => prev.trim().length === 0 ? item.message : prev);
|
|
304
334
|
}
|
|
@@ -486,10 +516,10 @@ export function ChatPage() {
|
|
|
486
516
|
<div className="text-sm text-gray-500">{t('chatHistoryLoading')}</div>
|
|
487
517
|
) : (
|
|
488
518
|
<>
|
|
489
|
-
{
|
|
519
|
+
{mergedEvents.length === 0 ? (
|
|
490
520
|
<div className="text-sm text-gray-500">{t('chatNoMessages')}</div>
|
|
491
521
|
) : (
|
|
492
|
-
<ChatThread
|
|
522
|
+
<ChatThread events={mergedEvents} isSending={isSending && !streamingAssistantText.trim()} />
|
|
493
523
|
)}
|
|
494
524
|
</>
|
|
495
525
|
)}
|
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
import { useMemo } from 'react';
|
|
2
|
-
import type { SessionMessageView } from '@/api/types';
|
|
2
|
+
import type { SessionEventView, SessionMessageView } from '@/api/types';
|
|
3
3
|
import { cn } from '@/lib/utils';
|
|
4
4
|
import {
|
|
5
|
-
|
|
5
|
+
buildChatTimeline,
|
|
6
6
|
extractMessageText,
|
|
7
7
|
extractToolCards,
|
|
8
|
-
|
|
8
|
+
normalizeChatRole,
|
|
9
9
|
type ChatRole,
|
|
10
|
+
type ChatTimelineAssistantFlowItem,
|
|
10
11
|
type ToolCard
|
|
11
12
|
} from '@/lib/chat-message';
|
|
12
13
|
import { formatDateTime, t } from '@/lib/i18n';
|
|
@@ -16,7 +17,7 @@ import remarkGfm from 'remark-gfm';
|
|
|
16
17
|
import { Bot, Clock3, FileSearch, Globe, Search, SendHorizontal, Terminal, User, Wrench } from 'lucide-react';
|
|
17
18
|
|
|
18
19
|
type ChatThreadProps = {
|
|
19
|
-
|
|
20
|
+
events: SessionEventView[];
|
|
20
21
|
isSending: boolean;
|
|
21
22
|
className?: string;
|
|
22
23
|
};
|
|
@@ -142,12 +143,26 @@ function ToolCardView({ card }: { card: ToolCard }) {
|
|
|
142
143
|
);
|
|
143
144
|
}
|
|
144
145
|
|
|
145
|
-
function
|
|
146
|
-
|
|
146
|
+
function ReasoningBlock({ reasoning, isUser }: { reasoning: string; isUser: boolean }) {
|
|
147
|
+
return (
|
|
148
|
+
<details className="mt-3">
|
|
149
|
+
<summary className={cn('cursor-pointer text-xs', isUser ? 'text-primary-100' : 'text-gray-500')}>
|
|
150
|
+
{t('chatReasoning')}
|
|
151
|
+
</summary>
|
|
152
|
+
<pre className={cn('mt-2 text-[11px] whitespace-pre-wrap break-words rounded-lg p-2', isUser ? 'bg-primary-700/60' : 'bg-gray-100')}>
|
|
153
|
+
{reasoning}
|
|
154
|
+
</pre>
|
|
155
|
+
</details>
|
|
156
|
+
);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
function MessageCard({ message }: { message: SessionMessageView }) {
|
|
160
|
+
const role = normalizeChatRole(message);
|
|
161
|
+
const primaryText = extractMessageText(message.content).trim();
|
|
162
|
+
const primaryReasoning = typeof message.reasoning_content === 'string' ? message.reasoning_content.trim() : '';
|
|
147
163
|
const toolCards = extractToolCards(message);
|
|
148
|
-
const reasoning = typeof message.reasoning_content === 'string' ? message.reasoning_content.trim() : '';
|
|
149
|
-
const shouldRenderText = Boolean(text) && !(role === 'tool' && toolCards.length > 0);
|
|
150
164
|
const isUser = role === 'user';
|
|
165
|
+
const shouldRenderPrimaryText = Boolean(primaryText) && !(role === 'tool' && toolCards.length > 0);
|
|
151
166
|
|
|
152
167
|
return (
|
|
153
168
|
<div
|
|
@@ -160,19 +175,10 @@ function MessageCard({ message, role }: { message: SessionMessageView; role: Cha
|
|
|
160
175
|
: 'bg-orange-50/70 text-gray-900 border-orange-200/80'
|
|
161
176
|
)}
|
|
162
177
|
>
|
|
163
|
-
{
|
|
164
|
-
{
|
|
165
|
-
<details className="mt-3">
|
|
166
|
-
<summary className={cn('cursor-pointer text-xs', isUser ? 'text-primary-100' : 'text-gray-500')}>
|
|
167
|
-
{t('chatReasoning')}
|
|
168
|
-
</summary>
|
|
169
|
-
<pre className={cn('mt-2 text-[11px] whitespace-pre-wrap break-words rounded-lg p-2', isUser ? 'bg-primary-700/60' : 'bg-gray-100')}>
|
|
170
|
-
{reasoning}
|
|
171
|
-
</pre>
|
|
172
|
-
</details>
|
|
173
|
-
)}
|
|
178
|
+
{shouldRenderPrimaryText && <MarkdownBlock text={primaryText} role={role} />}
|
|
179
|
+
{primaryReasoning && <ReasoningBlock reasoning={primaryReasoning} isUser={isUser} />}
|
|
174
180
|
{toolCards.length > 0 && (
|
|
175
|
-
<div className=
|
|
181
|
+
<div className={cn('space-y-2', (shouldRenderPrimaryText || primaryReasoning) && 'mt-3')}>
|
|
176
182
|
{toolCards.map((card, index) => (
|
|
177
183
|
<ToolCardView key={`${card.kind}-${card.name}-${card.callId ?? index}`} card={card} />
|
|
178
184
|
))}
|
|
@@ -182,26 +188,46 @@ function MessageCard({ message, role }: { message: SessionMessageView; role: Cha
|
|
|
182
188
|
);
|
|
183
189
|
}
|
|
184
190
|
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
191
|
+
function AssistantFlowCard({ item }: { item: ChatTimelineAssistantFlowItem }) {
|
|
192
|
+
return (
|
|
193
|
+
<div className="rounded-2xl border px-4 py-3 shadow-sm bg-white text-gray-900 border-gray-200">
|
|
194
|
+
{item.primaryText && <MarkdownBlock text={item.primaryText} role="assistant" />}
|
|
195
|
+
{item.primaryReasoning && <ReasoningBlock reasoning={item.primaryReasoning} isUser={false} />}
|
|
196
|
+
{item.toolCards.length > 0 && (
|
|
197
|
+
<div className={cn('space-y-2', (item.primaryText || item.primaryReasoning) && 'mt-3')}>
|
|
198
|
+
{item.toolCards.map((card, index) => (
|
|
199
|
+
<ToolCardView key={`${card.kind}-${card.name}-${card.callId ?? index}`} card={card} />
|
|
200
|
+
))}
|
|
201
|
+
</div>
|
|
202
|
+
)}
|
|
203
|
+
{item.followupReasoning && <ReasoningBlock reasoning={item.followupReasoning} isUser={false} />}
|
|
204
|
+
{item.followupText && <div className="mt-3"><MarkdownBlock text={item.followupText} role="assistant" /></div>}
|
|
205
|
+
</div>
|
|
206
|
+
);
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
export function ChatThread({ events, isSending, className }: ChatThreadProps) {
|
|
210
|
+
const timeline = useMemo(() => buildChatTimeline(events), [events]);
|
|
188
211
|
|
|
189
212
|
return (
|
|
190
213
|
<div className={cn('space-y-5', className)}>
|
|
191
|
-
{
|
|
192
|
-
const
|
|
214
|
+
{timeline.map((item) => {
|
|
215
|
+
const role = item.kind === 'assistant_flow' ? 'assistant' : item.role;
|
|
216
|
+
const isUser = role === 'user';
|
|
193
217
|
return (
|
|
194
|
-
<div key={
|
|
195
|
-
{!isUser && <RoleAvatar role={
|
|
218
|
+
<div key={item.key} className={cn('flex gap-3', isUser ? 'justify-end' : 'justify-start')}>
|
|
219
|
+
{!isUser && <RoleAvatar role={role} />}
|
|
196
220
|
<div className={cn('max-w-[88%] min-w-[280px] space-y-2', isUser && 'flex flex-col items-end')}>
|
|
197
|
-
{
|
|
198
|
-
<
|
|
199
|
-
)
|
|
221
|
+
{item.kind === 'assistant_flow' ? (
|
|
222
|
+
<AssistantFlowCard item={item} />
|
|
223
|
+
) : (
|
|
224
|
+
<MessageCard message={item.message} />
|
|
225
|
+
)}
|
|
200
226
|
<div className={cn('text-[11px] px-1', isUser ? 'text-primary-300' : 'text-gray-400')}>
|
|
201
|
-
{roleTitle(
|
|
227
|
+
{roleTitle(role)} · {formatDateTime(item.timestamp)}
|
|
202
228
|
</div>
|
|
203
229
|
</div>
|
|
204
|
-
{isUser && <RoleAvatar role={
|
|
230
|
+
{isUser && <RoleAvatar role={role} />}
|
|
205
231
|
</div>
|
|
206
232
|
);
|
|
207
233
|
})}
|