@nextclaw/ui 0.5.21 → 0.5.23
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-AKnD2r1L.js +1 -0
- package/dist/assets/ChatPage-DidO_pAN.js +32 -0
- package/dist/assets/{CronConfig-9dYfTRJl.js → CronConfig-C1pm-oKA.js} +1 -1
- package/dist/assets/{DocBrowser-BIV0vpA0.js → DocBrowser-BY90Lf6L.js} +1 -1
- package/dist/assets/{MarketplacePage-2Zi0JSVi.js → MarketplacePage-BRtmhP3G.js} +1 -1
- package/dist/assets/{ModelConfig-h21P5rV0.js → ModelConfig-Dga1Ko7_.js} +1 -1
- package/dist/assets/{ProvidersList-DEaK1a3y.js → ProvidersList-DvCoBTrT.js} +1 -1
- package/dist/assets/{RuntimeConfig-DXMzf-gF.js → RuntimeConfig-2aqBJ6Xn.js} +1 -1
- package/dist/assets/SecretsConfig-wlnh__z0.js +3 -0
- package/dist/assets/{SessionsConfig-SdXvn_9E.js → SessionsConfig-CN2WymbH.js} +2 -2
- package/dist/assets/{action-link-C9xMkxl2.js → action-link-DLZDwUfD.js} +1 -1
- package/dist/assets/{card-Cnqfntk5.js → card-D3dD-I5t.js} +1 -1
- package/dist/assets/chat-message-DZV2Z5oc.js +5 -0
- package/dist/assets/{dialog-DJs630RE.js → dialog-DZ0VC-RD.js} +1 -1
- package/dist/assets/index-BsDasSXm.css +1 -0
- package/dist/assets/index-D_vv0E-O.js +2 -0
- package/dist/assets/{label-CXGuE6Oa.js → label-CJIvvG6o.js} +1 -1
- package/dist/assets/{page-layout-BVZlyPFt.js → page-layout-CLgr0qym.js} +1 -1
- package/dist/assets/{switch-BLF45eI3.js → switch-C-0Q8OH2.js} +1 -1
- package/dist/assets/{tabs-custom-DQ0GpEV5.js → tabs-custom-D4Gs3BGM.js} +1 -1
- package/dist/assets/useConfig-R5uGhZtD.js +1 -0
- package/dist/assets/{useConfirmDialog-CK7KAyDf.js → useConfirmDialog-AMeSTA83.js} +1 -1
- package/dist/assets/{vendor-RXIbhDBC.js → vendor-H2M3a_4Z.js} +1 -1
- package/dist/index.html +3 -3
- package/package.json +1 -1
- package/src/App.tsx +2 -0
- package/src/api/config.ts +16 -0
- package/src/api/types.ts +52 -0
- package/src/components/chat/ChatPage.tsx +67 -9
- package/src/components/chat/ChatThread.tsx +12 -3
- package/src/components/config/ChannelForm.tsx +9 -0
- package/src/components/config/SecretsConfig.tsx +469 -0
- package/src/components/layout/Sidebar.tsx +6 -1
- package/src/hooks/useConfig.ts +17 -0
- package/src/lib/chat-message.ts +80 -2
- package/src/lib/i18n.ts +42 -0
- package/dist/assets/ChannelsList-TFFw4Cem.js +0 -1
- package/dist/assets/ChatPage-BUm3UPap.js +0 -32
- package/dist/assets/chat-message-B7oqvJ2d.js +0 -3
- package/dist/assets/index-CrUDzcei.js +0 -2
- package/dist/assets/index-Zy7fAOe1.css +0 -1
- package/dist/assets/useConfig-vFQvF4kn.js +0 -1
package/src/api/types.ts
CHANGED
|
@@ -175,6 +175,57 @@ export type RuntimeConfigUpdate = {
|
|
|
175
175
|
session?: SessionConfigView;
|
|
176
176
|
};
|
|
177
177
|
|
|
178
|
+
export type SecretSourceView = "env" | "file" | "exec";
|
|
179
|
+
|
|
180
|
+
export type SecretRefView = {
|
|
181
|
+
source: SecretSourceView;
|
|
182
|
+
provider?: string;
|
|
183
|
+
id: string;
|
|
184
|
+
};
|
|
185
|
+
|
|
186
|
+
export type SecretProviderEnvView = {
|
|
187
|
+
source: "env";
|
|
188
|
+
prefix?: string;
|
|
189
|
+
};
|
|
190
|
+
|
|
191
|
+
export type SecretProviderFileView = {
|
|
192
|
+
source: "file";
|
|
193
|
+
path: string;
|
|
194
|
+
format?: "json";
|
|
195
|
+
};
|
|
196
|
+
|
|
197
|
+
export type SecretProviderExecView = {
|
|
198
|
+
source: "exec";
|
|
199
|
+
command: string;
|
|
200
|
+
args?: string[];
|
|
201
|
+
cwd?: string;
|
|
202
|
+
timeoutMs?: number;
|
|
203
|
+
};
|
|
204
|
+
|
|
205
|
+
export type SecretProviderView = SecretProviderEnvView | SecretProviderFileView | SecretProviderExecView;
|
|
206
|
+
|
|
207
|
+
export type SecretsView = {
|
|
208
|
+
enabled: boolean;
|
|
209
|
+
defaults: {
|
|
210
|
+
env?: string;
|
|
211
|
+
file?: string;
|
|
212
|
+
exec?: string;
|
|
213
|
+
};
|
|
214
|
+
providers: Record<string, SecretProviderView>;
|
|
215
|
+
refs: Record<string, SecretRefView>;
|
|
216
|
+
};
|
|
217
|
+
|
|
218
|
+
export type SecretsConfigUpdate = {
|
|
219
|
+
enabled?: boolean;
|
|
220
|
+
defaults?: {
|
|
221
|
+
env?: string | null;
|
|
222
|
+
file?: string | null;
|
|
223
|
+
exec?: string | null;
|
|
224
|
+
};
|
|
225
|
+
providers?: Record<string, SecretProviderView> | null;
|
|
226
|
+
refs?: Record<string, SecretRefView> | null;
|
|
227
|
+
};
|
|
228
|
+
|
|
178
229
|
export type ChannelConfigUpdate = Record<string, unknown>;
|
|
179
230
|
|
|
180
231
|
export type ConfigView = {
|
|
@@ -207,6 +258,7 @@ export type ConfigView = {
|
|
|
207
258
|
session?: SessionConfigView;
|
|
208
259
|
tools?: Record<string, unknown>;
|
|
209
260
|
gateway?: Record<string, unknown>;
|
|
261
|
+
secrets?: SecretsView;
|
|
210
262
|
};
|
|
211
263
|
|
|
212
264
|
export type ProviderSpecView = {
|
|
@@ -12,6 +12,21 @@ import { formatDateTime, t } from '@/lib/i18n';
|
|
|
12
12
|
import { MessageSquareText, Plus, RefreshCw, Search, Send, Trash2 } from 'lucide-react';
|
|
13
13
|
|
|
14
14
|
const CHAT_SESSION_STORAGE_KEY = 'nextclaw.ui.chat.activeSession';
|
|
15
|
+
const STREAM_FRAME_MS = 18;
|
|
16
|
+
|
|
17
|
+
function streamChunkSize(remaining: number): number {
|
|
18
|
+
if (remaining > 2400) return 120;
|
|
19
|
+
if (remaining > 1200) return 72;
|
|
20
|
+
if (remaining > 600) return 40;
|
|
21
|
+
if (remaining > 220) return 20;
|
|
22
|
+
return 8;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function delay(ms: number): Promise<void> {
|
|
26
|
+
return new Promise((resolve) => {
|
|
27
|
+
window.setTimeout(resolve, ms);
|
|
28
|
+
});
|
|
29
|
+
}
|
|
15
30
|
|
|
16
31
|
function readStoredSessionKey(): string | null {
|
|
17
32
|
if (typeof window === 'undefined') {
|
|
@@ -68,9 +83,11 @@ export function ChatPage() {
|
|
|
68
83
|
const [selectedSessionKey, setSelectedSessionKey] = useState<string | null>(() => readStoredSessionKey());
|
|
69
84
|
const [selectedAgentId, setSelectedAgentId] = useState('main');
|
|
70
85
|
const [optimisticUserMessage, setOptimisticUserMessage] = useState<SessionMessageView | null>(null);
|
|
86
|
+
const [streamingAssistantMessage, setStreamingAssistantMessage] = useState<SessionMessageView | null>(null);
|
|
71
87
|
|
|
72
88
|
const { confirm, ConfirmDialog } = useConfirmDialog();
|
|
73
89
|
const threadRef = useRef<HTMLDivElement | null>(null);
|
|
90
|
+
const streamRunIdRef = useRef(0);
|
|
74
91
|
|
|
75
92
|
const configQuery = useConfig();
|
|
76
93
|
const sessionsQuery = useSessions({ q: query.trim() || undefined, limit: 120, activeMinutes: 0 });
|
|
@@ -96,12 +113,20 @@ export function ChatPage() {
|
|
|
96
113
|
);
|
|
97
114
|
|
|
98
115
|
const historyMessages = useMemo(() => historyQuery.data?.messages ?? [], [historyQuery.data?.messages]);
|
|
116
|
+
const isGenerating = sendChatTurn.isPending || Boolean(streamingAssistantMessage);
|
|
99
117
|
const mergedMessages = useMemo(() => {
|
|
100
|
-
if (!optimisticUserMessage) {
|
|
118
|
+
if (!optimisticUserMessage && !streamingAssistantMessage) {
|
|
101
119
|
return historyMessages;
|
|
102
120
|
}
|
|
103
|
-
|
|
104
|
-
|
|
121
|
+
const next = [...historyMessages];
|
|
122
|
+
if (optimisticUserMessage) {
|
|
123
|
+
next.push(optimisticUserMessage);
|
|
124
|
+
}
|
|
125
|
+
if (streamingAssistantMessage) {
|
|
126
|
+
next.push(streamingAssistantMessage);
|
|
127
|
+
}
|
|
128
|
+
return next;
|
|
129
|
+
}, [historyMessages, optimisticUserMessage, streamingAssistantMessage]);
|
|
105
130
|
|
|
106
131
|
useEffect(() => {
|
|
107
132
|
if (!selectedSessionKey && sessions.length > 0) {
|
|
@@ -129,9 +154,17 @@ export function ChatPage() {
|
|
|
129
154
|
return;
|
|
130
155
|
}
|
|
131
156
|
element.scrollTop = element.scrollHeight;
|
|
132
|
-
}, [mergedMessages
|
|
157
|
+
}, [mergedMessages, sendChatTurn.isPending, selectedSessionKey]);
|
|
158
|
+
|
|
159
|
+
useEffect(() => {
|
|
160
|
+
return () => {
|
|
161
|
+
streamRunIdRef.current += 1;
|
|
162
|
+
};
|
|
163
|
+
}, []);
|
|
133
164
|
|
|
134
165
|
const createNewSession = () => {
|
|
166
|
+
streamRunIdRef.current += 1;
|
|
167
|
+
setStreamingAssistantMessage(null);
|
|
135
168
|
const next = buildNewSessionKey(selectedAgentId);
|
|
136
169
|
setSelectedSessionKey(next);
|
|
137
170
|
setOptimisticUserMessage(null);
|
|
@@ -153,6 +186,8 @@ export function ChatPage() {
|
|
|
153
186
|
{ key: selectedSessionKey },
|
|
154
187
|
{
|
|
155
188
|
onSuccess: async () => {
|
|
189
|
+
streamRunIdRef.current += 1;
|
|
190
|
+
setStreamingAssistantMessage(null);
|
|
156
191
|
setSelectedSessionKey(null);
|
|
157
192
|
setOptimisticUserMessage(null);
|
|
158
193
|
await sessionsQuery.refetch();
|
|
@@ -163,10 +198,12 @@ export function ChatPage() {
|
|
|
163
198
|
|
|
164
199
|
const handleSend = async () => {
|
|
165
200
|
const message = draft.trim();
|
|
166
|
-
if (!message ||
|
|
201
|
+
if (!message || isGenerating) {
|
|
167
202
|
return;
|
|
168
203
|
}
|
|
169
204
|
|
|
205
|
+
streamRunIdRef.current += 1;
|
|
206
|
+
setStreamingAssistantMessage(null);
|
|
170
207
|
const hadActiveSession = Boolean(selectedSessionKey);
|
|
171
208
|
const sessionKey = selectedSessionKey ?? buildNewSessionKey(selectedAgentId);
|
|
172
209
|
if (!selectedSessionKey) {
|
|
@@ -193,11 +230,32 @@ export function ChatPage() {
|
|
|
193
230
|
if (result.sessionKey !== sessionKey) {
|
|
194
231
|
setSelectedSessionKey(result.sessionKey);
|
|
195
232
|
}
|
|
233
|
+
const replyText = typeof result.reply === 'string' ? result.reply : '';
|
|
234
|
+
let previewRunId: number | null = null;
|
|
235
|
+
if (replyText.trim()) {
|
|
236
|
+
previewRunId = ++streamRunIdRef.current;
|
|
237
|
+
const timestamp = new Date().toISOString();
|
|
238
|
+
let cursor = 0;
|
|
239
|
+
while (cursor < replyText.length && previewRunId === streamRunIdRef.current) {
|
|
240
|
+
cursor = Math.min(replyText.length, cursor + streamChunkSize(replyText.length - cursor));
|
|
241
|
+
setStreamingAssistantMessage({
|
|
242
|
+
role: 'assistant',
|
|
243
|
+
content: replyText.slice(0, cursor),
|
|
244
|
+
timestamp
|
|
245
|
+
});
|
|
246
|
+
await delay(STREAM_FRAME_MS);
|
|
247
|
+
}
|
|
248
|
+
}
|
|
196
249
|
await sessionsQuery.refetch();
|
|
197
250
|
if (hadActiveSession) {
|
|
198
251
|
await historyQuery.refetch();
|
|
199
252
|
}
|
|
253
|
+
if (previewRunId && previewRunId === streamRunIdRef.current) {
|
|
254
|
+
setStreamingAssistantMessage(null);
|
|
255
|
+
}
|
|
200
256
|
} catch {
|
|
257
|
+
streamRunIdRef.current += 1;
|
|
258
|
+
setStreamingAssistantMessage(null);
|
|
201
259
|
setOptimisticUserMessage(null);
|
|
202
260
|
setDraft(message);
|
|
203
261
|
}
|
|
@@ -335,7 +393,7 @@ export function ChatPage() {
|
|
|
335
393
|
{mergedMessages.length === 0 ? (
|
|
336
394
|
<div className="text-sm text-gray-500">{t('chatNoMessages')}</div>
|
|
337
395
|
) : (
|
|
338
|
-
<ChatThread messages={mergedMessages} isSending={sendChatTurn.isPending} />
|
|
396
|
+
<ChatThread messages={mergedMessages} isSending={sendChatTurn.isPending && !streamingAssistantMessage} />
|
|
339
397
|
)}
|
|
340
398
|
</>
|
|
341
399
|
)}
|
|
@@ -354,7 +412,7 @@ export function ChatPage() {
|
|
|
354
412
|
}}
|
|
355
413
|
placeholder={t('chatInputPlaceholder')}
|
|
356
414
|
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={
|
|
415
|
+
disabled={isGenerating}
|
|
358
416
|
/>
|
|
359
417
|
<div className="flex items-center justify-between px-2 pb-1">
|
|
360
418
|
<div className="text-[11px] text-gray-400">{t('chatInputHint')}</div>
|
|
@@ -362,10 +420,10 @@ export function ChatPage() {
|
|
|
362
420
|
size="sm"
|
|
363
421
|
className="rounded-lg"
|
|
364
422
|
onClick={() => void handleSend()}
|
|
365
|
-
disabled={
|
|
423
|
+
disabled={isGenerating || draft.trim().length === 0}
|
|
366
424
|
>
|
|
367
425
|
<Send className="h-3.5 w-3.5 mr-1.5" />
|
|
368
|
-
{
|
|
426
|
+
{isGenerating ? t('chatSending') : t('chatSend')}
|
|
369
427
|
</Button>
|
|
370
428
|
</div>
|
|
371
429
|
</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)}>
|
|
@@ -256,6 +256,15 @@ export function ChannelForm() {
|
|
|
256
256
|
if (!channelName) return;
|
|
257
257
|
|
|
258
258
|
const payload: Record<string, unknown> = { ...formData };
|
|
259
|
+
for (const field of fields) {
|
|
260
|
+
if (field.type !== 'password') {
|
|
261
|
+
continue;
|
|
262
|
+
}
|
|
263
|
+
const value = payload[field.name];
|
|
264
|
+
if (typeof value !== 'string' || value.length === 0) {
|
|
265
|
+
delete payload[field.name];
|
|
266
|
+
}
|
|
267
|
+
}
|
|
259
268
|
for (const field of fields) {
|
|
260
269
|
if (field.type !== 'json') {
|
|
261
270
|
continue;
|