@nextclaw/ui 0.6.10 → 0.6.12
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.eslintrc.cjs +10 -0
- package/CHANGELOG.md +16 -0
- package/dist/assets/ChannelsList-DBDjwf-X.js +1 -0
- package/dist/assets/ChatPage-C18sGGk1.js +36 -0
- package/dist/assets/DocBrowser-ZOplDEMS.js +1 -0
- package/dist/assets/LogoBadge-2LMzEMwe.js +1 -0
- package/dist/assets/MarketplacePage-D4JHYcB5.js +49 -0
- package/dist/assets/ModelConfig-DZVvdLFq.js +1 -0
- package/dist/assets/ProvidersList-Dum31480.js +1 -0
- package/dist/assets/{RuntimeConfig-BO6s-ls-.js → RuntimeConfig-4sb3mpkd.js} +1 -1
- package/dist/assets/SearchConfig-B4u_MxRG.js +1 -0
- package/dist/assets/{SecretsConfig-mayFdxpM.js → SecretsConfig-BQXblZvb.js} +2 -2
- package/dist/assets/SessionsConfig-Jk29xjQU.js +2 -0
- package/dist/assets/{card-BP5YnL-G.js → card-BekAnCgX.js} +1 -1
- package/dist/assets/config-layout-BHnOoweL.js +1 -0
- package/dist/assets/index-BXwjfCEO.css +1 -0
- package/dist/assets/index-Dl6t70wA.js +8 -0
- package/dist/assets/{input-B1D2QX0O.js → input-MMn_Na9q.js} +1 -1
- package/dist/assets/{label-DW0j-fXA.js → label-Dg2ydpN0.js} +1 -1
- package/dist/assets/{page-layout-Ch-H9gD-.js → page-layout-7K0rcz0I.js} +1 -1
- package/dist/assets/session-run-status-CAdjSqeb.js +3 -0
- package/dist/assets/{switch-_cZHlGKB.js → switch-DnDMlDVu.js} +1 -1
- package/dist/assets/{tabs-custom-ARxqYYjG.js → tabs-custom-khLM8lWj.js} +1 -1
- package/dist/assets/{useConfirmDialog-BaU7nIat.js → useConfirmDialog-BYA1XnVU.js} +2 -2
- package/dist/assets/{vendor-C--HHaLf.js → vendor-d7E8OgNx.js} +84 -84
- package/dist/index.html +3 -3
- package/package.json +4 -2
- package/src/App.tsx +3 -2
- package/src/api/config.ts +212 -200
- package/src/api/types.ts +93 -24
- package/src/components/chat/ChatConversationPanel.tsx +102 -121
- package/src/components/chat/ChatPage.tsx +165 -437
- package/src/components/chat/ChatSidebar.tsx +30 -36
- package/src/components/chat/ChatThread.tsx +73 -131
- package/src/components/chat/chat-input/ChatInputBarView.tsx +82 -0
- package/src/components/chat/chat-input/ChatInputBottomToolbar.tsx +71 -0
- package/src/components/chat/chat-input/components/ChatInputModelStateHint.tsx +39 -0
- package/src/components/chat/chat-input/components/ChatInputSelectedSkillsSection.tsx +31 -0
- package/src/components/chat/chat-input/components/ChatInputSlashPanelSection.tsx +112 -0
- package/src/components/chat/chat-input/components/bottom-toolbar/ChatInputAttachButton.tsx +24 -0
- package/src/components/chat/chat-input/components/bottom-toolbar/ChatInputModelSelector.tsx +58 -0
- package/src/components/chat/chat-input/components/bottom-toolbar/ChatInputSendControls.tsx +56 -0
- package/src/components/chat/chat-input/components/bottom-toolbar/ChatInputSessionTypeSelector.tsx +40 -0
- package/src/components/chat/chat-input/useChatInputBarController.ts +313 -0
- package/src/components/chat/chat-input.types.ts +15 -0
- package/src/components/chat/chat-page-data.ts +121 -0
- package/src/components/chat/chat-page-runtime.ts +221 -0
- package/src/components/chat/chat-session-route.ts +59 -0
- package/src/components/chat/chat-stream/nextbot-parsers.ts +52 -0
- package/src/components/chat/chat-stream/nextbot-runtime-agent.ts +413 -0
- package/src/components/chat/chat-stream/stream-event-adapter.ts +98 -0
- package/src/components/chat/chat-stream/transport.ts +159 -0
- package/src/components/chat/chat-stream/types.ts +76 -0
- package/src/components/chat/managers/chat-input.manager.ts +142 -0
- package/src/components/chat/managers/chat-run-status.manager.ts +32 -0
- package/src/components/chat/managers/chat-session-list.manager.ts +77 -0
- package/src/components/chat/managers/chat-stream-actions.manager.ts +34 -0
- package/src/components/chat/managers/chat-thread.manager.ts +86 -0
- package/src/components/chat/managers/chat-ui.manager.ts +65 -0
- package/src/components/chat/presenter/chat-presenter-context.tsx +25 -0
- package/src/components/chat/presenter/chat.presenter.ts +32 -0
- package/src/components/chat/stores/chat-input.store.ts +62 -0
- package/src/components/chat/stores/chat-run-status.store.ts +30 -0
- package/src/components/chat/stores/chat-session-list.store.ts +34 -0
- package/src/components/chat/stores/chat-thread.store.ts +52 -0
- package/src/components/chat/useChatRuntimeController.ts +134 -0
- package/src/components/chat/useChatSessionTypeState.ts +148 -0
- package/src/components/common/MaskedInput.tsx +1 -1
- package/src/components/config/SearchConfig.tsx +297 -0
- package/src/components/layout/Sidebar.tsx +6 -1
- package/src/hooks/useConfig.ts +48 -1
- package/src/hooks/useObservable.ts +20 -0
- package/src/lib/chat-message.ts +2 -202
- package/src/lib/chat-runtime-utils.ts +250 -0
- package/src/lib/i18n.ts +31 -0
- package/tsconfig.json +2 -1
- package/vite.config.ts +2 -1
- package/dist/assets/ChannelsList-TyMb5Mgz.js +0 -1
- package/dist/assets/ChatPage-CQerYqvy.js +0 -34
- package/dist/assets/DocBrowser-CNtrA0ps.js +0 -1
- package/dist/assets/LogoBadge-BLqiOM5D.js +0 -1
- package/dist/assets/MarketplacePage-CotZxxNe.js +0 -49
- package/dist/assets/ModelConfig-CCsQ8KFq.js +0 -1
- package/dist/assets/ProvidersList-BYYX5K_g.js +0 -1
- package/dist/assets/SessionsConfig-DAIczdBj.js +0 -2
- package/dist/assets/index-BUiahmWm.css +0 -1
- package/dist/assets/index-D6_5HaDl.js +0 -7
- package/dist/assets/session-run-status-BUYsQeWs.js +0 -5
- package/src/components/chat/ChatInputBar.tsx +0 -590
- package/src/components/chat/useChatStreamController.ts +0 -591
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { cn } from '@/lib/utils';
|
|
2
2
|
import { LANGUAGE_OPTIONS, t, type I18nLanguage } from '@/lib/i18n';
|
|
3
3
|
import { THEME_OPTIONS, type UiTheme } from '@/lib/theme';
|
|
4
|
-
import { Cpu, GitBranch, History, MessageCircle, MessageSquare, Sparkles, BookOpen, Plug, BrainCircuit, AlarmClock, Languages, Palette, KeyRound, Settings, ArrowLeft } from 'lucide-react';
|
|
4
|
+
import { Cpu, GitBranch, History, MessageCircle, MessageSquare, Sparkles, BookOpen, Plug, BrainCircuit, AlarmClock, Languages, Palette, KeyRound, Settings, ArrowLeft, Search } from 'lucide-react';
|
|
5
5
|
import { NavLink } from 'react-router-dom';
|
|
6
6
|
import { useDocBrowser } from '@/components/doc-browser';
|
|
7
7
|
import { BrandHeader } from '@/components/common/BrandHeader';
|
|
@@ -67,6 +67,11 @@ export function Sidebar({ mode }: SidebarProps) {
|
|
|
67
67
|
label: t('providers'),
|
|
68
68
|
icon: Sparkles,
|
|
69
69
|
},
|
|
70
|
+
{
|
|
71
|
+
target: '/search',
|
|
72
|
+
label: t('searchChannels'),
|
|
73
|
+
icon: Search,
|
|
74
|
+
},
|
|
70
75
|
{
|
|
71
76
|
target: '/channels',
|
|
72
77
|
label: t('channels'),
|
package/src/hooks/useConfig.ts
CHANGED
|
@@ -5,6 +5,7 @@ import {
|
|
|
5
5
|
fetchConfigMeta,
|
|
6
6
|
fetchConfigSchema,
|
|
7
7
|
updateModel,
|
|
8
|
+
updateSearch,
|
|
8
9
|
createProvider,
|
|
9
10
|
deleteProvider,
|
|
10
11
|
updateProvider,
|
|
@@ -24,6 +25,7 @@ import {
|
|
|
24
25
|
fetchChatRun,
|
|
25
26
|
fetchChatRuns,
|
|
26
27
|
fetchChatCapabilities,
|
|
28
|
+
fetchChatSessionTypes,
|
|
27
29
|
fetchCronJobs,
|
|
28
30
|
deleteCronJob,
|
|
29
31
|
setCronJobEnabled,
|
|
@@ -80,6 +82,22 @@ export function useUpdateModel() {
|
|
|
80
82
|
});
|
|
81
83
|
}
|
|
82
84
|
|
|
85
|
+
export function useUpdateSearch() {
|
|
86
|
+
const queryClient = useQueryClient();
|
|
87
|
+
|
|
88
|
+
return useMutation({
|
|
89
|
+
mutationFn: ({ data }: { data: Parameters<typeof updateSearch>[0] }) => updateSearch(data),
|
|
90
|
+
onSuccess: () => {
|
|
91
|
+
queryClient.invalidateQueries({ queryKey: ['config'] });
|
|
92
|
+
queryClient.invalidateQueries({ queryKey: ['config-meta'] });
|
|
93
|
+
toast.success(t('configSavedApplied'));
|
|
94
|
+
},
|
|
95
|
+
onError: (error: Error) => {
|
|
96
|
+
toast.error(t('configSaveFailed') + ': ' + error.message);
|
|
97
|
+
}
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
|
|
83
101
|
export function useUpdateProvider() {
|
|
84
102
|
const queryClient = useQueryClient();
|
|
85
103
|
|
|
@@ -294,9 +312,26 @@ export function useChatCapabilities(params?: { sessionKey?: string | null; agent
|
|
|
294
312
|
});
|
|
295
313
|
}
|
|
296
314
|
|
|
297
|
-
export function
|
|
315
|
+
export function useChatSessionTypes() {
|
|
316
|
+
return useQuery({
|
|
317
|
+
queryKey: ['chat-session-types'],
|
|
318
|
+
queryFn: fetchChatSessionTypes,
|
|
319
|
+
staleTime: 10_000,
|
|
320
|
+
retry: false
|
|
321
|
+
});
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
export function useChatRuns(params?: {
|
|
325
|
+
sessionKey?: string | null;
|
|
326
|
+
states?: Array<'queued' | 'running' | 'completed' | 'failed' | 'aborted'>;
|
|
327
|
+
limit?: number;
|
|
328
|
+
syncActiveStates?: boolean;
|
|
329
|
+
isLocallyRunning?: boolean;
|
|
330
|
+
}) {
|
|
298
331
|
const sessionKey = params?.sessionKey?.trim() || undefined;
|
|
299
332
|
const states = Array.isArray(params?.states) && params.states.length > 0 ? params.states : undefined;
|
|
333
|
+
const isActiveStatesQuery = Boolean(states?.some((state) => state === 'queued' || state === 'running'));
|
|
334
|
+
const shouldSyncActiveStates = Boolean(params?.syncActiveStates && isActiveStatesQuery);
|
|
300
335
|
return useQuery({
|
|
301
336
|
queryKey: ['chat-runs', sessionKey ?? null, states ?? null, params?.limit ?? null],
|
|
302
337
|
queryFn: () => fetchChatRuns({
|
|
@@ -306,6 +341,18 @@ export function useChatRuns(params?: { sessionKey?: string | null; states?: Arra
|
|
|
306
341
|
}),
|
|
307
342
|
enabled: Boolean(sessionKey) || Boolean(states),
|
|
308
343
|
staleTime: 5_000,
|
|
344
|
+
refetchInterval: (query) => {
|
|
345
|
+
if (!shouldSyncActiveStates) {
|
|
346
|
+
return false;
|
|
347
|
+
}
|
|
348
|
+
if (params?.isLocallyRunning) {
|
|
349
|
+
return 800;
|
|
350
|
+
}
|
|
351
|
+
const data = query.state.data;
|
|
352
|
+
const hasActiveRuns = Array.isArray(data?.runs) && data.runs.length > 0;
|
|
353
|
+
return hasActiveRuns ? 800 : false;
|
|
354
|
+
},
|
|
355
|
+
refetchIntervalInBackground: false,
|
|
309
356
|
retry: false
|
|
310
357
|
});
|
|
311
358
|
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { useEffect, useState } from 'react';
|
|
2
|
+
import type { BehaviorSubject, Observable } from 'rxjs';
|
|
3
|
+
|
|
4
|
+
export function useValueFromBehaviorSubject<T>(subject: BehaviorSubject<T>): T {
|
|
5
|
+
const [state, setState] = useState<T>(subject.getValue());
|
|
6
|
+
useEffect(() => {
|
|
7
|
+
const subscription = subject.subscribe(setState);
|
|
8
|
+
return () => subscription.unsubscribe();
|
|
9
|
+
}, [subject]);
|
|
10
|
+
return state;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function useValueFromObservable<T>(observable: Observable<T>, defaultValue: T): T {
|
|
14
|
+
const [state, setState] = useState<T>(defaultValue);
|
|
15
|
+
useEffect(() => {
|
|
16
|
+
const subscription = observable.subscribe(setState);
|
|
17
|
+
return () => subscription.unsubscribe();
|
|
18
|
+
}, [observable]);
|
|
19
|
+
return state;
|
|
20
|
+
}
|
package/src/lib/chat-message.ts
CHANGED
|
@@ -11,37 +11,6 @@ export type ToolCard = {
|
|
|
11
11
|
hasResult?: boolean;
|
|
12
12
|
};
|
|
13
13
|
|
|
14
|
-
export type ChatTimelineMessageItem = {
|
|
15
|
-
kind: 'message';
|
|
16
|
-
key: string;
|
|
17
|
-
role: ChatRole;
|
|
18
|
-
timestamp: string;
|
|
19
|
-
message: SessionMessageView;
|
|
20
|
-
};
|
|
21
|
-
|
|
22
|
-
export type ChatTimelineAssistantTurnSegment =
|
|
23
|
-
| {
|
|
24
|
-
kind: 'assistant_message';
|
|
25
|
-
key: string;
|
|
26
|
-
text: string;
|
|
27
|
-
reasoning: string;
|
|
28
|
-
}
|
|
29
|
-
| {
|
|
30
|
-
kind: 'tool_card';
|
|
31
|
-
key: string;
|
|
32
|
-
card: ToolCard;
|
|
33
|
-
};
|
|
34
|
-
|
|
35
|
-
export type ChatTimelineAssistantTurnItem = {
|
|
36
|
-
kind: 'assistant_turn';
|
|
37
|
-
key: string;
|
|
38
|
-
role: 'assistant';
|
|
39
|
-
timestamp: string;
|
|
40
|
-
segments: ChatTimelineAssistantTurnSegment[];
|
|
41
|
-
};
|
|
42
|
-
|
|
43
|
-
export type ChatTimelineItem = ChatTimelineMessageItem | ChatTimelineAssistantTurnItem;
|
|
44
|
-
|
|
45
14
|
const TOOL_DETAIL_FIELDS = ['cmd', 'command', 'query', 'q', 'path', 'url', 'to', 'channel', 'agentId', 'sessionKey'];
|
|
46
15
|
|
|
47
16
|
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
@@ -55,7 +24,7 @@ function truncateText(value: string, maxChars = 2400): string {
|
|
|
55
24
|
return `${value.slice(0, maxChars)}\n…`;
|
|
56
25
|
}
|
|
57
26
|
|
|
58
|
-
function stringifyUnknown(value: unknown): string {
|
|
27
|
+
export function stringifyUnknown(value: unknown): string {
|
|
59
28
|
if (typeof value === 'string') {
|
|
60
29
|
return value;
|
|
61
30
|
}
|
|
@@ -91,7 +60,7 @@ function parseArgsObject(value: unknown): Record<string, unknown> | null {
|
|
|
91
60
|
}
|
|
92
61
|
}
|
|
93
62
|
|
|
94
|
-
function summarizeToolArgs(args: unknown): string | undefined {
|
|
63
|
+
export function summarizeToolArgs(args: unknown): string | undefined {
|
|
95
64
|
const parsed = parseArgsObject(args);
|
|
96
65
|
if (!parsed) {
|
|
97
66
|
const text = stringifyUnknown(args).trim();
|
|
@@ -212,20 +181,6 @@ export function extractToolCards(message: SessionMessageView): ToolCard[] {
|
|
|
212
181
|
return cards;
|
|
213
182
|
}
|
|
214
183
|
|
|
215
|
-
function normalizeEvent(event: SessionEventView, index: number): SessionEventView & { _idx: number; _seq: number } {
|
|
216
|
-
const seq = Number.isFinite(event.seq) && event.seq > 0 ? Math.trunc(event.seq) : index + 1;
|
|
217
|
-
const timestamp =
|
|
218
|
-
typeof event.timestamp === 'string' && event.timestamp
|
|
219
|
-
? event.timestamp
|
|
220
|
-
: event.message?.timestamp ?? new Date().toISOString();
|
|
221
|
-
return {
|
|
222
|
-
...event,
|
|
223
|
-
timestamp,
|
|
224
|
-
_idx: index,
|
|
225
|
-
_seq: seq
|
|
226
|
-
};
|
|
227
|
-
}
|
|
228
|
-
|
|
229
184
|
function inferEventTypeFromMessage(message: SessionMessageView): string {
|
|
230
185
|
const role = normalizeChatRole(message);
|
|
231
186
|
if (role === 'assistant' && hasToolCalls(message)) {
|
|
@@ -245,158 +200,3 @@ export function buildFallbackEventsFromMessages(messages: SessionMessageView[]):
|
|
|
245
200
|
message
|
|
246
201
|
}));
|
|
247
202
|
}
|
|
248
|
-
|
|
249
|
-
function appendText(base: string, next: string): string {
|
|
250
|
-
if (!next) {
|
|
251
|
-
return base;
|
|
252
|
-
}
|
|
253
|
-
if (!base) {
|
|
254
|
-
return next;
|
|
255
|
-
}
|
|
256
|
-
return `${base}\n\n${next}`;
|
|
257
|
-
}
|
|
258
|
-
|
|
259
|
-
export function buildChatTimeline(events: SessionEventView[]): ChatTimelineItem[] {
|
|
260
|
-
const normalized = events
|
|
261
|
-
.map((event, index) => normalizeEvent(event, index))
|
|
262
|
-
.sort((left, right) => {
|
|
263
|
-
if (left._seq !== right._seq) {
|
|
264
|
-
return left._seq - right._seq;
|
|
265
|
-
}
|
|
266
|
-
const leftTs = Date.parse(left.timestamp);
|
|
267
|
-
const rightTs = Date.parse(right.timestamp);
|
|
268
|
-
if (Number.isFinite(leftTs) && Number.isFinite(rightTs) && leftTs !== rightTs) {
|
|
269
|
-
return leftTs - rightTs;
|
|
270
|
-
}
|
|
271
|
-
return left._idx - right._idx;
|
|
272
|
-
});
|
|
273
|
-
|
|
274
|
-
const timeline: ChatTimelineItem[] = [];
|
|
275
|
-
let activeTurn:
|
|
276
|
-
| {
|
|
277
|
-
item: ChatTimelineAssistantTurnItem;
|
|
278
|
-
cardByCallId: Map<string, ToolCard>;
|
|
279
|
-
}
|
|
280
|
-
| null = null;
|
|
281
|
-
|
|
282
|
-
const closeActiveTurn = () => {
|
|
283
|
-
activeTurn = null;
|
|
284
|
-
};
|
|
285
|
-
|
|
286
|
-
const ensureActiveTurn = (eventKey: string, timestamp: string) => {
|
|
287
|
-
if (activeTurn) {
|
|
288
|
-
activeTurn.item.timestamp = timestamp;
|
|
289
|
-
return activeTurn;
|
|
290
|
-
}
|
|
291
|
-
const item: ChatTimelineAssistantTurnItem = {
|
|
292
|
-
kind: 'assistant_turn',
|
|
293
|
-
key: `turn-${eventKey}`,
|
|
294
|
-
role: 'assistant',
|
|
295
|
-
timestamp,
|
|
296
|
-
segments: []
|
|
297
|
-
};
|
|
298
|
-
timeline.push(item);
|
|
299
|
-
activeTurn = {
|
|
300
|
-
item,
|
|
301
|
-
cardByCallId: new Map<string, ToolCard>()
|
|
302
|
-
};
|
|
303
|
-
return activeTurn;
|
|
304
|
-
};
|
|
305
|
-
|
|
306
|
-
const pushAssistantMessageSegment = (
|
|
307
|
-
target: { item: ChatTimelineAssistantTurnItem },
|
|
308
|
-
eventKey: string,
|
|
309
|
-
message: SessionMessageView
|
|
310
|
-
) => {
|
|
311
|
-
const text = extractMessageText(message.content).trim();
|
|
312
|
-
const reasoning =
|
|
313
|
-
typeof message.reasoning_content === 'string' ? message.reasoning_content.trim() : '';
|
|
314
|
-
if (!text && !reasoning) {
|
|
315
|
-
return;
|
|
316
|
-
}
|
|
317
|
-
target.item.segments.push({
|
|
318
|
-
kind: 'assistant_message',
|
|
319
|
-
key: `assistant-${eventKey}-${target.item.segments.length}`,
|
|
320
|
-
text,
|
|
321
|
-
reasoning
|
|
322
|
-
});
|
|
323
|
-
};
|
|
324
|
-
|
|
325
|
-
for (const event of normalized) {
|
|
326
|
-
const message = event.message;
|
|
327
|
-
if (!message) {
|
|
328
|
-
continue;
|
|
329
|
-
}
|
|
330
|
-
|
|
331
|
-
const role = normalizeChatRole(message);
|
|
332
|
-
const timestamp =
|
|
333
|
-
typeof message.timestamp === 'string' && message.timestamp
|
|
334
|
-
? message.timestamp
|
|
335
|
-
: event.timestamp;
|
|
336
|
-
const eventKey = `${event._seq}-${event._idx}`;
|
|
337
|
-
|
|
338
|
-
if (role === 'assistant') {
|
|
339
|
-
const turn = ensureActiveTurn(eventKey, timestamp);
|
|
340
|
-
pushAssistantMessageSegment(turn, eventKey, message);
|
|
341
|
-
if (!hasToolCalls(message)) {
|
|
342
|
-
continue;
|
|
343
|
-
}
|
|
344
|
-
|
|
345
|
-
const toolCards = buildToolCallCards(message);
|
|
346
|
-
for (const card of toolCards) {
|
|
347
|
-
turn.item.segments.push({
|
|
348
|
-
kind: 'tool_card',
|
|
349
|
-
key: `tool-call-${eventKey}-${turn.item.segments.length}`,
|
|
350
|
-
card
|
|
351
|
-
});
|
|
352
|
-
if (typeof card.callId === 'string' && card.callId.trim()) {
|
|
353
|
-
turn.cardByCallId.set(card.callId, card);
|
|
354
|
-
}
|
|
355
|
-
}
|
|
356
|
-
continue;
|
|
357
|
-
}
|
|
358
|
-
|
|
359
|
-
if (role === 'tool') {
|
|
360
|
-
const turn = ensureActiveTurn(eventKey, timestamp);
|
|
361
|
-
const callId =
|
|
362
|
-
typeof message.tool_call_id === 'string' && message.tool_call_id.trim()
|
|
363
|
-
? message.tool_call_id.trim()
|
|
364
|
-
: undefined;
|
|
365
|
-
if (callId && turn.cardByCallId.has(callId)) {
|
|
366
|
-
const card = turn.cardByCallId.get(callId)!;
|
|
367
|
-
const resultText = extractMessageText(message.content).trim();
|
|
368
|
-
card.text = appendText(card.text ?? '', resultText);
|
|
369
|
-
card.hasResult = true;
|
|
370
|
-
if (typeof message.name === 'string' && message.name.trim()) {
|
|
371
|
-
card.name = message.name.trim();
|
|
372
|
-
}
|
|
373
|
-
turn.item.timestamp = timestamp;
|
|
374
|
-
continue;
|
|
375
|
-
}
|
|
376
|
-
|
|
377
|
-
turn.item.segments.push({
|
|
378
|
-
kind: 'tool_card',
|
|
379
|
-
key: `tool-result-${eventKey}-${turn.item.segments.length}`,
|
|
380
|
-
card: {
|
|
381
|
-
kind: 'result',
|
|
382
|
-
name: toToolName(message.name),
|
|
383
|
-
text: extractMessageText(message.content).trim(),
|
|
384
|
-
callId,
|
|
385
|
-
hasResult: true
|
|
386
|
-
}
|
|
387
|
-
});
|
|
388
|
-
continue;
|
|
389
|
-
}
|
|
390
|
-
|
|
391
|
-
timeline.push({
|
|
392
|
-
kind: 'message',
|
|
393
|
-
key: `message-${event._seq}-${event._idx}`,
|
|
394
|
-
role,
|
|
395
|
-
timestamp,
|
|
396
|
-
message
|
|
397
|
-
});
|
|
398
|
-
closeActiveTurn();
|
|
399
|
-
}
|
|
400
|
-
|
|
401
|
-
return timeline;
|
|
402
|
-
}
|
|
@@ -0,0 +1,250 @@
|
|
|
1
|
+
import type { SessionMessageView } from '@/api/types';
|
|
2
|
+
import { extractMessageText } from '@/lib/chat-message';
|
|
3
|
+
import { ToolInvocationStatus, type UIMessage } from '@nextclaw/agent-chat';
|
|
4
|
+
|
|
5
|
+
export { isAbortLikeError, formatSendError, buildLocalAssistantMessage } from '@nextclaw/agent-chat';
|
|
6
|
+
|
|
7
|
+
export function normalizeRequestedSkills(value: string[] | undefined): string[] {
|
|
8
|
+
if (!Array.isArray(value)) {
|
|
9
|
+
return [];
|
|
10
|
+
}
|
|
11
|
+
const deduped = new Set<string>();
|
|
12
|
+
for (const item of value) {
|
|
13
|
+
const trimmed = item.trim();
|
|
14
|
+
if (trimmed) {
|
|
15
|
+
deduped.add(trimmed);
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
return [...deduped];
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function buildUiMessagesFromHistoryMessages(messages: SessionMessageView[]): UIMessage[] {
|
|
22
|
+
const normalizedToolRoles = new Set(['tool', 'tool_result', 'toolresult', 'function']);
|
|
23
|
+
const output: UIMessage[] = [];
|
|
24
|
+
let cursor = 0;
|
|
25
|
+
let assistantIndex = 0;
|
|
26
|
+
let activeAssistant: UIMessage | null = null;
|
|
27
|
+
|
|
28
|
+
const buildId = (role: UIMessage['role'], timestamp: string) => {
|
|
29
|
+
cursor += 1;
|
|
30
|
+
return `history-${role}-${timestamp || 'unknown'}-${cursor}`;
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
const parseArgsPayload = (raw: unknown): { args: string; parsedArgs?: unknown } => {
|
|
34
|
+
const args = typeof raw === 'string' ? raw : JSON.stringify(raw ?? {});
|
|
35
|
+
try {
|
|
36
|
+
return { args, parsedArgs: JSON.parse(args) };
|
|
37
|
+
} catch {
|
|
38
|
+
return { args };
|
|
39
|
+
}
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
const findToolPartIndex = (parts: UIMessage['parts'], toolCallId: string): number => {
|
|
43
|
+
for (let index = parts.length - 1; index >= 0; index -= 1) {
|
|
44
|
+
const part = parts[index];
|
|
45
|
+
if (part.type === 'tool-invocation' && part.toolInvocation.toolCallId === toolCallId) {
|
|
46
|
+
return index;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
return -1;
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
const ensureAssistant = (timestamp: string): UIMessage => {
|
|
53
|
+
if (activeAssistant) {
|
|
54
|
+
activeAssistant = {
|
|
55
|
+
...activeAssistant,
|
|
56
|
+
meta: {
|
|
57
|
+
...activeAssistant.meta,
|
|
58
|
+
timestamp
|
|
59
|
+
}
|
|
60
|
+
};
|
|
61
|
+
return activeAssistant;
|
|
62
|
+
}
|
|
63
|
+
assistantIndex += 1;
|
|
64
|
+
activeAssistant = {
|
|
65
|
+
id: `history-assistant-${assistantIndex}-${timestamp || 'unknown'}`,
|
|
66
|
+
role: 'assistant',
|
|
67
|
+
parts: [],
|
|
68
|
+
meta: {
|
|
69
|
+
source: 'history',
|
|
70
|
+
status: 'final',
|
|
71
|
+
timestamp
|
|
72
|
+
}
|
|
73
|
+
};
|
|
74
|
+
return activeAssistant;
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
const flushAssistant = () => {
|
|
78
|
+
if (!activeAssistant) {
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
81
|
+
if (activeAssistant.parts.length > 0) {
|
|
82
|
+
output.push(activeAssistant);
|
|
83
|
+
}
|
|
84
|
+
activeAssistant = null;
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
const appendAssistantText = (timestamp: string, text: string) => {
|
|
88
|
+
if (!text) {
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
91
|
+
const assistant = ensureAssistant(timestamp);
|
|
92
|
+
assistant.parts = [...assistant.parts, { type: 'text', text }];
|
|
93
|
+
};
|
|
94
|
+
|
|
95
|
+
const appendAssistantReasoning = (timestamp: string, reasoning: string) => {
|
|
96
|
+
if (!reasoning) {
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
99
|
+
const assistant = ensureAssistant(timestamp);
|
|
100
|
+
assistant.parts = [...assistant.parts, { type: 'reasoning', reasoning, details: [] }];
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
const appendAssistantToolCall = (params: {
|
|
104
|
+
timestamp: string;
|
|
105
|
+
toolCallId: string;
|
|
106
|
+
toolName: string;
|
|
107
|
+
args: string;
|
|
108
|
+
parsedArgs?: unknown;
|
|
109
|
+
}) => {
|
|
110
|
+
const assistant = ensureAssistant(params.timestamp);
|
|
111
|
+
const partIndex = findToolPartIndex(assistant.parts, params.toolCallId);
|
|
112
|
+
const part = {
|
|
113
|
+
type: 'tool-invocation' as const,
|
|
114
|
+
toolInvocation: {
|
|
115
|
+
status: ToolInvocationStatus.CALL,
|
|
116
|
+
toolCallId: params.toolCallId,
|
|
117
|
+
toolName: params.toolName,
|
|
118
|
+
args: params.args,
|
|
119
|
+
parsedArgs: params.parsedArgs
|
|
120
|
+
}
|
|
121
|
+
};
|
|
122
|
+
if (partIndex >= 0) {
|
|
123
|
+
assistant.parts = [...assistant.parts.slice(0, partIndex), part, ...assistant.parts.slice(partIndex + 1)];
|
|
124
|
+
return;
|
|
125
|
+
}
|
|
126
|
+
assistant.parts = [...assistant.parts, part];
|
|
127
|
+
};
|
|
128
|
+
|
|
129
|
+
const appendAssistantToolResult = (params: {
|
|
130
|
+
timestamp: string;
|
|
131
|
+
toolCallId: string;
|
|
132
|
+
toolName: string;
|
|
133
|
+
result: unknown;
|
|
134
|
+
}) => {
|
|
135
|
+
if (!params.toolCallId) {
|
|
136
|
+
return;
|
|
137
|
+
}
|
|
138
|
+
const assistant = ensureAssistant(params.timestamp);
|
|
139
|
+
const partIndex = findToolPartIndex(assistant.parts, params.toolCallId);
|
|
140
|
+
if (partIndex < 0) {
|
|
141
|
+
assistant.parts = [
|
|
142
|
+
...assistant.parts,
|
|
143
|
+
{
|
|
144
|
+
type: 'tool-invocation',
|
|
145
|
+
toolInvocation: {
|
|
146
|
+
status: ToolInvocationStatus.RESULT,
|
|
147
|
+
toolCallId: params.toolCallId,
|
|
148
|
+
toolName: params.toolName,
|
|
149
|
+
args: '{}',
|
|
150
|
+
parsedArgs: undefined,
|
|
151
|
+
result: params.result
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
];
|
|
155
|
+
return;
|
|
156
|
+
}
|
|
157
|
+
const part = assistant.parts[partIndex];
|
|
158
|
+
if (part.type !== 'tool-invocation') {
|
|
159
|
+
return;
|
|
160
|
+
}
|
|
161
|
+
assistant.parts = [
|
|
162
|
+
...assistant.parts.slice(0, partIndex),
|
|
163
|
+
{
|
|
164
|
+
...part,
|
|
165
|
+
toolInvocation: {
|
|
166
|
+
...part.toolInvocation,
|
|
167
|
+
status: ToolInvocationStatus.RESULT,
|
|
168
|
+
result: params.result
|
|
169
|
+
}
|
|
170
|
+
},
|
|
171
|
+
...assistant.parts.slice(partIndex + 1)
|
|
172
|
+
];
|
|
173
|
+
};
|
|
174
|
+
|
|
175
|
+
for (const message of messages) {
|
|
176
|
+
const roleValue = message.role?.toLowerCase().trim();
|
|
177
|
+
if (!roleValue) {
|
|
178
|
+
continue;
|
|
179
|
+
}
|
|
180
|
+
const timestamp = message.timestamp;
|
|
181
|
+
|
|
182
|
+
if (roleValue === 'user' || roleValue === 'system' || roleValue === 'data') {
|
|
183
|
+
flushAssistant();
|
|
184
|
+
const text = extractMessageText(message.content).trim();
|
|
185
|
+
if (!text) {
|
|
186
|
+
continue;
|
|
187
|
+
}
|
|
188
|
+
output.push({
|
|
189
|
+
id: buildId(roleValue as UIMessage['role'], timestamp),
|
|
190
|
+
role: roleValue as UIMessage['role'],
|
|
191
|
+
parts: [{ type: 'text', text }],
|
|
192
|
+
meta: {
|
|
193
|
+
source: 'history',
|
|
194
|
+
status: 'final',
|
|
195
|
+
timestamp
|
|
196
|
+
}
|
|
197
|
+
});
|
|
198
|
+
continue;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
if (roleValue === 'assistant') {
|
|
202
|
+
const text = extractMessageText(message.content).trim();
|
|
203
|
+
if (text) {
|
|
204
|
+
appendAssistantText(timestamp, text);
|
|
205
|
+
}
|
|
206
|
+
if (typeof message.reasoning_content === 'string' && message.reasoning_content.trim()) {
|
|
207
|
+
appendAssistantReasoning(timestamp, message.reasoning_content.trim());
|
|
208
|
+
}
|
|
209
|
+
if (Array.isArray(message.tool_calls)) {
|
|
210
|
+
for (const call of message.tool_calls) {
|
|
211
|
+
if (!call || typeof call !== 'object') {
|
|
212
|
+
continue;
|
|
213
|
+
}
|
|
214
|
+
const callRecord = call as Record<string, unknown>;
|
|
215
|
+
const fnValue = callRecord.function;
|
|
216
|
+
const fn = typeof fnValue === 'object' && fnValue ? (fnValue as { name?: unknown; arguments?: unknown }) : null;
|
|
217
|
+
const toolCallId = typeof callRecord.id === 'string' ? callRecord.id.trim() : '';
|
|
218
|
+
if (!toolCallId) {
|
|
219
|
+
continue;
|
|
220
|
+
}
|
|
221
|
+
const toolName =
|
|
222
|
+
typeof fn?.name === 'string' ? fn.name : typeof callRecord.name === 'string' ? callRecord.name : 'tool';
|
|
223
|
+
const payload = parseArgsPayload(fn?.arguments ?? callRecord.arguments ?? '');
|
|
224
|
+
appendAssistantToolCall({
|
|
225
|
+
timestamp,
|
|
226
|
+
toolCallId,
|
|
227
|
+
toolName,
|
|
228
|
+
args: payload.args,
|
|
229
|
+
parsedArgs: payload.parsedArgs
|
|
230
|
+
});
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
continue;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
if (normalizedToolRoles.has(roleValue)) {
|
|
237
|
+
const toolCallId = typeof message.tool_call_id === 'string' ? message.tool_call_id.trim() : '';
|
|
238
|
+
const toolName = typeof message.name === 'string' && message.name.trim() ? message.name.trim() : 'tool';
|
|
239
|
+
appendAssistantToolResult({
|
|
240
|
+
timestamp,
|
|
241
|
+
toolCallId,
|
|
242
|
+
toolName,
|
|
243
|
+
result: message.content
|
|
244
|
+
});
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
flushAssistant();
|
|
249
|
+
return output;
|
|
250
|
+
}
|
package/src/lib/i18n.ts
CHANGED
|
@@ -125,6 +125,7 @@ export const LABELS: Record<string, { zh: string; en: string }> = {
|
|
|
125
125
|
// Navigation
|
|
126
126
|
chat: { zh: '对话', en: 'Chat' },
|
|
127
127
|
model: { zh: '模型', en: 'Model' },
|
|
128
|
+
searchChannels: { zh: '搜索渠道', en: 'Search Channels' },
|
|
128
129
|
providers: { zh: '提供商', en: 'Providers' },
|
|
129
130
|
channels: { zh: '渠道', en: 'Channels' },
|
|
130
131
|
cron: { zh: '定时任务', en: 'Cron Jobs' },
|
|
@@ -182,6 +183,27 @@ export const LABELS: Record<string, { zh: string; en: string }> = {
|
|
|
182
183
|
saveChanges: { zh: '保存变更', en: 'Save Changes' },
|
|
183
184
|
|
|
184
185
|
// Provider
|
|
186
|
+
searchPageTitle: { zh: '搜索渠道', en: 'Search Channels' },
|
|
187
|
+
searchPageDescription: { zh: '配置网页搜索提供商', en: 'Configure web search providers.' },
|
|
188
|
+
searchActiveProvider: { zh: '当前搜索提供商', en: 'Active Search Provider' },
|
|
189
|
+
searchDefaultMaxResults: { zh: '默认返回条数', en: 'Default Result Count' },
|
|
190
|
+
searchProviderSummary: { zh: '结果摘要', en: 'Result Summary' },
|
|
191
|
+
searchProviderFreshness: { zh: '时间范围', en: 'Freshness' },
|
|
192
|
+
searchProviderBaseUrl: { zh: '接口地址', en: 'API Base URL' },
|
|
193
|
+
searchProviderOpenDocs: { zh: '获取博查 API', en: 'Get Bocha API' },
|
|
194
|
+
searchProviderActivate: { zh: '激活', en: 'Activate' },
|
|
195
|
+
searchProviderActivated: { zh: '已激活', en: 'Activated' },
|
|
196
|
+
searchProviderDeactivate: { zh: '取消激活', en: 'Deactivate' },
|
|
197
|
+
searchProviderBochaDescription: { zh: '更适合中国大陆用户的 AI 搜索。', en: 'AI-ready search that works better for mainland China users.' },
|
|
198
|
+
searchProviderBraveDescription: { zh: '保留 Brave 作为可选 provider。', en: 'Keep Brave as an optional provider.' },
|
|
199
|
+
searchStatusConfigured: { zh: '已配置', en: 'Configured' },
|
|
200
|
+
searchStatusNeedsSetup: { zh: '待配置', en: 'Needs Setup' },
|
|
201
|
+
searchFreshnessNoLimit: { zh: '不限', en: 'No Limit' },
|
|
202
|
+
searchFreshnessOneDay: { zh: '一天内', en: 'One Day' },
|
|
203
|
+
searchFreshnessOneWeek: { zh: '一周内', en: 'One Week' },
|
|
204
|
+
searchFreshnessOneMonth: { zh: '一个月内', en: 'One Month' },
|
|
205
|
+
searchFreshnessOneYear: { zh: '一年内', en: 'One Year' },
|
|
206
|
+
searchNoProviderSelected: { zh: '请选择左侧搜索 provider', en: 'Select a search provider from the left.' },
|
|
185
207
|
providersPageTitle: { zh: 'AI 提供商', en: 'AI Providers' },
|
|
186
208
|
providersPageDescription: { zh: '在一个页面内完成提供商切换、配置与保存。', en: 'Switch, configure, and save providers in one continuous workspace.' },
|
|
187
209
|
providersLoading: { zh: '加载中...', en: 'Loading...' },
|
|
@@ -506,6 +528,14 @@ export const LABELS: Record<string, { zh: string; en: string }> = {
|
|
|
506
528
|
chatSelectAgent: { zh: '选择 Agent', en: 'Select Agent' },
|
|
507
529
|
chatModelLabel: { zh: '对话模型', en: 'Chat Model' },
|
|
508
530
|
chatSelectModel: { zh: '选择模型', en: 'Select model' },
|
|
531
|
+
chatSessionTypeLabel: { zh: '会话类型', en: 'Session Type' },
|
|
532
|
+
chatSessionTypeNative: { zh: '原生', en: 'Native' },
|
|
533
|
+
chatSessionTypeCodex: { zh: 'Codex', en: 'Codex' },
|
|
534
|
+
chatSessionTypeClaude: { zh: 'Claude Code', en: 'Claude Code' },
|
|
535
|
+
chatSessionTypeUnavailableSuffix: {
|
|
536
|
+
zh: '当前不可用,请启用对应插件或新建 Native 会话。',
|
|
537
|
+
en: 'is unavailable now. Re-enable the plugin or create a native session.'
|
|
538
|
+
},
|
|
509
539
|
chatModelNoOptions: { zh: '暂无可用模型,请先配置提供商。', en: 'No available models. Configure a provider first.' },
|
|
510
540
|
chatGoConfigureProvider: { zh: '去配置提供商', en: 'Go to Providers' },
|
|
511
541
|
chatProviderSetupTitle: { zh: '开始前先配置提供商', en: 'Configure a Provider First' },
|
|
@@ -539,6 +569,7 @@ export const LABELS: Record<string, { zh: string; en: string }> = {
|
|
|
539
569
|
chatQueueSend: { zh: '排队发送', en: 'Queue' },
|
|
540
570
|
chatQueuedHintPrefix: { zh: '当前有', en: 'Queued' },
|
|
541
571
|
chatQueuedHintSuffix: { zh: '条消息待发送。', en: 'pending messages.' },
|
|
572
|
+
chatQueueMoveFirst: { zh: '置顶到下一条', en: 'Move to Next' },
|
|
542
573
|
chatDeleteSession: { zh: '删除会话', en: 'Delete Session' },
|
|
543
574
|
chatDeleteSessionConfirm: { zh: '确认删除当前会话?', en: 'Delete the current session?' },
|
|
544
575
|
chatSendFailed: { zh: '发送消息失败', en: 'Failed to send message' },
|
package/tsconfig.json
CHANGED
package/vite.config.ts
CHANGED
|
@@ -9,7 +9,8 @@ export default defineConfig({
|
|
|
9
9
|
plugins: [react(), splitVendorChunkPlugin()],
|
|
10
10
|
resolve: {
|
|
11
11
|
alias: {
|
|
12
|
-
'@': path.resolve(__dirname, './src')
|
|
12
|
+
'@': path.resolve(__dirname, './src'),
|
|
13
|
+
'@nextclaw/agent-chat': path.resolve(__dirname, '../nextclaw-agent-chat/src/index.ts')
|
|
13
14
|
}
|
|
14
15
|
},
|
|
15
16
|
server: {
|