@nextclaw/ui 0.5.20 → 0.5.21
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 +6 -0
- package/dist/assets/ChannelsList-TFFw4Cem.js +1 -0
- package/dist/assets/ChatPage-BUm3UPap.js +32 -0
- package/dist/assets/CronConfig-9dYfTRJl.js +1 -0
- package/dist/assets/{DocBrowser-C35MebbI.js → DocBrowser-BIV0vpA0.js} +1 -1
- package/dist/assets/{MarketplacePage-HjEQ8sFt.js → MarketplacePage-2Zi0JSVi.js} +1 -1
- package/dist/assets/{ModelConfig-BpBoi1sz.js → ModelConfig-h21P5rV0.js} +1 -1
- package/dist/assets/ProvidersList-DEaK1a3y.js +1 -0
- package/dist/assets/RuntimeConfig-DXMzf-gF.js +1 -0
- package/dist/assets/SessionsConfig-SdXvn_9E.js +2 -0
- package/dist/assets/{action-link-CSScZ_id.js → action-link-C9xMkxl2.js} +1 -1
- package/dist/assets/{card-Cj58-DCd.js → card-Cnqfntk5.js} +1 -1
- package/dist/assets/chat-message-B7oqvJ2d.js +3 -0
- package/dist/assets/{dialog-Ce8jNftN.js → dialog-DJs630RE.js} +2 -2
- package/dist/assets/index-CrUDzcei.js +2 -0
- package/dist/assets/index-Zy7fAOe1.css +1 -0
- package/dist/assets/{label-CQdP2NhF.js → label-CXGuE6Oa.js} +1 -1
- package/dist/assets/{page-layout-Byyxptub.js → page-layout-BVZlyPFt.js} +1 -1
- package/dist/assets/{switch-ChJzdp0x.js → switch-BLF45eI3.js} +1 -1
- package/dist/assets/{tabs-custom-DWlAbbCy.js → tabs-custom-DQ0GpEV5.js} +1 -1
- package/dist/assets/{useConfig-8lC_4LwH.js → useConfig-vFQvF4kn.js} +1 -1
- package/dist/assets/{useConfirmDialog-B7iWHb5k.js → useConfirmDialog-CK7KAyDf.js} +1 -1
- package/dist/assets/{vendor-Dz2q6Qmc.js → vendor-RXIbhDBC.js} +87 -62
- package/dist/index.html +3 -3
- package/package.json +4 -1
- package/src/api/types.ts +3 -1
- package/src/components/chat/ChatPage.tsx +4 -39
- package/src/components/chat/ChatThread.tsx +210 -0
- package/src/components/config/SessionsConfig.tsx +3 -1
- package/src/index.css +69 -0
- package/src/lib/chat-message.ts +215 -0
- package/src/lib/i18n.ts +10 -0
- package/dist/assets/ChannelsList-DqgRRdUH.js +0 -1
- package/dist/assets/ChatPage-BQyomkth.js +0 -1
- package/dist/assets/CronConfig-Bmg449JI.js +0 -1
- package/dist/assets/ProvidersList-0tYTV40v.js +0 -1
- package/dist/assets/RuntimeConfig-B_WI-DHf.js +0 -1
- package/dist/assets/SessionsConfig-BEt-f6WS.js +0 -2
- package/dist/assets/index-CPFSdkyQ.css +0 -1
- package/dist/assets/index-C_z1Na9N.js +0 -2
package/dist/index.html
CHANGED
|
@@ -6,9 +6,9 @@
|
|
|
6
6
|
<link rel="icon" type="image/svg+xml" href="/logo.svg" />
|
|
7
7
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
8
8
|
<title>NextClaw - 系统配置</title>
|
|
9
|
-
<script type="module" crossorigin src="/assets/index-
|
|
10
|
-
<link rel="modulepreload" crossorigin href="/assets/vendor-
|
|
11
|
-
<link rel="stylesheet" crossorigin href="/assets/index-
|
|
9
|
+
<script type="module" crossorigin src="/assets/index-CrUDzcei.js"></script>
|
|
10
|
+
<link rel="modulepreload" crossorigin href="/assets/vendor-RXIbhDBC.js">
|
|
11
|
+
<link rel="stylesheet" crossorigin href="/assets/index-Zy7fAOe1.css">
|
|
12
12
|
</head>
|
|
13
13
|
|
|
14
14
|
<body>
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nextclaw/ui",
|
|
3
|
-
"version": "0.5.
|
|
3
|
+
"version": "0.5.21",
|
|
4
4
|
"private": false,
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -19,7 +19,10 @@
|
|
|
19
19
|
"react": "^18.3.1",
|
|
20
20
|
"react-dom": "^18.3.1",
|
|
21
21
|
"react-hook-form": "^7.53.2",
|
|
22
|
+
"react-markdown": "^10.1.0",
|
|
22
23
|
"react-router-dom": "^7.13.0",
|
|
24
|
+
"rehype-sanitize": "^6.0.0",
|
|
25
|
+
"remark-gfm": "^4.0.1",
|
|
23
26
|
"sonner": "^1.7.1",
|
|
24
27
|
"tailwind-merge": "^2.5.4",
|
|
25
28
|
"zod": "^3.23.8",
|
package/src/api/types.ts
CHANGED
|
@@ -73,10 +73,12 @@ export type SessionsListView = {
|
|
|
73
73
|
|
|
74
74
|
export type SessionMessageView = {
|
|
75
75
|
role: string;
|
|
76
|
-
content:
|
|
76
|
+
content: unknown;
|
|
77
77
|
timestamp: string;
|
|
78
78
|
name?: string;
|
|
79
79
|
tool_call_id?: string;
|
|
80
|
+
tool_calls?: Array<Record<string, unknown>>;
|
|
81
|
+
reasoning_content?: string;
|
|
80
82
|
};
|
|
81
83
|
|
|
82
84
|
export type SessionHistoryView = {
|
|
@@ -6,9 +6,10 @@ import { Button } from '@/components/ui/button';
|
|
|
6
6
|
import { Input } from '@/components/ui/input';
|
|
7
7
|
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
|
|
8
8
|
import { PageHeader, PageLayout } from '@/components/layout/page-layout';
|
|
9
|
+
import { ChatThread } from '@/components/chat/ChatThread';
|
|
9
10
|
import { cn } from '@/lib/utils';
|
|
10
11
|
import { formatDateTime, t } from '@/lib/i18n';
|
|
11
|
-
import {
|
|
12
|
+
import { MessageSquareText, Plus, RefreshCw, Search, Send, Trash2 } from 'lucide-react';
|
|
12
13
|
|
|
13
14
|
const CHAT_SESSION_STORAGE_KEY = 'nextclaw.ui.chat.activeSession';
|
|
14
15
|
|
|
@@ -61,30 +62,6 @@ function sessionDisplayName(session: SessionEntryView): string {
|
|
|
61
62
|
return chunks[chunks.length - 1] || session.key;
|
|
62
63
|
}
|
|
63
64
|
|
|
64
|
-
function MessageBubble({ message }: { message: SessionMessageView }) {
|
|
65
|
-
const role = message.role.toLowerCase();
|
|
66
|
-
const isUser = role === 'user';
|
|
67
|
-
return (
|
|
68
|
-
<div className={cn('flex w-full', isUser ? 'justify-end' : 'justify-start')}>
|
|
69
|
-
<div
|
|
70
|
-
className={cn(
|
|
71
|
-
'max-w-[88%] rounded-2xl px-4 py-3 shadow-sm border',
|
|
72
|
-
isUser
|
|
73
|
-
? 'bg-primary text-white border-primary rounded-br-md'
|
|
74
|
-
: 'bg-white text-gray-800 border-gray-200 rounded-bl-md'
|
|
75
|
-
)}
|
|
76
|
-
>
|
|
77
|
-
<div className="mb-1 flex items-center gap-2 text-[11px] opacity-80">
|
|
78
|
-
{isUser ? <User className="h-3.5 w-3.5" /> : <Bot className="h-3.5 w-3.5" />}
|
|
79
|
-
<span className="font-semibold">{message.role}</span>
|
|
80
|
-
<span>{formatDateTime(message.timestamp)}</span>
|
|
81
|
-
</div>
|
|
82
|
-
<div className="whitespace-pre-wrap break-words text-sm leading-relaxed">{message.content}</div>
|
|
83
|
-
</div>
|
|
84
|
-
</div>
|
|
85
|
-
);
|
|
86
|
-
}
|
|
87
|
-
|
|
88
65
|
export function ChatPage() {
|
|
89
66
|
const [query, setQuery] = useState('');
|
|
90
67
|
const [draft, setDraft] = useState('');
|
|
@@ -342,7 +319,7 @@ export function ChatPage() {
|
|
|
342
319
|
</Button>
|
|
343
320
|
</div>
|
|
344
321
|
|
|
345
|
-
<div ref={threadRef} className="flex-1 min-h-0 overflow-y-auto custom-scrollbar px-5 py-5
|
|
322
|
+
<div ref={threadRef} className="flex-1 min-h-0 overflow-y-auto custom-scrollbar px-5 py-5">
|
|
346
323
|
{!selectedSessionKey ? (
|
|
347
324
|
<div className="h-full flex items-center justify-center">
|
|
348
325
|
<div className="text-center text-gray-500">
|
|
@@ -358,19 +335,7 @@ export function ChatPage() {
|
|
|
358
335
|
{mergedMessages.length === 0 ? (
|
|
359
336
|
<div className="text-sm text-gray-500">{t('chatNoMessages')}</div>
|
|
360
337
|
) : (
|
|
361
|
-
mergedMessages.
|
|
362
|
-
<MessageBubble
|
|
363
|
-
key={`${message.timestamp}-${message.role}-${index}`}
|
|
364
|
-
message={message}
|
|
365
|
-
/>
|
|
366
|
-
))
|
|
367
|
-
)}
|
|
368
|
-
{sendChatTurn.isPending && (
|
|
369
|
-
<div className="flex justify-start">
|
|
370
|
-
<div className="rounded-2xl rounded-bl-md border border-gray-200 bg-white px-4 py-3 text-sm text-gray-500">
|
|
371
|
-
{t('chatTyping')}
|
|
372
|
-
</div>
|
|
373
|
-
</div>
|
|
338
|
+
<ChatThread messages={mergedMessages} isSending={sendChatTurn.isPending} />
|
|
374
339
|
)}
|
|
375
340
|
</>
|
|
376
341
|
)}
|
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
import { useMemo } from 'react';
|
|
2
|
+
import type { SessionMessageView } from '@/api/types';
|
|
3
|
+
import { cn } from '@/lib/utils';
|
|
4
|
+
import { extractMessageText, extractToolCards, groupChatMessages, type ChatRole, type ToolCard } from '@/lib/chat-message';
|
|
5
|
+
import { formatDateTime, t } from '@/lib/i18n';
|
|
6
|
+
import ReactMarkdown from 'react-markdown';
|
|
7
|
+
import rehypeSanitize from 'rehype-sanitize';
|
|
8
|
+
import remarkGfm from 'remark-gfm';
|
|
9
|
+
import { Bot, Clock3, FileSearch, Globe, Search, SendHorizontal, Terminal, User, Wrench } from 'lucide-react';
|
|
10
|
+
|
|
11
|
+
type ChatThreadProps = {
|
|
12
|
+
messages: SessionMessageView[];
|
|
13
|
+
isSending: boolean;
|
|
14
|
+
className?: string;
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
const MARKDOWN_MAX_CHARS = 140_000;
|
|
18
|
+
const TOOL_OUTPUT_PREVIEW_MAX = 220;
|
|
19
|
+
|
|
20
|
+
function trimMarkdown(value: string): string {
|
|
21
|
+
if (value.length <= MARKDOWN_MAX_CHARS) {
|
|
22
|
+
return value;
|
|
23
|
+
}
|
|
24
|
+
return `${value.slice(0, MARKDOWN_MAX_CHARS)}\n\n…`;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function roleTitle(role: ChatRole): string {
|
|
28
|
+
if (role === 'user') return t('chatRoleUser');
|
|
29
|
+
if (role === 'assistant') return t('chatRoleAssistant');
|
|
30
|
+
if (role === 'tool') return t('chatRoleTool');
|
|
31
|
+
if (role === 'system') return t('chatRoleSystem');
|
|
32
|
+
return t('chatRoleMessage');
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function renderToolIcon(name: string) {
|
|
36
|
+
const lowered = name.toLowerCase();
|
|
37
|
+
if (lowered.includes('exec') || lowered.includes('shell') || lowered.includes('command')) {
|
|
38
|
+
return <Terminal className="h-3.5 w-3.5" />;
|
|
39
|
+
}
|
|
40
|
+
if (lowered.includes('search')) {
|
|
41
|
+
return <Search className="h-3.5 w-3.5" />;
|
|
42
|
+
}
|
|
43
|
+
if (lowered.includes('fetch') || lowered.includes('http') || lowered.includes('web')) {
|
|
44
|
+
return <Globe className="h-3.5 w-3.5" />;
|
|
45
|
+
}
|
|
46
|
+
if (lowered.includes('read') || lowered.includes('file')) {
|
|
47
|
+
return <FileSearch className="h-3.5 w-3.5" />;
|
|
48
|
+
}
|
|
49
|
+
if (lowered.includes('message') || lowered.includes('send')) {
|
|
50
|
+
return <SendHorizontal className="h-3.5 w-3.5" />;
|
|
51
|
+
}
|
|
52
|
+
if (lowered.includes('cron') || lowered.includes('schedule')) {
|
|
53
|
+
return <Clock3 className="h-3.5 w-3.5" />;
|
|
54
|
+
}
|
|
55
|
+
return <Wrench className="h-3.5 w-3.5" />;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function RoleAvatar({ role }: { role: ChatRole }) {
|
|
59
|
+
if (role === 'user') {
|
|
60
|
+
return (
|
|
61
|
+
<div className="h-8 w-8 rounded-full bg-primary text-white flex items-center justify-center shadow-sm">
|
|
62
|
+
<User className="h-4 w-4" />
|
|
63
|
+
</div>
|
|
64
|
+
);
|
|
65
|
+
}
|
|
66
|
+
if (role === 'assistant') {
|
|
67
|
+
return (
|
|
68
|
+
<div className="h-8 w-8 rounded-full bg-slate-900 text-white flex items-center justify-center shadow-sm">
|
|
69
|
+
<Bot className="h-4 w-4" />
|
|
70
|
+
</div>
|
|
71
|
+
);
|
|
72
|
+
}
|
|
73
|
+
return (
|
|
74
|
+
<div className="h-8 w-8 rounded-full bg-amber-100 text-amber-700 flex items-center justify-center shadow-sm">
|
|
75
|
+
<Wrench className="h-4 w-4" />
|
|
76
|
+
</div>
|
|
77
|
+
);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function MarkdownBlock({ text, role }: { text: string; role: ChatRole }) {
|
|
81
|
+
const isUser = role === 'user';
|
|
82
|
+
return (
|
|
83
|
+
<div className={cn('chat-markdown', isUser ? 'chat-markdown-user' : 'chat-markdown-assistant')}>
|
|
84
|
+
<ReactMarkdown
|
|
85
|
+
remarkPlugins={[remarkGfm]}
|
|
86
|
+
rehypePlugins={[rehypeSanitize]}
|
|
87
|
+
components={{
|
|
88
|
+
a: ({ ...props }) => (
|
|
89
|
+
<a {...props} target="_blank" rel="noreferrer noopener" />
|
|
90
|
+
)
|
|
91
|
+
}}
|
|
92
|
+
>
|
|
93
|
+
{trimMarkdown(text)}
|
|
94
|
+
</ReactMarkdown>
|
|
95
|
+
</div>
|
|
96
|
+
);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function ToolCardView({ card }: { card: ToolCard }) {
|
|
100
|
+
const title = card.kind === 'call' ? t('chatToolCall') : t('chatToolResult');
|
|
101
|
+
const output = card.text?.trim() ?? '';
|
|
102
|
+
const showDetails = output.length > TOOL_OUTPUT_PREVIEW_MAX || output.includes('\n');
|
|
103
|
+
const preview = showDetails ? `${output.slice(0, TOOL_OUTPUT_PREVIEW_MAX)}…` : output;
|
|
104
|
+
|
|
105
|
+
return (
|
|
106
|
+
<div className="rounded-xl border border-amber-200/80 bg-amber-50/60 px-3 py-2.5">
|
|
107
|
+
<div className="flex items-center gap-2 text-xs text-amber-800 font-semibold">
|
|
108
|
+
{renderToolIcon(card.name)}
|
|
109
|
+
<span>{title}</span>
|
|
110
|
+
<span className="font-mono text-[11px] text-amber-900/80">{card.name}</span>
|
|
111
|
+
</div>
|
|
112
|
+
{card.detail && (
|
|
113
|
+
<div className="mt-1 text-[11px] text-amber-800/90 font-mono break-words">{card.detail}</div>
|
|
114
|
+
)}
|
|
115
|
+
{card.kind === 'result' && (
|
|
116
|
+
<div className="mt-2">
|
|
117
|
+
{!output ? (
|
|
118
|
+
<div className="text-[11px] text-amber-700/80">{t('chatToolNoOutput')}</div>
|
|
119
|
+
) : showDetails ? (
|
|
120
|
+
<details className="group">
|
|
121
|
+
<summary className="cursor-pointer text-[11px] text-amber-700">{t('chatToolOutput')}</summary>
|
|
122
|
+
<pre className="mt-2 rounded-lg border border-amber-200 bg-amber-100/40 p-2 text-[11px] whitespace-pre-wrap break-words text-amber-900">
|
|
123
|
+
{output}
|
|
124
|
+
</pre>
|
|
125
|
+
</details>
|
|
126
|
+
) : (
|
|
127
|
+
<pre className="rounded-lg border border-amber-200 bg-amber-100/40 p-2 text-[11px] whitespace-pre-wrap break-words text-amber-900">
|
|
128
|
+
{preview}
|
|
129
|
+
</pre>
|
|
130
|
+
)}
|
|
131
|
+
</div>
|
|
132
|
+
)}
|
|
133
|
+
</div>
|
|
134
|
+
);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
function MessageCard({ message, role }: { message: SessionMessageView; role: ChatRole }) {
|
|
138
|
+
const text = extractMessageText(message.content).trim();
|
|
139
|
+
const toolCards = extractToolCards(message);
|
|
140
|
+
const reasoning = typeof message.reasoning_content === 'string' ? message.reasoning_content.trim() : '';
|
|
141
|
+
const shouldRenderText = Boolean(text) && !(role === 'tool' && toolCards.length > 0);
|
|
142
|
+
const isUser = role === 'user';
|
|
143
|
+
|
|
144
|
+
return (
|
|
145
|
+
<div
|
|
146
|
+
className={cn(
|
|
147
|
+
'rounded-2xl border px-4 py-3 shadow-sm',
|
|
148
|
+
isUser
|
|
149
|
+
? 'bg-primary text-white border-primary'
|
|
150
|
+
: role === 'assistant'
|
|
151
|
+
? 'bg-white text-gray-900 border-gray-200'
|
|
152
|
+
: 'bg-orange-50/70 text-gray-900 border-orange-200/80'
|
|
153
|
+
)}
|
|
154
|
+
>
|
|
155
|
+
{shouldRenderText && <MarkdownBlock text={text} role={role} />}
|
|
156
|
+
{reasoning && (
|
|
157
|
+
<details className="mt-3">
|
|
158
|
+
<summary className={cn('cursor-pointer text-xs', isUser ? 'text-primary-100' : 'text-gray-500')}>
|
|
159
|
+
{t('chatReasoning')}
|
|
160
|
+
</summary>
|
|
161
|
+
<pre className={cn('mt-2 text-[11px] whitespace-pre-wrap break-words rounded-lg p-2', isUser ? 'bg-primary-700/60' : 'bg-gray-100')}>
|
|
162
|
+
{reasoning}
|
|
163
|
+
</pre>
|
|
164
|
+
</details>
|
|
165
|
+
)}
|
|
166
|
+
{toolCards.length > 0 && (
|
|
167
|
+
<div className="mt-3 space-y-2">
|
|
168
|
+
{toolCards.map((card, index) => (
|
|
169
|
+
<ToolCardView key={`${card.kind}-${card.name}-${card.callId ?? index}`} card={card} />
|
|
170
|
+
))}
|
|
171
|
+
</div>
|
|
172
|
+
)}
|
|
173
|
+
</div>
|
|
174
|
+
);
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
export function ChatThread({ messages, isSending, className }: ChatThreadProps) {
|
|
178
|
+
const groups = useMemo(() => groupChatMessages(messages), [messages]);
|
|
179
|
+
|
|
180
|
+
return (
|
|
181
|
+
<div className={cn('space-y-5', className)}>
|
|
182
|
+
{groups.map((group) => {
|
|
183
|
+
const isUser = group.role === 'user';
|
|
184
|
+
return (
|
|
185
|
+
<div key={group.key} className={cn('flex gap-3', isUser ? 'justify-end' : 'justify-start')}>
|
|
186
|
+
{!isUser && <RoleAvatar role={group.role} />}
|
|
187
|
+
<div className={cn('max-w-[88%] min-w-[280px] space-y-2', isUser && 'flex flex-col items-end')}>
|
|
188
|
+
{group.messages.map((message, index) => (
|
|
189
|
+
<MessageCard key={`${group.key}-${index}`} message={message} role={group.role} />
|
|
190
|
+
))}
|
|
191
|
+
<div className={cn('text-[11px] px-1', isUser ? 'text-primary-300' : 'text-gray-400')}>
|
|
192
|
+
{roleTitle(group.role)} · {formatDateTime(group.timestamp)}
|
|
193
|
+
</div>
|
|
194
|
+
</div>
|
|
195
|
+
{isUser && <RoleAvatar role={group.role} />}
|
|
196
|
+
</div>
|
|
197
|
+
);
|
|
198
|
+
})}
|
|
199
|
+
|
|
200
|
+
{isSending && (
|
|
201
|
+
<div className="flex gap-3 justify-start">
|
|
202
|
+
<RoleAvatar role="assistant" />
|
|
203
|
+
<div className="rounded-2xl border border-gray-200 bg-white px-4 py-3 text-sm text-gray-500 shadow-sm">
|
|
204
|
+
{t('chatTyping')}
|
|
205
|
+
</div>
|
|
206
|
+
</div>
|
|
207
|
+
)}
|
|
208
|
+
</div>
|
|
209
|
+
);
|
|
210
|
+
}
|
|
@@ -7,6 +7,7 @@ import { Input } from '@/components/ui/input';
|
|
|
7
7
|
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
|
|
8
8
|
import { cn } from '@/lib/utils';
|
|
9
9
|
import { formatDateShort, formatDateTime, t } from '@/lib/i18n';
|
|
10
|
+
import { extractMessageText } from '@/lib/chat-message';
|
|
10
11
|
import { PageLayout, PageHeader } from '@/components/layout/page-layout';
|
|
11
12
|
import { RefreshCw, Search, Clock, Inbox, Hash, Bot, User, MessageCircle, Settings as SettingsIcon } from 'lucide-react';
|
|
12
13
|
|
|
@@ -86,6 +87,7 @@ function SessionListItem({ session, channel, isSelected, onSelect }: SessionList
|
|
|
86
87
|
|
|
87
88
|
function SessionMessageBubble({ message }: { message: SessionMessageView }) {
|
|
88
89
|
const isUser = message.role.toLowerCase() === 'user';
|
|
90
|
+
const content = extractMessageText(message.content).trim();
|
|
89
91
|
|
|
90
92
|
return (
|
|
91
93
|
<div className={cn("flex w-full mb-6", isUser ? "justify-end" : "justify-start")}>
|
|
@@ -108,7 +110,7 @@ function SessionMessageBubble({ message }: { message: SessionMessageView }) {
|
|
|
108
110
|
</span>
|
|
109
111
|
</div>
|
|
110
112
|
<div className="whitespace-pre-wrap break-words leading-relaxed text-[15px]">
|
|
111
|
-
{
|
|
113
|
+
{content || '-'}
|
|
112
114
|
</div>
|
|
113
115
|
</div>
|
|
114
116
|
</div>
|
package/src/index.css
CHANGED
|
@@ -182,3 +182,72 @@
|
|
|
182
182
|
.animate-pulse-soft {
|
|
183
183
|
animation: pulse-soft 3s ease-in-out infinite;
|
|
184
184
|
}
|
|
185
|
+
|
|
186
|
+
.chat-markdown {
|
|
187
|
+
font-size: 0.9rem;
|
|
188
|
+
line-height: 1.6;
|
|
189
|
+
word-break: break-word;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
.chat-markdown > * + * {
|
|
193
|
+
margin-top: 0.5rem;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
.chat-markdown h1,
|
|
197
|
+
.chat-markdown h2,
|
|
198
|
+
.chat-markdown h3 {
|
|
199
|
+
font-size: 1rem;
|
|
200
|
+
font-weight: 700;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
.chat-markdown ul,
|
|
204
|
+
.chat-markdown ol {
|
|
205
|
+
padding-left: 1.1rem;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
.chat-markdown code {
|
|
209
|
+
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
|
|
210
|
+
font-size: 0.8rem;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
.chat-markdown :not(pre) > code {
|
|
214
|
+
padding: 0.1rem 0.3rem;
|
|
215
|
+
border-radius: 0.35rem;
|
|
216
|
+
background: rgba(148, 163, 184, 0.18);
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
.chat-markdown pre {
|
|
220
|
+
overflow-x: auto;
|
|
221
|
+
border-radius: 0.65rem;
|
|
222
|
+
padding: 0.65rem 0.75rem;
|
|
223
|
+
background: rgba(15, 23, 42, 0.9);
|
|
224
|
+
color: #e2e8f0;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
.chat-markdown table {
|
|
228
|
+
width: 100%;
|
|
229
|
+
border-collapse: collapse;
|
|
230
|
+
font-size: 0.8rem;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
.chat-markdown th,
|
|
234
|
+
.chat-markdown td {
|
|
235
|
+
border: 1px solid rgba(148, 163, 184, 0.3);
|
|
236
|
+
padding: 0.35rem 0.45rem;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
.chat-markdown blockquote {
|
|
240
|
+
border-left: 3px solid rgba(148, 163, 184, 0.55);
|
|
241
|
+
padding-left: 0.65rem;
|
|
242
|
+
color: rgba(30, 41, 59, 0.85);
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
.chat-markdown-user a {
|
|
246
|
+
color: #dbeafe;
|
|
247
|
+
text-decoration: underline;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
.chat-markdown-assistant a {
|
|
251
|
+
color: hsl(var(--primary));
|
|
252
|
+
text-decoration: underline;
|
|
253
|
+
}
|
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
import type { SessionMessageView } from '@/api/types';
|
|
2
|
+
|
|
3
|
+
export type ChatRole = 'user' | 'assistant' | 'tool' | 'system' | 'other';
|
|
4
|
+
|
|
5
|
+
export type ToolCard = {
|
|
6
|
+
kind: 'call' | 'result';
|
|
7
|
+
name: string;
|
|
8
|
+
detail?: string;
|
|
9
|
+
text?: string;
|
|
10
|
+
callId?: string;
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
export type GroupedChatMessage = {
|
|
14
|
+
key: string;
|
|
15
|
+
role: ChatRole;
|
|
16
|
+
messages: SessionMessageView[];
|
|
17
|
+
timestamp: string;
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
const MERGE_WINDOW_MS = 2 * 60 * 1000;
|
|
21
|
+
const TOOL_DETAIL_FIELDS = ['cmd', 'command', 'query', 'q', 'path', 'url', 'to', 'channel', 'agentId', 'sessionKey'];
|
|
22
|
+
|
|
23
|
+
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
24
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function truncateText(value: string, maxChars = 2400): string {
|
|
28
|
+
if (value.length <= maxChars) {
|
|
29
|
+
return value;
|
|
30
|
+
}
|
|
31
|
+
return `${value.slice(0, maxChars)}\n…`;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function stringifyUnknown(value: unknown): string {
|
|
35
|
+
if (typeof value === 'string') {
|
|
36
|
+
return value;
|
|
37
|
+
}
|
|
38
|
+
if (typeof value === 'number' || typeof value === 'boolean') {
|
|
39
|
+
return String(value);
|
|
40
|
+
}
|
|
41
|
+
if (value == null) {
|
|
42
|
+
return '';
|
|
43
|
+
}
|
|
44
|
+
try {
|
|
45
|
+
return truncateText(JSON.stringify(value, null, 2));
|
|
46
|
+
} catch {
|
|
47
|
+
return String(value);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function parseArgsObject(value: unknown): Record<string, unknown> | null {
|
|
52
|
+
if (isRecord(value)) {
|
|
53
|
+
return value;
|
|
54
|
+
}
|
|
55
|
+
if (typeof value !== 'string') {
|
|
56
|
+
return null;
|
|
57
|
+
}
|
|
58
|
+
const trimmed = value.trim();
|
|
59
|
+
if (!trimmed.startsWith('{') && !trimmed.startsWith('[')) {
|
|
60
|
+
return null;
|
|
61
|
+
}
|
|
62
|
+
try {
|
|
63
|
+
const parsed = JSON.parse(trimmed) as unknown;
|
|
64
|
+
return isRecord(parsed) ? parsed : null;
|
|
65
|
+
} catch {
|
|
66
|
+
return null;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function summarizeToolArgs(args: unknown): string | undefined {
|
|
71
|
+
const parsed = parseArgsObject(args);
|
|
72
|
+
if (!parsed) {
|
|
73
|
+
const text = stringifyUnknown(args).trim();
|
|
74
|
+
return text ? truncateText(text, 120) : undefined;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const items: string[] = [];
|
|
78
|
+
for (const field of TOOL_DETAIL_FIELDS) {
|
|
79
|
+
const value = parsed[field];
|
|
80
|
+
if (typeof value === 'string' && value.trim()) {
|
|
81
|
+
items.push(`${field}: ${value.trim()}`);
|
|
82
|
+
} else if (typeof value === 'number' || typeof value === 'boolean') {
|
|
83
|
+
items.push(`${field}: ${String(value)}`);
|
|
84
|
+
}
|
|
85
|
+
if (items.length >= 2) {
|
|
86
|
+
break;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
if (items.length > 0) {
|
|
90
|
+
return items.join(' · ');
|
|
91
|
+
}
|
|
92
|
+
return truncateText(stringifyUnknown(parsed), 140);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function toToolName(value: unknown): string {
|
|
96
|
+
if (typeof value !== 'string' || !value.trim()) {
|
|
97
|
+
return 'tool';
|
|
98
|
+
}
|
|
99
|
+
return value.trim();
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
export function normalizeChatRole(message: Pick<SessionMessageView, 'role' | 'name' | 'tool_call_id' | 'tool_calls'>): ChatRole {
|
|
103
|
+
const role = message.role.toLowerCase().trim();
|
|
104
|
+
if (role === 'user') {
|
|
105
|
+
return 'user';
|
|
106
|
+
}
|
|
107
|
+
if (role === 'assistant') {
|
|
108
|
+
return 'assistant';
|
|
109
|
+
}
|
|
110
|
+
if (role === 'system') {
|
|
111
|
+
return 'system';
|
|
112
|
+
}
|
|
113
|
+
if (role === 'tool' || role === 'tool_result' || role === 'toolresult' || role === 'function') {
|
|
114
|
+
return 'tool';
|
|
115
|
+
}
|
|
116
|
+
if (typeof message.tool_call_id === 'string' || Array.isArray(message.tool_calls) || typeof message.name === 'string') {
|
|
117
|
+
return 'tool';
|
|
118
|
+
}
|
|
119
|
+
return 'other';
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
export function extractMessageText(content: unknown): string {
|
|
123
|
+
if (typeof content === 'string') {
|
|
124
|
+
return content;
|
|
125
|
+
}
|
|
126
|
+
if (Array.isArray(content)) {
|
|
127
|
+
const parts: string[] = [];
|
|
128
|
+
for (const item of content) {
|
|
129
|
+
if (typeof item === 'string') {
|
|
130
|
+
parts.push(item);
|
|
131
|
+
continue;
|
|
132
|
+
}
|
|
133
|
+
if (!isRecord(item)) {
|
|
134
|
+
continue;
|
|
135
|
+
}
|
|
136
|
+
if (typeof item.text === 'string') {
|
|
137
|
+
parts.push(item.text);
|
|
138
|
+
continue;
|
|
139
|
+
}
|
|
140
|
+
if (typeof item.content === 'string') {
|
|
141
|
+
parts.push(item.content);
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
if (parts.length > 0) {
|
|
145
|
+
return parts.join('\n');
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
return stringifyUnknown(content);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
export function extractToolCards(message: SessionMessageView): ToolCard[] {
|
|
152
|
+
const cards: ToolCard[] = [];
|
|
153
|
+
const toolCalls = Array.isArray(message.tool_calls) ? message.tool_calls : [];
|
|
154
|
+
for (const call of toolCalls) {
|
|
155
|
+
if (!isRecord(call)) {
|
|
156
|
+
continue;
|
|
157
|
+
}
|
|
158
|
+
const fn = isRecord(call.function) ? call.function : null;
|
|
159
|
+
const name = toToolName(fn?.name ?? call.name);
|
|
160
|
+
const args = fn?.arguments ?? call.arguments;
|
|
161
|
+
cards.push({
|
|
162
|
+
kind: 'call',
|
|
163
|
+
name,
|
|
164
|
+
detail: summarizeToolArgs(args),
|
|
165
|
+
callId: typeof call.id === 'string' ? call.id : undefined
|
|
166
|
+
});
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
const role = normalizeChatRole(message);
|
|
170
|
+
if (role === 'tool' || typeof message.tool_call_id === 'string') {
|
|
171
|
+
const text = extractMessageText(message.content).trim();
|
|
172
|
+
cards.push({
|
|
173
|
+
kind: 'result',
|
|
174
|
+
name: toToolName(message.name ?? cards[0]?.name),
|
|
175
|
+
text,
|
|
176
|
+
callId: typeof message.tool_call_id === 'string' ? message.tool_call_id : undefined
|
|
177
|
+
});
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
return cards;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
export function groupChatMessages(messages: SessionMessageView[]): GroupedChatMessage[] {
|
|
184
|
+
const groups: GroupedChatMessage[] = [];
|
|
185
|
+
let lastTs = 0;
|
|
186
|
+
|
|
187
|
+
for (let index = 0; index < messages.length; index += 1) {
|
|
188
|
+
const message = messages[index];
|
|
189
|
+
const role = normalizeChatRole(message);
|
|
190
|
+
const parsedTs = Date.parse(message.timestamp);
|
|
191
|
+
const ts = Number.isFinite(parsedTs) ? parsedTs : Date.now();
|
|
192
|
+
const previous = groups[groups.length - 1];
|
|
193
|
+
const canMerge =
|
|
194
|
+
previous &&
|
|
195
|
+
previous.role === role &&
|
|
196
|
+
Math.abs(ts - lastTs) <= MERGE_WINDOW_MS;
|
|
197
|
+
|
|
198
|
+
if (canMerge) {
|
|
199
|
+
previous.messages.push(message);
|
|
200
|
+
previous.timestamp = message.timestamp;
|
|
201
|
+
lastTs = ts;
|
|
202
|
+
continue;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
groups.push({
|
|
206
|
+
key: `${message.timestamp}-${index}-${role}`,
|
|
207
|
+
role,
|
|
208
|
+
messages: [message],
|
|
209
|
+
timestamp: message.timestamp
|
|
210
|
+
});
|
|
211
|
+
lastTs = ts;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
return groups;
|
|
215
|
+
}
|
package/src/lib/i18n.ts
CHANGED
|
@@ -383,6 +383,16 @@ export const LABELS: Record<string, { zh: string; en: string }> = {
|
|
|
383
383
|
chatDeleteSession: { zh: '删除会话', en: 'Delete Session' },
|
|
384
384
|
chatDeleteSessionConfirm: { zh: '确认删除当前会话?', en: 'Delete the current session?' },
|
|
385
385
|
chatSendFailed: { zh: '发送消息失败', en: 'Failed to send message' },
|
|
386
|
+
chatRoleUser: { zh: '你', en: 'You' },
|
|
387
|
+
chatRoleAssistant: { zh: '助手', en: 'Assistant' },
|
|
388
|
+
chatRoleTool: { zh: '工具', en: 'Tool' },
|
|
389
|
+
chatRoleSystem: { zh: '系统', en: 'System' },
|
|
390
|
+
chatRoleMessage: { zh: '消息', en: 'Message' },
|
|
391
|
+
chatToolCall: { zh: '工具调用', en: 'Tool Call' },
|
|
392
|
+
chatToolResult: { zh: '工具结果', en: 'Tool Result' },
|
|
393
|
+
chatToolOutput: { zh: '查看输出', en: 'View Output' },
|
|
394
|
+
chatToolNoOutput: { zh: '无输出(执行完成)', en: 'No output (completed)' },
|
|
395
|
+
chatReasoning: { zh: '查看推理内容', en: 'Show reasoning' },
|
|
386
396
|
|
|
387
397
|
// Cron
|
|
388
398
|
cronPageTitle: { zh: '定时任务', en: 'Cron Jobs' },
|