devnonla-ui 0.1.0
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/LICENSE +21 -0
- package/README.md +140 -0
- package/package.json +66 -0
- package/src/alert/Alert.tsx +109 -0
- package/src/app/App.tsx +36 -0
- package/src/button/Button.tsx +278 -0
- package/src/button/ButtonCopy.tsx +60 -0
- package/src/calendar/Calendar.tsx +133 -0
- package/src/chat/AgentPanel.tsx +203 -0
- package/src/chat/common/bgTasks.ts +41 -0
- package/src/chat/common/sse.ts +156 -0
- package/src/chat/common/types.ts +43 -0
- package/src/chat/common/useAgentStream.ts +351 -0
- package/src/chat/common/utils.ts +73 -0
- package/src/chat/index.ts +56 -0
- package/src/chat/message-ui/ChatAgentMessage.tsx +29 -0
- package/src/chat/message-ui/ChatError.tsx +24 -0
- package/src/chat/message-ui/ChatInput.tsx +162 -0
- package/src/chat/message-ui/ChatMarkdown.tsx +128 -0
- package/src/chat/message-ui/ChatMarkdownTable.tsx +45 -0
- package/src/chat/message-ui/ChatSpinner.tsx +5 -0
- package/src/chat/message-ui/ChatThinking.tsx +61 -0
- package/src/chat/message-ui/ChatUserMessage.tsx +72 -0
- package/src/chat/message-ui/ChatWelcome.tsx +70 -0
- package/src/chat/message-ui/MermaidBlock.tsx +266 -0
- package/src/chat/message-ui/mermaidFence.ts +55 -0
- package/src/chat/message-ui/sanitizeMermaid.ts +135 -0
- package/src/chat/tool-ui/BackgroundTaskToolUI.tsx +36 -0
- package/src/chat/tool-ui/BackgroundTasksBar.tsx +152 -0
- package/src/chat/tool-ui/CallAgentToolUI.tsx +168 -0
- package/src/chat/tool-ui/ChatToolCall.tsx +77 -0
- package/src/chat/tool-ui/GetCurrentTimeToolUI.tsx +49 -0
- package/src/chat/tool-ui/ReadSkillToolUI.tsx +80 -0
- package/src/chat/tool-ui/RunJsToolUI.tsx +106 -0
- package/src/chat/tool-ui/ToolUiBadge.tsx +10 -0
- package/src/chat/tool-ui/ToolUiTrailing.tsx +24 -0
- package/src/chat/tool-ui/WebFetchToolUI.tsx +53 -0
- package/src/chat/tool-ui/registry.ts +29 -0
- package/src/chat/tool-ui/types.ts +22 -0
- package/src/checkbox/Checkbox.tsx +50 -0
- package/src/codeblock/CodeBlock.tsx +76 -0
- package/src/codeblock/highlight.ts +159 -0
- package/src/colorpicker/ColorPicker.tsx +45 -0
- package/src/datepicker/DatePicker.tsx +481 -0
- package/src/desktop/DesktopHeader.tsx +92 -0
- package/src/desktop/DesktopIcon.tsx +34 -0
- package/src/desktop/DesktopStage.tsx +6 -0
- package/src/desktop/DesktopWindow.tsx +504 -0
- package/src/desktop/MeadowDesktop.tsx +15 -0
- package/src/desktop/MeadowShell.tsx +12 -0
- package/src/desktop/MeadowWallpaper.tsx +5 -0
- package/src/desktop/assets/desktop-meadow.jpg +0 -0
- package/src/desktop/assets.d.ts +9 -0
- package/src/desktop/surface.ts +1 -0
- package/src/drawer/Drawer.tsx +125 -0
- package/src/dropdown/ContextMenu.tsx +97 -0
- package/src/dropdown/Dropdown.tsx +139 -0
- package/src/dropdown/menuClasses.ts +9 -0
- package/src/empty/Empty.tsx +47 -0
- package/src/form/Form.tsx +95 -0
- package/src/form/FormItem.tsx +145 -0
- package/src/form/common/context.ts +13 -0
- package/src/form/common/enum.ts +20 -0
- package/src/form/common/rules.ts +68 -0
- package/src/form/common/types.ts +169 -0
- package/src/form/index.ts +35 -0
- package/src/form/partials/FormItemHelps.tsx +31 -0
- package/src/form/variants/FieldCheckbox.tsx +20 -0
- package/src/form/variants/FieldColor.tsx +63 -0
- package/src/form/variants/FieldDateTime.tsx +25 -0
- package/src/form/variants/FieldHidden.tsx +9 -0
- package/src/form/variants/FieldInput.tsx +24 -0
- package/src/form/variants/FieldNumber.tsx +28 -0
- package/src/form/variants/FieldObject.tsx +29 -0
- package/src/form/variants/FieldRadio.tsx +40 -0
- package/src/form/variants/FieldRepeater.tsx +62 -0
- package/src/form/variants/FieldRepeaterItem.tsx +64 -0
- package/src/form/variants/FieldSelect.tsx +27 -0
- package/src/form/variants/FieldSelectMultiple.tsx +32 -0
- package/src/form/variants/FieldSelectRemote.tsx +66 -0
- package/src/form/variants/FieldSwitch.tsx +17 -0
- package/src/form/variants/FieldTextarea.tsx +25 -0
- package/src/form/variants/FieldTime.tsx +25 -0
- package/src/form-layout/FormLayout.tsx +74 -0
- package/src/icon/FluentIcon.tsx +28 -0
- package/src/icon/fluent.ts +75 -0
- package/src/index.ts +214 -0
- package/src/input/Input.tsx +371 -0
- package/src/input/SearchInput.tsx +73 -0
- package/src/lib/cn.ts +6 -0
- package/src/lib/placement.ts +44 -0
- package/src/lib/sizes.ts +117 -0
- package/src/lib/surface.ts +8 -0
- package/src/menu/Menu.tsx +98 -0
- package/src/message/message.tsx +214 -0
- package/src/modal/Modal.tsx +277 -0
- package/src/pagination/Pagination.tsx +103 -0
- package/src/popconfirm/Popconfirm.tsx +134 -0
- package/src/popover/Popover.tsx +116 -0
- package/src/scroll/OverlayScroll.tsx +141 -0
- package/src/segmented/Segmented.tsx +67 -0
- package/src/select/Select.tsx +319 -0
- package/src/skeleton/Skeleton.tsx +47 -0
- package/src/spin/Spin.tsx +160 -0
- package/src/styles.css +897 -0
- package/src/switch/Switch.tsx +58 -0
- package/src/table/Table.tsx +387 -0
- package/src/tag/Tag.tsx +109 -0
- package/src/theme.ts +131 -0
- package/src/timepicker/TimePicker.tsx +751 -0
- package/src/tooltip/Tooltip.tsx +59 -0
|
@@ -0,0 +1,351 @@
|
|
|
1
|
+
import { useCallback, useEffect, useRef, useState } from "react";
|
|
2
|
+
import { parseSseStream } from "./sse";
|
|
3
|
+
import type { AgentHistoryMessage, AgentMessage, AgentPanelEndpoint, AgentToolAction } from "./types";
|
|
4
|
+
import { formatToolName } from "./utils";
|
|
5
|
+
|
|
6
|
+
let _id = 0;
|
|
7
|
+
function nextId(prefix: string) {
|
|
8
|
+
return `${prefix}-${Date.now()}-${++_id}`;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
function thinkingDurationSec(startedAt: number): number {
|
|
12
|
+
if (!startedAt) return 0;
|
|
13
|
+
return Math.round((Date.now() - startedAt) / 1000);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function buildAgentHistory(messages: AgentMessage[]): AgentHistoryMessage[] {
|
|
17
|
+
return messages
|
|
18
|
+
.filter((m) => {
|
|
19
|
+
if (m.role === "user") return m.content.trim() !== "";
|
|
20
|
+
if (m.role === "assistant") return m.content.trim() !== "";
|
|
21
|
+
if (m.role === "tool-call") return m.toolOutput != null;
|
|
22
|
+
return false;
|
|
23
|
+
})
|
|
24
|
+
.map((m) => {
|
|
25
|
+
if (m.role === "tool-call") {
|
|
26
|
+
return {
|
|
27
|
+
role: "tool-call" as const,
|
|
28
|
+
content: "" as const,
|
|
29
|
+
toolCallId: m.toolCallId,
|
|
30
|
+
toolName: m.toolName,
|
|
31
|
+
toolInput: m.toolInput,
|
|
32
|
+
toolOutput: m.toolOutput,
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
return { role: m.role as "user" | "assistant", content: m.content };
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function finalizeStreaming(prev: AgentMessage[]): AgentMessage[] {
|
|
40
|
+
return prev
|
|
41
|
+
.map((m) => {
|
|
42
|
+
if (m.role === "assistant" && m.streaming) {
|
|
43
|
+
if (!m.content.trim() && m.meta?.thinking) {
|
|
44
|
+
return {
|
|
45
|
+
...m,
|
|
46
|
+
role: "thinking" as const,
|
|
47
|
+
content: String(m.meta.thinking),
|
|
48
|
+
streaming: false,
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
return { ...m, streaming: false };
|
|
52
|
+
}
|
|
53
|
+
return m;
|
|
54
|
+
})
|
|
55
|
+
.filter((m) => !(m.role === "assistant" && !m.content.trim() && !m.meta?.thinking));
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function failOpenTools(prev: AgentMessage[], error: string): AgentMessage[] {
|
|
59
|
+
return prev.map((m) => {
|
|
60
|
+
if (m.role !== "tool-call" || m.toolOutput != null || m.toolError) return m;
|
|
61
|
+
return { ...m, toolError: error, toolOutput: JSON.stringify({ success: false, error }) };
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export type UseAgentStreamOptions = {
|
|
66
|
+
endpoint: AgentPanelEndpoint;
|
|
67
|
+
extraBody?: Record<string, unknown> | (() => Record<string, unknown>);
|
|
68
|
+
headers?: Record<string, string>;
|
|
69
|
+
fetcher?: typeof fetch;
|
|
70
|
+
initialMessages?: AgentMessage[];
|
|
71
|
+
onToolAction?: (event: AgentToolAction) => void;
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
export function useAgentStream({ endpoint, extraBody, headers, fetcher = fetch, initialMessages, onToolAction }: UseAgentStreamOptions) {
|
|
75
|
+
const [messages, setMessages] = useState<AgentMessage[]>(() => initialMessages ?? []);
|
|
76
|
+
const [generating, setGenerating] = useState(false);
|
|
77
|
+
const thinkingRef = useRef("");
|
|
78
|
+
const thinkingStartRef = useRef(0);
|
|
79
|
+
const abortRef = useRef<AbortController | null>(null);
|
|
80
|
+
const onToolActionRef = useRef(onToolAction);
|
|
81
|
+
onToolActionRef.current = onToolAction;
|
|
82
|
+
const extraBodyRef = useRef(extraBody);
|
|
83
|
+
extraBodyRef.current = extraBody;
|
|
84
|
+
const endpointRef = useRef(endpoint);
|
|
85
|
+
endpointRef.current = endpoint;
|
|
86
|
+
|
|
87
|
+
useEffect(() => {
|
|
88
|
+
return () => {
|
|
89
|
+
abortRef.current?.abort();
|
|
90
|
+
abortRef.current = null;
|
|
91
|
+
};
|
|
92
|
+
}, []);
|
|
93
|
+
|
|
94
|
+
const cancel = useCallback(() => {
|
|
95
|
+
abortRef.current?.abort();
|
|
96
|
+
abortRef.current = null;
|
|
97
|
+
setMessages((prev) => finalizeStreaming(failOpenTools(prev, "Cancelled")));
|
|
98
|
+
setGenerating(false);
|
|
99
|
+
thinkingRef.current = "";
|
|
100
|
+
thinkingStartRef.current = 0;
|
|
101
|
+
}, []);
|
|
102
|
+
|
|
103
|
+
const clear = useCallback(() => {
|
|
104
|
+
abortRef.current?.abort();
|
|
105
|
+
abortRef.current = null;
|
|
106
|
+
setMessages([]);
|
|
107
|
+
setGenerating(false);
|
|
108
|
+
thinkingRef.current = "";
|
|
109
|
+
thinkingStartRef.current = 0;
|
|
110
|
+
}, []);
|
|
111
|
+
|
|
112
|
+
const send = useCallback(
|
|
113
|
+
async (text: string) => {
|
|
114
|
+
const trimmed = text.trim();
|
|
115
|
+
if (!trimmed || generating) return;
|
|
116
|
+
|
|
117
|
+
abortRef.current?.abort();
|
|
118
|
+
const controller = new AbortController();
|
|
119
|
+
abortRef.current = controller;
|
|
120
|
+
|
|
121
|
+
const userMsg: AgentMessage = { id: nextId("u"), role: "user", content: trimmed, timestamp: new Date() };
|
|
122
|
+
const assistantId = nextId("a");
|
|
123
|
+
const assistantMsg: AgentMessage = { id: assistantId, role: "assistant", content: "", streaming: true, timestamp: new Date() };
|
|
124
|
+
const historySnapshot = messages;
|
|
125
|
+
|
|
126
|
+
setMessages((prev) => [...prev, userMsg, assistantMsg]);
|
|
127
|
+
setGenerating(true);
|
|
128
|
+
thinkingRef.current = "";
|
|
129
|
+
thinkingStartRef.current = 0;
|
|
130
|
+
|
|
131
|
+
const history = buildAgentHistory(historySnapshot);
|
|
132
|
+
let assistantText = "";
|
|
133
|
+
let currentId = assistantId;
|
|
134
|
+
let needsNewBubble = false;
|
|
135
|
+
|
|
136
|
+
const freezeOpen = () => {
|
|
137
|
+
const freezeId = currentId;
|
|
138
|
+
if (freezeId) {
|
|
139
|
+
const thinkingSnapshot = thinkingRef.current;
|
|
140
|
+
const liveDuration = thinkingStartRef.current > 0 ? thinkingDurationSec(thinkingStartRef.current) : undefined;
|
|
141
|
+
if (assistantText.trim()) {
|
|
142
|
+
setMessages((prev) =>
|
|
143
|
+
prev.map((m) => {
|
|
144
|
+
if (m.id !== freezeId) return m;
|
|
145
|
+
const existing = typeof m.meta?.thinkingDuration === "number" ? m.meta.thinkingDuration : undefined;
|
|
146
|
+
return {
|
|
147
|
+
...m,
|
|
148
|
+
content: assistantText,
|
|
149
|
+
streaming: false,
|
|
150
|
+
meta: thinkingSnapshot ? { ...m.meta, thinking: thinkingSnapshot, thinkingDuration: liveDuration ?? existing ?? 0 } : m.meta,
|
|
151
|
+
};
|
|
152
|
+
}),
|
|
153
|
+
);
|
|
154
|
+
} else if (thinkingSnapshot) {
|
|
155
|
+
setMessages((prev) =>
|
|
156
|
+
prev.map((m) =>
|
|
157
|
+
m.id === freezeId
|
|
158
|
+
? {
|
|
159
|
+
id: freezeId,
|
|
160
|
+
role: "thinking" as const,
|
|
161
|
+
content: thinkingSnapshot,
|
|
162
|
+
streaming: false,
|
|
163
|
+
timestamp: m.timestamp,
|
|
164
|
+
meta: { thinking: thinkingSnapshot, thinkingDuration: liveDuration ?? 0 },
|
|
165
|
+
}
|
|
166
|
+
: m,
|
|
167
|
+
),
|
|
168
|
+
);
|
|
169
|
+
} else {
|
|
170
|
+
setMessages((prev) => prev.filter((m) => m.id !== freezeId));
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
thinkingRef.current = "";
|
|
174
|
+
thinkingStartRef.current = 0;
|
|
175
|
+
assistantText = "";
|
|
176
|
+
currentId = "";
|
|
177
|
+
needsNewBubble = true;
|
|
178
|
+
};
|
|
179
|
+
|
|
180
|
+
const ensureBubble = (seed?: { content?: string; thinking?: string }): string => {
|
|
181
|
+
if (!needsNewBubble && currentId) return currentId;
|
|
182
|
+
const newId = nextId("a");
|
|
183
|
+
currentId = newId;
|
|
184
|
+
needsNewBubble = false;
|
|
185
|
+
setMessages((prev) => [
|
|
186
|
+
...prev,
|
|
187
|
+
{
|
|
188
|
+
id: newId,
|
|
189
|
+
role: "assistant",
|
|
190
|
+
content: seed?.content ?? "",
|
|
191
|
+
streaming: true,
|
|
192
|
+
timestamp: new Date(),
|
|
193
|
+
meta: seed?.thinking ? { thinking: seed.thinking } : undefined,
|
|
194
|
+
},
|
|
195
|
+
]);
|
|
196
|
+
return newId;
|
|
197
|
+
};
|
|
198
|
+
|
|
199
|
+
const stampThinkingDuration = () => {
|
|
200
|
+
if (!thinkingRef.current || !thinkingStartRef.current) return;
|
|
201
|
+
const duration = thinkingDurationSec(thinkingStartRef.current);
|
|
202
|
+
const targetId = currentId;
|
|
203
|
+
if (targetId) {
|
|
204
|
+
setMessages((prev) => prev.map((m) => (m.id === targetId ? { ...m, meta: { ...m.meta, thinkingDuration: duration } } : m)));
|
|
205
|
+
}
|
|
206
|
+
thinkingStartRef.current = 0;
|
|
207
|
+
};
|
|
208
|
+
|
|
209
|
+
try {
|
|
210
|
+
const extra = typeof extraBodyRef.current === "function" ? extraBodyRef.current() : extraBodyRef.current;
|
|
211
|
+
const payload = [...history, { role: "user" as const, content: trimmed }];
|
|
212
|
+
const ep = endpointRef.current;
|
|
213
|
+
const response =
|
|
214
|
+
typeof ep === "function"
|
|
215
|
+
? await ep({ messages: payload, signal: controller.signal })
|
|
216
|
+
: await fetcher(ep, {
|
|
217
|
+
method: "POST",
|
|
218
|
+
credentials: "include",
|
|
219
|
+
headers: { "Content-Type": "application/json", ...headers },
|
|
220
|
+
body: JSON.stringify({ messages: payload, ...extra }),
|
|
221
|
+
signal: controller.signal,
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
if (!response.ok || !response.body) {
|
|
225
|
+
const errText = await response.text();
|
|
226
|
+
throw new Error(errText || `HTTP ${response.status}`);
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
const result = await parseSseStream(
|
|
230
|
+
response.body,
|
|
231
|
+
{
|
|
232
|
+
onTextDelta: (delta) => {
|
|
233
|
+
stampThinkingDuration();
|
|
234
|
+
if (needsNewBubble || !currentId) {
|
|
235
|
+
assistantText = delta;
|
|
236
|
+
thinkingRef.current = "";
|
|
237
|
+
thinkingStartRef.current = 0;
|
|
238
|
+
ensureBubble({ content: delta });
|
|
239
|
+
} else {
|
|
240
|
+
assistantText += delta;
|
|
241
|
+
const targetId = currentId;
|
|
242
|
+
setMessages((prev) => prev.map((m) => (m.id === targetId ? { ...m, content: assistantText, streaming: true } : m)));
|
|
243
|
+
}
|
|
244
|
+
},
|
|
245
|
+
onThinkingDelta: (delta) => {
|
|
246
|
+
if (needsNewBubble || !currentId) {
|
|
247
|
+
thinkingRef.current = delta;
|
|
248
|
+
thinkingStartRef.current = Date.now();
|
|
249
|
+
assistantText = "";
|
|
250
|
+
ensureBubble({ thinking: delta });
|
|
251
|
+
return;
|
|
252
|
+
}
|
|
253
|
+
if (!thinkingRef.current) thinkingStartRef.current = Date.now();
|
|
254
|
+
thinkingRef.current += delta;
|
|
255
|
+
const thinking = thinkingRef.current;
|
|
256
|
+
const targetId = currentId;
|
|
257
|
+
setMessages((prev) => prev.map((m) => (m.id === targetId ? { ...m, meta: { ...m.meta, thinking } } : m)));
|
|
258
|
+
},
|
|
259
|
+
onToolCall: (event) => {
|
|
260
|
+
const label = event.toolLabel ? (event.toolLabel.includes(" ") ? event.toolLabel : formatToolName(event.toolLabel)) : formatToolName(event.toolName);
|
|
261
|
+
freezeOpen();
|
|
262
|
+
setMessages((prev) => [
|
|
263
|
+
...prev,
|
|
264
|
+
{
|
|
265
|
+
id: nextId("tc"),
|
|
266
|
+
role: "tool-call",
|
|
267
|
+
content: event.toolName,
|
|
268
|
+
toolCallId: event.toolCallId,
|
|
269
|
+
toolName: event.toolName,
|
|
270
|
+
toolLabel: label,
|
|
271
|
+
toolInput: event.input,
|
|
272
|
+
timestamp: new Date(),
|
|
273
|
+
},
|
|
274
|
+
]);
|
|
275
|
+
onToolActionRef.current?.({ type: "tool-call", toolName: event.toolName, toolLabel: label, input: event.input });
|
|
276
|
+
},
|
|
277
|
+
onToolResult: (event) => {
|
|
278
|
+
const raw = event.result;
|
|
279
|
+
const resultStr = typeof raw === "string" ? raw : JSON.stringify(raw);
|
|
280
|
+
setMessages((prev) => {
|
|
281
|
+
let matchIdx = -1;
|
|
282
|
+
if (event.toolCallId) {
|
|
283
|
+
matchIdx = prev.findIndex((m) => m.role === "tool-call" && m.toolCallId === event.toolCallId && m.toolOutput == null);
|
|
284
|
+
}
|
|
285
|
+
if (matchIdx === -1) {
|
|
286
|
+
const rev = [...prev].reverse().findIndex((m) => m.role === "tool-call" && m.toolName === event.toolName && !m.toolOutput);
|
|
287
|
+
matchIdx = rev === -1 ? -1 : prev.length - 1 - rev;
|
|
288
|
+
}
|
|
289
|
+
if (matchIdx === -1) return prev;
|
|
290
|
+
return prev.map((m, i) => (i === matchIdx ? { ...m, toolOutput: resultStr } : m));
|
|
291
|
+
});
|
|
292
|
+
onToolActionRef.current?.({ type: "tool-result", toolName: event.toolName, output: raw });
|
|
293
|
+
},
|
|
294
|
+
onDone: () => {
|
|
295
|
+
setMessages((prev) => finalizeStreaming(failOpenTools(prev, "Tool did not return a result")));
|
|
296
|
+
setGenerating(false);
|
|
297
|
+
thinkingRef.current = "";
|
|
298
|
+
thinkingStartRef.current = 0;
|
|
299
|
+
},
|
|
300
|
+
onError: (error) => {
|
|
301
|
+
if (error === "Connection lost") {
|
|
302
|
+
setMessages((prev) => finalizeStreaming(failOpenTools(prev, "Connection lost")));
|
|
303
|
+
setGenerating(false);
|
|
304
|
+
thinkingRef.current = "";
|
|
305
|
+
thinkingStartRef.current = 0;
|
|
306
|
+
return;
|
|
307
|
+
}
|
|
308
|
+
setMessages((prev) => [
|
|
309
|
+
...finalizeStreaming(failOpenTools(prev, error === "cancelled" ? "Cancelled" : error)),
|
|
310
|
+
...(error === "cancelled" ? [] : [{ id: nextId("err"), role: "error" as const, content: error, timestamp: new Date() }]),
|
|
311
|
+
]);
|
|
312
|
+
setGenerating(false);
|
|
313
|
+
thinkingRef.current = "";
|
|
314
|
+
thinkingStartRef.current = 0;
|
|
315
|
+
},
|
|
316
|
+
},
|
|
317
|
+
{ signal: controller.signal },
|
|
318
|
+
);
|
|
319
|
+
|
|
320
|
+
if (result === "aborted") {
|
|
321
|
+
setMessages((prev) => finalizeStreaming(failOpenTools(prev, "Cancelled")));
|
|
322
|
+
setGenerating(false);
|
|
323
|
+
thinkingRef.current = "";
|
|
324
|
+
thinkingStartRef.current = 0;
|
|
325
|
+
return;
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
setMessages((prev) => finalizeStreaming(failOpenTools(prev, "Tool did not return a result")));
|
|
329
|
+
setGenerating(false);
|
|
330
|
+
} catch (err: unknown) {
|
|
331
|
+
if ((err as Error)?.name === "AbortError") {
|
|
332
|
+
setMessages((prev) => finalizeStreaming(failOpenTools(prev, "Cancelled")));
|
|
333
|
+
setGenerating(false);
|
|
334
|
+
thinkingRef.current = "";
|
|
335
|
+
thinkingStartRef.current = 0;
|
|
336
|
+
return;
|
|
337
|
+
}
|
|
338
|
+
setMessages((prev) => [
|
|
339
|
+
...finalizeStreaming(failOpenTools(prev, err instanceof Error ? err.message : String(err))),
|
|
340
|
+
{ id: nextId("err"), role: "error", content: err instanceof Error ? err.message : String(err), timestamp: new Date() },
|
|
341
|
+
]);
|
|
342
|
+
setGenerating(false);
|
|
343
|
+
} finally {
|
|
344
|
+
abortRef.current = null;
|
|
345
|
+
}
|
|
346
|
+
},
|
|
347
|
+
[generating, messages, fetcher, headers],
|
|
348
|
+
);
|
|
349
|
+
|
|
350
|
+
return { messages, generating, send, cancel, clear };
|
|
351
|
+
}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
/** snake_case / camelCase → Title Case (e.g. edit_code → Edit Code). */
|
|
2
|
+
export function formatToolName(name: string): string {
|
|
3
|
+
return name
|
|
4
|
+
.replace(/([A-Z])/g, " $1")
|
|
5
|
+
.replace(/[_-]+/g, " ")
|
|
6
|
+
.trim()
|
|
7
|
+
.replace(/\b\w/g, (c) => c.toUpperCase());
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export function prettyJson(raw: unknown): string {
|
|
11
|
+
if (typeof raw === "string") {
|
|
12
|
+
try {
|
|
13
|
+
return JSON.stringify(JSON.parse(raw), null, 2);
|
|
14
|
+
} catch {
|
|
15
|
+
return raw;
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
try {
|
|
19
|
+
return JSON.stringify(raw, null, 2);
|
|
20
|
+
} catch {
|
|
21
|
+
return String(raw);
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function hasMeaningfulInput(input: unknown): boolean {
|
|
26
|
+
if (input == null) return false;
|
|
27
|
+
if (typeof input === "string") {
|
|
28
|
+
const trimmed = input.trim();
|
|
29
|
+
return trimmed.length > 0 && trimmed !== "{}" && trimmed !== "[]";
|
|
30
|
+
}
|
|
31
|
+
if (Array.isArray(input)) return input.length > 0;
|
|
32
|
+
if (typeof input === "object") return Object.keys(input as object).length > 0;
|
|
33
|
+
return true;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function parseJsonObject<T extends object>(raw: unknown): T | null {
|
|
37
|
+
if (raw == null) return null;
|
|
38
|
+
if (typeof raw === "object") return raw as T;
|
|
39
|
+
if (typeof raw !== "string") return null;
|
|
40
|
+
try {
|
|
41
|
+
const parsed = JSON.parse(raw) as unknown;
|
|
42
|
+
if (parsed && typeof parsed === "object") return parsed as T;
|
|
43
|
+
} catch {
|
|
44
|
+
/* ignore */
|
|
45
|
+
}
|
|
46
|
+
return null;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const CALL_AGENT_PREFIX = "call_agent__";
|
|
50
|
+
|
|
51
|
+
/** True for legacy `call_agent` and per-target `call_agent__<uuid>` tools. */
|
|
52
|
+
export function isCallAgentToolName(name: string | null | undefined): boolean {
|
|
53
|
+
if (!name) return false;
|
|
54
|
+
return name === "call_agent" || name.startsWith(CALL_AGENT_PREFIX);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/** Reconstruct target agent UUID from `call_agent__<uuid_with_underscores>`. */
|
|
58
|
+
export function parseCallAgentToolTargetId(toolName: string): string | null {
|
|
59
|
+
if (!toolName.startsWith(CALL_AGENT_PREFIX)) return null;
|
|
60
|
+
const parts = toolName.slice(CALL_AGENT_PREFIX.length).split("_");
|
|
61
|
+
if (parts.length !== 5) return null;
|
|
62
|
+
return parts.join("-");
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export function timestampMs(value: unknown): number {
|
|
66
|
+
if (value instanceof Date) return value.getTime();
|
|
67
|
+
if (typeof value === "number" && Number.isFinite(value)) return value;
|
|
68
|
+
if (typeof value === "string") {
|
|
69
|
+
const parsed = Date.parse(value);
|
|
70
|
+
if (!Number.isNaN(parsed)) return parsed;
|
|
71
|
+
}
|
|
72
|
+
return Date.now();
|
|
73
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
export { ChatThinking } from "./message-ui/ChatThinking";
|
|
2
|
+
export type { ChatThinkingProps } from "./message-ui/ChatThinking";
|
|
3
|
+
|
|
4
|
+
export { ChatUserMessage } from "./message-ui/ChatUserMessage";
|
|
5
|
+
export type { ChatUserMessageProps } from "./message-ui/ChatUserMessage";
|
|
6
|
+
|
|
7
|
+
export { ChatAgentMessage } from "./message-ui/ChatAgentMessage";
|
|
8
|
+
export type { ChatAgentMessageProps } from "./message-ui/ChatAgentMessage";
|
|
9
|
+
|
|
10
|
+
export { ChatError } from "./message-ui/ChatError";
|
|
11
|
+
export type { ChatErrorProps } from "./message-ui/ChatError";
|
|
12
|
+
|
|
13
|
+
export { ChatToolCall } from "./tool-ui/ChatToolCall";
|
|
14
|
+
export type { ChatToolCallProps } from "./tool-ui/ChatToolCall";
|
|
15
|
+
|
|
16
|
+
export { resolveToolUI } from "./tool-ui/registry";
|
|
17
|
+
export type { ToolUIProps, ChatToolMessage } from "./tool-ui/types";
|
|
18
|
+
export { CallAgentToolUI } from "./tool-ui/CallAgentToolUI";
|
|
19
|
+
export { WebFetchToolUI } from "./tool-ui/WebFetchToolUI";
|
|
20
|
+
export { GetCurrentTimeToolUI } from "./tool-ui/GetCurrentTimeToolUI";
|
|
21
|
+
export { ReadSkillToolUI } from "./tool-ui/ReadSkillToolUI";
|
|
22
|
+
export { RunJsToolUI } from "./tool-ui/RunJsToolUI";
|
|
23
|
+
export { BackgroundTaskToolUI } from "./tool-ui/BackgroundTaskToolUI";
|
|
24
|
+
export { BackgroundTasksBar } from "./tool-ui/BackgroundTasksBar";
|
|
25
|
+
export { ToolUiTrailing } from "./tool-ui/ToolUiTrailing";
|
|
26
|
+
export type { BackgroundTasksBarProps } from "./tool-ui/BackgroundTasksBar";
|
|
27
|
+
|
|
28
|
+
export { ChatInput } from "./message-ui/ChatInput";
|
|
29
|
+
export type { ChatInputProps } from "./message-ui/ChatInput";
|
|
30
|
+
|
|
31
|
+
export { ChatWelcome } from "./message-ui/ChatWelcome";
|
|
32
|
+
export type { ChatWelcomeProps } from "./message-ui/ChatWelcome";
|
|
33
|
+
|
|
34
|
+
export { ChatSpinner } from "./message-ui/ChatSpinner";
|
|
35
|
+
|
|
36
|
+
export { ChatMarkdown, createChatMarkdownComponents, chatMarkdownComponents, chatMarkdownClass, chatBodyClass } from "./message-ui/ChatMarkdown";
|
|
37
|
+
export type { ChatMarkdownProps, ChatMarkdownStreamState } from "./message-ui/ChatMarkdown";
|
|
38
|
+
|
|
39
|
+
export { MermaidBlock } from "./message-ui/MermaidBlock";
|
|
40
|
+
export type { MermaidBlockProps } from "./message-ui/MermaidBlock";
|
|
41
|
+
|
|
42
|
+
export { AgentPanel } from "./AgentPanel";
|
|
43
|
+
export type { AgentPanelProps } from "./AgentPanel";
|
|
44
|
+
|
|
45
|
+
export { useAgentStream, buildAgentHistory } from "./common/useAgentStream";
|
|
46
|
+
export type { UseAgentStreamOptions } from "./common/useAgentStream";
|
|
47
|
+
|
|
48
|
+
export { parseSseStream, normalizeSseEvent } from "./common/sse";
|
|
49
|
+
export type { AgentSseEvent, AgentSseCallbacks, ParseSseResult } from "./common/sse";
|
|
50
|
+
|
|
51
|
+
export type { AgentMessage, AgentMessageRole, AgentHistoryMessage, AgentStreamRequest, AgentPanelEndpoint, AgentToolAction } from "./common/types";
|
|
52
|
+
|
|
53
|
+
export { formatToolName, prettyJson, isCallAgentToolName, parseCallAgentToolTargetId } from "./common/utils";
|
|
54
|
+
|
|
55
|
+
export { parseBgTaskRef, formatBgElapsed } from "./common/bgTasks";
|
|
56
|
+
export type { ChatBgTask, ChatBgTaskStatus } from "./common/bgTasks";
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import type { ReactNode } from "react";
|
|
2
|
+
import { cn } from "../../lib/cn";
|
|
3
|
+
import { ChatThinking } from "./ChatThinking";
|
|
4
|
+
|
|
5
|
+
export type ChatAgentMessageProps = {
|
|
6
|
+
/** Plain text fallback when `children` is omitted. */
|
|
7
|
+
content?: string;
|
|
8
|
+
/** Prefer passing rendered markdown / rich content as children. */
|
|
9
|
+
children?: ReactNode;
|
|
10
|
+
thinking?: string;
|
|
11
|
+
thinkingDuration?: number;
|
|
12
|
+
thinkingStreaming?: boolean;
|
|
13
|
+
className?: string;
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
export function ChatAgentMessage({ content, children, thinking, thinkingDuration, thinkingStreaming, className }: ChatAgentMessageProps) {
|
|
17
|
+
const body = children ?? (content ? <p className="m-0 whitespace-pre-wrap wrap-break-word">{content}</p> : null);
|
|
18
|
+
|
|
19
|
+
return (
|
|
20
|
+
<div className={cn("nonla-chat-agent mt-1", className)}>
|
|
21
|
+
{thinking ? <ChatThinking thinking={thinking} duration={thinkingDuration ?? 0} streaming={thinkingStreaming} /> : null}
|
|
22
|
+
{body ? (
|
|
23
|
+
<div className="min-w-0 px-4 pb-0.5">
|
|
24
|
+
<div className="nonla-chat-body text-(length:--chat-body-size) leading-(--chat-body-leading) text-[var(--nonla-ink)]">{body}</div>
|
|
25
|
+
</div>
|
|
26
|
+
) : null}
|
|
27
|
+
</div>
|
|
28
|
+
);
|
|
29
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import type { ReactNode } from "react";
|
|
2
|
+
import { cn } from "../../lib/cn";
|
|
3
|
+
|
|
4
|
+
export type ChatErrorProps = {
|
|
5
|
+
children: ReactNode;
|
|
6
|
+
className?: string;
|
|
7
|
+
};
|
|
8
|
+
|
|
9
|
+
export function ChatError({ children, className }: ChatErrorProps) {
|
|
10
|
+
return (
|
|
11
|
+
<div className={cn("nonla-chat-error px-4 py-1", className)}>
|
|
12
|
+
<div
|
|
13
|
+
role="alert"
|
|
14
|
+
className="flex items-center gap-2 rounded-lg border border-destructive/40 bg-[color-mix(in_oklab,var(--destructive)_14%,var(--nonla-surface))] px-3 py-2 text-(length:--chat-body-size) leading-(--chat-body-leading) text-destructive"
|
|
15
|
+
>
|
|
16
|
+
<svg className="size-4 shrink-0" viewBox="0 0 16 16" fill="none" aria-hidden>
|
|
17
|
+
<circle cx="8" cy="8" r="5.75" stroke="currentColor" strokeWidth="1.4" />
|
|
18
|
+
<path d="M6 6l4 4M10 6l-4 4" stroke="currentColor" strokeWidth="1.4" strokeLinecap="round" />
|
|
19
|
+
</svg>
|
|
20
|
+
<div className="min-w-0">{children}</div>
|
|
21
|
+
</div>
|
|
22
|
+
</div>
|
|
23
|
+
);
|
|
24
|
+
}
|