@icen.ai/ui 0.5.0 → 0.8.1
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/AGENTS.md +262 -0
- package/README.md +52 -13
- package/dist/base.css +33 -5
- package/dist/behaviors/accordion.d.mts +7 -0
- package/dist/behaviors/accordion.mjs +37 -11
- package/dist/behaviors/ai-chat.d.mts +59 -0
- package/dist/behaviors/ai-chat.mjs +580 -0
- package/dist/behaviors/ai-composer.d.mts +137 -0
- package/dist/behaviors/ai-composer.mjs +2114 -0
- package/dist/behaviors/ai-core.d.mts +156 -0
- package/dist/behaviors/ai-core.mjs +504 -0
- package/dist/behaviors/ai-diff.d.mts +53 -0
- package/dist/behaviors/ai-diff.mjs +413 -0
- package/dist/behaviors/ai-panel.d.mts +101 -0
- package/dist/behaviors/ai-panel.mjs +1087 -0
- package/dist/behaviors/ai-provider.d.mts +198 -0
- package/dist/behaviors/ai-provider.mjs +1061 -0
- package/dist/behaviors/ai-tool.d.mts +26 -0
- package/dist/behaviors/ai-tool.mjs +2173 -0
- package/dist/behaviors/ai-tools.d.mts +84 -0
- package/dist/behaviors/ai-tools.mjs +1762 -0
- package/dist/behaviors/back-top.d.mts +20 -0
- package/dist/behaviors/back-top.mjs +64 -32
- package/dist/behaviors/carousel.d.mts +7 -0
- package/dist/behaviors/carousel.mjs +72 -27
- package/dist/behaviors/charts.d.mts +296 -0
- package/dist/behaviors/charts.mjs +1364 -440
- package/dist/behaviors/command-palette.d.mts +42 -0
- package/dist/behaviors/command-palette.mjs +167 -27
- package/dist/behaviors/context-menu.d.mts +53 -0
- package/dist/behaviors/context-menu.mjs +127 -39
- package/dist/behaviors/controls.d.mts +59 -0
- package/dist/behaviors/controls.mjs +601 -0
- package/dist/behaviors/copy.d.mts +7 -0
- package/dist/behaviors/copy.mjs +41 -15
- package/dist/behaviors/datatable.d.mts +90 -0
- package/dist/behaviors/datatable.mjs +279 -53
- package/dist/behaviors/date-picker.d.mts +4 -0
- package/dist/behaviors/date-picker.mjs +276 -69
- package/dist/behaviors/dropdown.d.mts +8 -0
- package/dist/behaviors/dropdown.mjs +171 -28
- package/dist/behaviors/events.d.mts +330 -0
- package/dist/behaviors/events.mjs +208 -0
- package/dist/behaviors/input.d.mts +4 -0
- package/dist/behaviors/input.mjs +52 -32
- package/dist/behaviors/modal.d.mts +30 -0
- package/dist/behaviors/modal.mjs +87 -16
- package/dist/behaviors/nav.d.mts +7 -0
- package/dist/behaviors/nav.mjs +57 -15
- package/dist/behaviors/notification.d.mts +188 -0
- package/dist/behaviors/notification.mjs +274 -140
- package/dist/behaviors/popover.d.mts +82 -0
- package/dist/behaviors/popover.mjs +92 -5
- package/dist/behaviors/select.d.mts +6 -0
- package/dist/behaviors/select.mjs +287 -45
- package/dist/behaviors/sidebar.d.mts +7 -0
- package/dist/behaviors/sidebar.mjs +44 -13
- package/dist/behaviors/slider.d.mts +7 -0
- package/dist/behaviors/slider.mjs +55 -43
- package/dist/behaviors/split-pane.d.mts +8 -0
- package/dist/behaviors/split-pane.mjs +70 -29
- package/dist/behaviors/tabs.d.mts +9 -0
- package/dist/behaviors/tabs.mjs +102 -26
- package/dist/behaviors/tag-input.d.mts +7 -0
- package/dist/behaviors/tag-input.mjs +66 -17
- package/dist/behaviors/theme.d.mts +36 -0
- package/dist/behaviors/theme.mjs +51 -5
- package/dist/behaviors/toast.d.mts +48 -0
- package/dist/behaviors/toast.mjs +98 -25
- package/dist/behaviors/tree.d.mts +7 -0
- package/dist/behaviors/tree.mjs +39 -15
- package/dist/behaviors/upload.d.mts +4 -0
- package/dist/behaviors/upload.mjs +79 -44
- package/dist/cli.mjs +1 -1
- package/dist/components/ai-chat.css +1037 -0
- package/dist/components/ai-chat.mjs +3 -0
- package/dist/components/ai-composer.mjs +3 -0
- package/dist/components/ai-context.mjs +2 -0
- package/dist/components/ai-diff.css +311 -0
- package/dist/components/ai-diff.mjs +3 -0
- package/dist/components/ai-files.mjs +3 -0
- package/dist/components/ai-message.mjs +3 -0
- package/dist/components/ai-panel.css +682 -0
- package/dist/components/ai-reasoning.mjs +3 -0
- package/dist/components/ai-subagent.mjs +2 -0
- package/dist/components/ai-todo.mjs +2 -0
- package/dist/components/ai-tool-call.mjs +2 -0
- package/dist/components/ai-tool.css +532 -0
- package/dist/components/ai-usage.mjs +3 -0
- package/dist/components/badge.css +5 -9
- package/dist/components/breadcrumb.css +0 -36
- package/dist/components/btn.css +4 -7
- package/dist/components/card.css +3 -6
- package/dist/components/chart-bar.css +10 -0
- package/dist/components/chart-calendar.mjs +4 -0
- package/dist/components/chart-gauge.css +1 -1
- package/dist/components/chart-heatmap.css +40 -0
- package/dist/components/chart-pie.css +0 -6
- package/dist/components/chart-scatter.css +24 -0
- package/dist/components/chart-scatter.mjs +4 -0
- package/dist/components/charts.css +87 -2
- package/dist/components/charts.mjs +1 -0
- package/dist/components/command-palette.css +2 -2
- package/dist/components/content.css +11 -25
- package/dist/components/copy.css +8 -8
- package/dist/components/datatable.css +5 -9
- package/dist/components/date-picker.css +4 -8
- package/dist/components/empty.css +0 -4
- package/dist/components/feedback.css +10 -17
- package/dist/components/form.css +13 -14
- package/dist/components/input.css +2 -2
- package/dist/components/layout.css +36 -36
- package/dist/components/media.css +12 -8
- package/dist/components/menu.css +2 -2
- package/dist/components/modal.css +1 -1
- package/dist/components/nav.css +11 -6
- package/dist/components/notification.css +27 -28
- package/dist/components/pagination.css +4 -2
- package/dist/components/pagination.mjs +1 -0
- package/dist/components/panel.css +16 -16
- package/dist/components/pill.css +15 -15
- package/dist/components/rating.mjs +1 -0
- package/dist/components/segmented.css +14 -6
- package/dist/components/segmented.mjs +1 -0
- package/dist/components/select.css +79 -24
- package/dist/components/sidebar.css +1 -1
- package/dist/components/split-pane.css +0 -1
- package/dist/components/stat.css +14 -18
- package/dist/components/steps.css +15 -5
- package/dist/components/steps.mjs +1 -0
- package/dist/components/switch.css +11 -13
- package/dist/components/switch.mjs +1 -0
- package/dist/components/table.css +46 -47
- package/dist/components/table.mjs +1 -0
- package/dist/components/tabs.css +57 -45
- package/dist/components/tag-input.css +5 -10
- package/dist/components/toast.css +16 -13
- package/dist/components/tooltip.css +2 -2
- package/dist/components/upload.css +0 -15
- package/dist/index.d.mts +39 -0
- package/dist/index.mjs +11243 -3312
- package/dist/registry.json +147 -9
- package/dist/retro-effects.css +23 -32
- package/dist/tokens/colors.css +4 -1
- package/dist/tokens/retro-effects.css +23 -32
- package/dist/tokens/style-profiles.css +24 -2
- package/dist/tokens/typography.css +2 -0
- package/dist/tokens.css +30 -3
- package/dist/ui.css +3420 -645
- package/docs/spec/ai-native.md +662 -0
- package/package.json +15 -5
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { AiContent, AiContentPart } from './ai-core.js';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* 初始化 root 下的全部 .ai-chat(root 自身匹配时包含自身)。
|
|
5
|
+
* 幂等;返回销毁函数:移除监听、断开观察器并复位幂等标记(可重新 init)。
|
|
6
|
+
*/
|
|
7
|
+
declare function initAiChat(root?: ParentNode): () => void;
|
|
8
|
+
interface AiStreamHandle {
|
|
9
|
+
/** textContent 级追加一段文本(不解析 HTML) */
|
|
10
|
+
append(text: string): void;
|
|
11
|
+
/** 流正常结束:移除 .is-streaming / aria-busy;reasoning 自动折叠 + 回填耗时 */
|
|
12
|
+
done(): void;
|
|
13
|
+
/** 流被取消:同 done,但不折叠 reasoning、不计耗时 */
|
|
14
|
+
cancel(): void;
|
|
15
|
+
/** 流失败:终止追加并给宿主挂 .is-error(错误文本展示由消费方/renderAiMessage.setError 负责) */
|
|
16
|
+
fail(): void;
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* 创建流式追加句柄:追加期间宿主挂 .is-streaming(aria-busy=true),
|
|
20
|
+
* 完成/取消移除。宿主在 .ai-reasoning 内时同步驱动 reasoning 状态:
|
|
21
|
+
* 流式中根挂 .is-streaming(CSS shimmer),done() 自动折叠并显示耗时。
|
|
22
|
+
*/
|
|
23
|
+
declare function createAiStream(el: HTMLElement): AiStreamHandle;
|
|
24
|
+
interface AiMessageModel {
|
|
25
|
+
role: 'user' | 'assistant' | 'system' | 'tool';
|
|
26
|
+
/** 标准化内容:字符串(纯文本)或部件数组(文字/图片/音频/视频/文件/资源链接) */
|
|
27
|
+
content: AiContent;
|
|
28
|
+
/** 元信息行("12:04 · 1.2k tok") */
|
|
29
|
+
meta?: string;
|
|
30
|
+
/** assistant 消息的模型名标签 */
|
|
31
|
+
model?: string;
|
|
32
|
+
/** 错误态:消息挂 .ai-msg--error + 错误文本行 */
|
|
33
|
+
error?: string;
|
|
34
|
+
/** 初始即流式态(正文末文本 span 挂 .is-streaming) */
|
|
35
|
+
streaming?: boolean;
|
|
36
|
+
}
|
|
37
|
+
interface AiMessageHandle {
|
|
38
|
+
/** 消息行元素(.ai-msg) */
|
|
39
|
+
el: HTMLElement;
|
|
40
|
+
/** 正文容器(.ai-msg-body):消费方追加自定义内容的挂载点 */
|
|
41
|
+
body: HTMLElement;
|
|
42
|
+
setMeta(meta: string): void;
|
|
43
|
+
setError(message: string): void;
|
|
44
|
+
/** 在正文末开一个流式文本节点,返回 createAiStream 句柄 */
|
|
45
|
+
stream(): AiStreamHandle;
|
|
46
|
+
}
|
|
47
|
+
/** 单个内容部件 → DOM 节点(文本一律 textContent;媒体 src 走 aiContentUrl 归一)。
|
|
48
|
+
* 导出供 ai-tool 的 IO 区复用(MCP 多模态回执;类名 .ai-msg-* 语义即「消息部件」,全局生效)。
|
|
49
|
+
* 前置条件:浏览器环境——本函数无 SSR 守卫,SSR 由调用方守卫覆盖(renderAiMessage /
|
|
50
|
+
* renderAiToolCall 头部守卫后才会触达此处)。 */
|
|
51
|
+
declare function renderAiContentPart(part: AiContentPart): HTMLElement | undefined;
|
|
52
|
+
/**
|
|
53
|
+
* DOM API 渲染一条消息(append 到容器末尾):多角色变体 + 多模态部件 + copy/retry
|
|
54
|
+
* 动作钮(委托归 initAiChat)+ meta/model 标签 + 错误变体。返回句柄可继续
|
|
55
|
+
* setMeta / setError / stream()。滚动跟随由 initAiChat 的钉底机制自动处理。
|
|
56
|
+
*/
|
|
57
|
+
declare function renderAiMessage(scrollEl: HTMLElement, model: AiMessageModel): AiMessageHandle;
|
|
58
|
+
|
|
59
|
+
export { type AiMessageHandle, type AiMessageModel, type AiStreamHandle, createAiStream, initAiChat, renderAiContentPart, renderAiMessage };
|
|
@@ -0,0 +1,580 @@
|
|
|
1
|
+
// src/behaviors/ai-core.ts
|
|
2
|
+
var svg = (body) => `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">${body}</svg>`;
|
|
3
|
+
var ICON_TERMINAL = svg('<path d="m4 17 6-6-6-6"/><path d="M12 19h8"/>');
|
|
4
|
+
var ICON_FILE = svg(
|
|
5
|
+
'<path d="M15 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V7z"/><path d="M14 2v4a2 2 0 0 0 2 2h4"/>'
|
|
6
|
+
);
|
|
7
|
+
var ICON_FILE_PEN = svg(
|
|
8
|
+
'<path d="M12.5 22H6a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h8l5 5v3.5"/><path d="M14 2v4a2 2 0 0 0 2 2h4"/><path d="M19 6.5 16.5 4"/><path d="M21.378 16.626a1 1 0 0 0-3.004-3.004l-4.01 4.012a2 2 0 0 0-.506.854l-.837 2.87a.5.5 0 0 0 .62.62l2.87-.837a2 2 0 0 0 .854-.506z"/>'
|
|
9
|
+
);
|
|
10
|
+
var ICON_FILE_PLUS = svg(
|
|
11
|
+
'<path d="M15 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V7z"/><path d="M14 2v4a2 2 0 0 0 2 2h4"/><path d="M9 15h6"/><path d="M12 12v6"/>'
|
|
12
|
+
);
|
|
13
|
+
var ICON_TRASH = svg(
|
|
14
|
+
'<path d="M3 6h18"/><path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6"/><path d="M8 6V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"/><path d="M10 11v6"/><path d="M14 11v6"/>'
|
|
15
|
+
);
|
|
16
|
+
var ICON_SEARCH = svg('<circle cx="11" cy="11" r="8"/><path d="m21 21-4.3-4.3"/>');
|
|
17
|
+
var ICON_FOLDER_SEARCH = svg(
|
|
18
|
+
'<path d="M11 20H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h3.9a2 2 0 0 1 1.69.9l.81 1.2a2 2 0 0 0 1.67.9H20a2 2 0 0 1 2 2v4"/><circle cx="17" cy="17" r="3"/><path d="m21 21-1.9-1.9"/>'
|
|
19
|
+
);
|
|
20
|
+
var ICON_SCAN_SEARCH = svg(
|
|
21
|
+
'<path d="M3 7V5a2 2 0 0 1 2-2h2"/><path d="M17 3h2a2 2 0 0 1 2 2v2"/><path d="M21 17v2a2 2 0 0 1-2 2h-2"/><path d="M7 21H5a2 2 0 0 1-2-2v-2"/><circle cx="12" cy="12" r="3"/><path d="m16 16-1.9-1.9"/>'
|
|
22
|
+
);
|
|
23
|
+
var ICON_GLOBE = svg(
|
|
24
|
+
'<circle cx="12" cy="12" r="10"/><path d="M12 2a14.5 14.5 0 0 0 0 20 14.5 14.5 0 0 0 0-20"/><path d="M2 12h20"/>'
|
|
25
|
+
);
|
|
26
|
+
var ICON_DOWNLOAD = svg(
|
|
27
|
+
'<path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/><path d="m7 10 5 5 5-5"/><path d="M12 15V3"/>'
|
|
28
|
+
);
|
|
29
|
+
var ICON_BLOCKS = svg(
|
|
30
|
+
'<rect width="7" height="7" x="4" y="4" rx="1"/><rect width="7" height="7" x="13" y="4" rx="1"/><rect width="7" height="7" x="4" y="13" rx="1"/><rect width="7" height="7" x="13" y="13" rx="1"/>'
|
|
31
|
+
);
|
|
32
|
+
var ICON_ZAP = svg(
|
|
33
|
+
'<path d="M4 14a1 1 0 0 1-.78-1.63l9.9-10.2a.5.5 0 0 1 .86.46l-1.92 6.02A1 1 0 0 0 13 10h7a1 1 0 0 1 .78 1.63l-9.9 10.2a.5.5 0 0 1-.86-.46l1.92-6.02A1 1 0 0 0 11 14z"/>'
|
|
34
|
+
);
|
|
35
|
+
var ICON_LIST = svg(
|
|
36
|
+
'<path d="M3 12h.01"/><path d="M3 18h.01"/><path d="M3 6h.01"/><path d="M8 12h13"/><path d="M8 18h13"/><path d="M8 6h13"/>'
|
|
37
|
+
);
|
|
38
|
+
var ICON_LIST_CHECKS = svg(
|
|
39
|
+
'<path d="m3 17 2 2 4-4"/><path d="m3 7 2 2 4-4"/><path d="M13 6h8"/><path d="M13 12h8"/><path d="M13 18h8"/>'
|
|
40
|
+
);
|
|
41
|
+
var ICON_BOT = svg(
|
|
42
|
+
'<path d="M12 8V4H8"/><rect width="16" height="12" x="4" y="8" rx="2"/><path d="M2 14h2"/><path d="M20 14h2"/><path d="M15 13v2"/><path d="M9 13v2"/>'
|
|
43
|
+
);
|
|
44
|
+
var ICON_CHART = svg(
|
|
45
|
+
'<path d="M3 3v18h18"/><path d="M7 15l4-6 3 4 5-8"/>'
|
|
46
|
+
);
|
|
47
|
+
var ICON_FILE_TEXT = svg(
|
|
48
|
+
'<path d="M15 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V7z"/><path d="M14 2v4a2 2 0 0 0 2 2h4"/><path d="M10 9H8"/><path d="M16 13H8"/><path d="M16 17H8"/>'
|
|
49
|
+
);
|
|
50
|
+
function isObj(v) {
|
|
51
|
+
return typeof v === "object" && v !== null;
|
|
52
|
+
}
|
|
53
|
+
function pickStr(input, keys) {
|
|
54
|
+
if (!isObj(input)) return void 0;
|
|
55
|
+
for (const k of keys) {
|
|
56
|
+
const v = input[k];
|
|
57
|
+
if (typeof v === "string" && v) return v;
|
|
58
|
+
}
|
|
59
|
+
return void 0;
|
|
60
|
+
}
|
|
61
|
+
function pickNum(src, keys) {
|
|
62
|
+
if (!isObj(src)) return void 0;
|
|
63
|
+
for (const k of keys) {
|
|
64
|
+
const v = src[k];
|
|
65
|
+
if (typeof v === "number" && Number.isFinite(v)) return v;
|
|
66
|
+
}
|
|
67
|
+
return void 0;
|
|
68
|
+
}
|
|
69
|
+
function firstLine(s, max = 80) {
|
|
70
|
+
const line = s.split("\n", 1)[0]?.trim() ?? "";
|
|
71
|
+
return line.length > max ? `${line.slice(0, max)}\u2026` : line;
|
|
72
|
+
}
|
|
73
|
+
function filePathOf(input) {
|
|
74
|
+
return pickStr(input, ["file_path", "path", "file", "filename"]);
|
|
75
|
+
}
|
|
76
|
+
var summarizePath = (input) => filePathOf(input);
|
|
77
|
+
function summarizeRead(input) {
|
|
78
|
+
const p = filePathOf(input);
|
|
79
|
+
if (!p) return void 0;
|
|
80
|
+
const offset = pickNum(input, ["offset", "start_line"]);
|
|
81
|
+
if (offset == null) return p;
|
|
82
|
+
const limit = pickNum(input, ["limit", "end_line"]);
|
|
83
|
+
return limit != null ? `${p}:${offset}-${offset + limit}` : `${p}:${offset}`;
|
|
84
|
+
}
|
|
85
|
+
function summarizeShell(input) {
|
|
86
|
+
const cmd = typeof input === "string" ? input : pickStr(input, ["command", "cmd", "script"]);
|
|
87
|
+
return cmd ? firstLine(cmd) : void 0;
|
|
88
|
+
}
|
|
89
|
+
function summarizeGrep(input) {
|
|
90
|
+
const pattern = pickStr(input, ["pattern", "regex", "query"]);
|
|
91
|
+
const p = filePathOf(input);
|
|
92
|
+
if (pattern) return p ? `${firstLine(pattern, 40)} \xB7 ${p}` : firstLine(pattern);
|
|
93
|
+
return p;
|
|
94
|
+
}
|
|
95
|
+
var sum = (fn) => {
|
|
96
|
+
return (input) => fn(input) ?? "";
|
|
97
|
+
};
|
|
98
|
+
var BUILTIN_KINDS = {
|
|
99
|
+
shell: { label: "\u547D\u4EE4", icon: ICON_TERMINAL, tint: "accent", summarize: sum(summarizeShell) },
|
|
100
|
+
read: { label: "\u8BFB\u53D6", icon: ICON_FILE, tint: "info", summarize: sum(summarizeRead) },
|
|
101
|
+
edit: { label: "\u7F16\u8F91", icon: ICON_FILE_PEN, tint: "warning", summarize: sum(summarizePath) },
|
|
102
|
+
write: { label: "\u5199\u5165", icon: ICON_FILE_PLUS, tint: "warning", summarize: sum(summarizePath) },
|
|
103
|
+
rm: { label: "\u5220\u9664", icon: ICON_TRASH, tint: "error", summarize: sum(summarizePath) },
|
|
104
|
+
grep: { label: "\u641C\u7D22", icon: ICON_SEARCH, tint: "info", summarize: sum(summarizeGrep) },
|
|
105
|
+
glob: { label: "\u5339\u914D", icon: ICON_FOLDER_SEARCH, tint: "info", summarize: sum((i) => pickStr(i, ["pattern", "glob", "path"])) },
|
|
106
|
+
browser: { label: "\u6D4F\u89C8", icon: ICON_GLOBE, tint: "accent", summarize: sum((i) => pickStr(i, ["url", "uri", "target"])) },
|
|
107
|
+
search: { label: "\u68C0\u7D22", icon: ICON_SCAN_SEARCH, tint: "accent", summarize: sum((i) => pickStr(i, ["query", "q", "pattern"])) },
|
|
108
|
+
fetch: { label: "\u8BF7\u6C42", icon: ICON_DOWNLOAD, tint: "info", summarize: sum((i) => pickStr(i, ["url", "uri", "endpoint"])) },
|
|
109
|
+
mcp: { label: "MCP", icon: ICON_BLOCKS, tint: "muted", summarize: sum((i) => pickStr(i, ["tool", "method", "name"])) },
|
|
110
|
+
skill: { label: "\u6280\u80FD", icon: ICON_ZAP, tint: "accent", summarize: sum((i) => pickStr(i, ["skill", "name", "command"])) },
|
|
111
|
+
todo: {
|
|
112
|
+
label: "\u5F85\u529E",
|
|
113
|
+
icon: ICON_LIST,
|
|
114
|
+
tint: "info",
|
|
115
|
+
summarize: sum((input) => {
|
|
116
|
+
const items = input && typeof input === "object" && Array.isArray(input.items) ? input.items : Array.isArray(input) ? input : [];
|
|
117
|
+
const done = items.filter((it) => it?.status === "done").length;
|
|
118
|
+
return items.length > 0 ? `${done}/${items.length} \u5B8C\u6210` : "";
|
|
119
|
+
})
|
|
120
|
+
},
|
|
121
|
+
plan: { label: "\u8BA1\u5212", icon: ICON_LIST_CHECKS, tint: "info" },
|
|
122
|
+
subagent: { label: "\u5B50\u4EE3\u7406", icon: ICON_BOT, tint: "accent", summarize: sum((i) => pickStr(i, ["task", "description", "prompt", "message", "subagent_type"])) },
|
|
123
|
+
note: { label: "\u7B14\u8BB0", icon: ICON_FILE_TEXT, tint: "muted" },
|
|
124
|
+
chart: { label: "\u56FE\u8868", icon: ICON_CHART, tint: "info", summarize: sum((i) => pickStr(i, ["title", "name"])) }
|
|
125
|
+
};
|
|
126
|
+
var kindRegistry = new Map(Object.entries(BUILTIN_KINDS));
|
|
127
|
+
function trim1(v) {
|
|
128
|
+
const s = (Math.round(v * 10) / 10).toFixed(1);
|
|
129
|
+
return s.endsWith(".0") ? s.slice(0, -2) : s;
|
|
130
|
+
}
|
|
131
|
+
function formatDuration(ms) {
|
|
132
|
+
if (!Number.isFinite(ms) || ms <= 0) return "0ms";
|
|
133
|
+
if (ms < 1e3) return `${Math.round(ms)}ms`;
|
|
134
|
+
if (ms < 6e4) return `${trim1(ms / 1e3)}s`;
|
|
135
|
+
const m = Math.floor(ms / 6e4);
|
|
136
|
+
const s = Math.round(ms % 6e4 / 1e3);
|
|
137
|
+
if (ms < 36e5) return s > 0 ? `${m}m${s}s` : `${m}m`;
|
|
138
|
+
const h2 = Math.floor(ms / 36e5);
|
|
139
|
+
const rm = Math.round(ms % 36e5 / 6e4);
|
|
140
|
+
return rm > 0 ? `${h2}h${rm}m` : `${h2}h`;
|
|
141
|
+
}
|
|
142
|
+
function aiContentUrl(part) {
|
|
143
|
+
if (part.type === "text" || part.type === "resource-link") return void 0;
|
|
144
|
+
if (typeof part.url === "string" && part.url) return part.url;
|
|
145
|
+
if (typeof part.data === "string" && part.data && typeof part.mimeType === "string") {
|
|
146
|
+
return `data:${part.mimeType};base64,${part.data}`;
|
|
147
|
+
}
|
|
148
|
+
return void 0;
|
|
149
|
+
}
|
|
150
|
+
function h(tag, className, text) {
|
|
151
|
+
const node = document.createElement(tag);
|
|
152
|
+
if (className) node.className = className;
|
|
153
|
+
if (text != null) node.textContent = text;
|
|
154
|
+
return node;
|
|
155
|
+
}
|
|
156
|
+
function svgIcon(svg2) {
|
|
157
|
+
if (typeof document === "undefined" || typeof DOMParser === "undefined") return null;
|
|
158
|
+
let parsed;
|
|
159
|
+
try {
|
|
160
|
+
parsed = new DOMParser().parseFromString(svg2, "image/svg+xml");
|
|
161
|
+
} catch {
|
|
162
|
+
return null;
|
|
163
|
+
}
|
|
164
|
+
const root = parsed.documentElement;
|
|
165
|
+
if (!root || root.tagName.toLowerCase() !== "svg" || parsed.querySelector("parsererror")) {
|
|
166
|
+
return null;
|
|
167
|
+
}
|
|
168
|
+
const sanitize = (el) => {
|
|
169
|
+
for (const attr of Array.from(el.attributes)) {
|
|
170
|
+
const name = attr.name.toLowerCase();
|
|
171
|
+
if (name.startsWith("on")) el.removeAttribute(attr.name);
|
|
172
|
+
if ((name === "href" || name === "xlink:href") && attr.value.trim().toLowerCase().startsWith("javascript:")) {
|
|
173
|
+
el.removeAttribute(attr.name);
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
for (const child of Array.from(el.children)) {
|
|
177
|
+
const tag = child.tagName.toLowerCase();
|
|
178
|
+
if (tag === "script" || tag === "foreignobject") {
|
|
179
|
+
child.remove();
|
|
180
|
+
continue;
|
|
181
|
+
}
|
|
182
|
+
sanitize(child);
|
|
183
|
+
}
|
|
184
|
+
};
|
|
185
|
+
sanitize(root);
|
|
186
|
+
return document.importNode(root, true);
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
// src/behaviors/events.ts
|
|
190
|
+
function emitIcen(source, type, detail) {
|
|
191
|
+
return source.dispatchEvent(
|
|
192
|
+
new CustomEvent(type, { detail, bubbles: true, composed: true, cancelable: true })
|
|
193
|
+
);
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
// src/behaviors/ai-chat.ts
|
|
197
|
+
var ICON_COPY = '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><rect x="8" y="2" width="8" height="4" rx="1"/><path d="M16 4h2a2 2 0 0 1 2 2v14a2 2 0 0 1-2 2H6a2 2 0 0 1-2-2V6a2 2 0 0 1 2-2h2"/></svg>';
|
|
198
|
+
var ICON_RETRY = '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M3 12a9 9 0 1 0 9-9 9.75 9.75 0 0 0-6.74 2.74L3 8"/><path d="M3 3v5h5"/></svg>';
|
|
199
|
+
function isBrowser() {
|
|
200
|
+
return typeof document !== "undefined" && typeof window !== "undefined";
|
|
201
|
+
}
|
|
202
|
+
function prefersReducedMotion() {
|
|
203
|
+
if (typeof window === "undefined" || typeof window.matchMedia !== "function") return false;
|
|
204
|
+
return window.matchMedia("(prefers-reduced-motion: reduce)").matches;
|
|
205
|
+
}
|
|
206
|
+
function emit(target, name, detail) {
|
|
207
|
+
emitIcen(target, name, detail);
|
|
208
|
+
}
|
|
209
|
+
function fallbackCopy(text) {
|
|
210
|
+
const ta = document.createElement("textarea");
|
|
211
|
+
ta.value = text;
|
|
212
|
+
ta.style.position = "fixed";
|
|
213
|
+
ta.style.opacity = "0";
|
|
214
|
+
document.body.appendChild(ta);
|
|
215
|
+
ta.select();
|
|
216
|
+
try {
|
|
217
|
+
document.execCommand("copy");
|
|
218
|
+
} catch {
|
|
219
|
+
}
|
|
220
|
+
ta.remove();
|
|
221
|
+
}
|
|
222
|
+
function copyText(text) {
|
|
223
|
+
if (typeof navigator !== "undefined" && navigator.clipboard) {
|
|
224
|
+
navigator.clipboard.writeText(text).catch(() => fallbackCopy(text));
|
|
225
|
+
} else {
|
|
226
|
+
fallbackCopy(text);
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
function setupChat(chat) {
|
|
230
|
+
const el = chat;
|
|
231
|
+
if (el.__icenAiChatInit) return void 0;
|
|
232
|
+
el.__icenAiChatInit = true;
|
|
233
|
+
const scrollEl = chat.querySelector(".ai-chat-scroll");
|
|
234
|
+
let jump = chat.querySelector(".ai-chat-jump") ?? null;
|
|
235
|
+
let pinned = true;
|
|
236
|
+
let unread = 0;
|
|
237
|
+
let lastMsgCount = 0;
|
|
238
|
+
let suppressUntil = 0;
|
|
239
|
+
let rafPending = false;
|
|
240
|
+
const NEAR_BOTTOM_PX = 32;
|
|
241
|
+
const isAtBottom = () => scrollEl ? scrollEl.scrollHeight - scrollEl.scrollTop - scrollEl.clientHeight < NEAR_BOTTOM_PX : true;
|
|
242
|
+
const updateJump = () => {
|
|
243
|
+
if (!jump) return;
|
|
244
|
+
if (pinned) {
|
|
245
|
+
jump.hidden = true;
|
|
246
|
+
return;
|
|
247
|
+
}
|
|
248
|
+
jump.textContent = unread > 0 ? `\u56DE\u5230\u5E95\u90E8 \xB7 ${unread} \u6761\u65B0\u6D88\u606F` : "\u56DE\u5230\u5E95\u90E8";
|
|
249
|
+
jump.hidden = false;
|
|
250
|
+
};
|
|
251
|
+
const scrollToBottom = (smooth) => {
|
|
252
|
+
if (!scrollEl) return;
|
|
253
|
+
const behavior = smooth && !prefersReducedMotion() ? "smooth" : "auto";
|
|
254
|
+
suppressUntil = Date.now() + (behavior === "smooth" ? 600 : 100);
|
|
255
|
+
scrollEl.scrollTo({ top: scrollEl.scrollHeight, behavior });
|
|
256
|
+
};
|
|
257
|
+
const onContentChange = () => {
|
|
258
|
+
rafPending = false;
|
|
259
|
+
if (!scrollEl) return;
|
|
260
|
+
if (pinned) {
|
|
261
|
+
scrollToBottom(false);
|
|
262
|
+
} else {
|
|
263
|
+
const count = scrollEl.querySelectorAll(".ai-msg").length;
|
|
264
|
+
if (count >= lastMsgCount) {
|
|
265
|
+
unread += count - lastMsgCount;
|
|
266
|
+
} else {
|
|
267
|
+
unread = 0;
|
|
268
|
+
}
|
|
269
|
+
lastMsgCount = count;
|
|
270
|
+
updateJump();
|
|
271
|
+
}
|
|
272
|
+
};
|
|
273
|
+
const scheduleContentCheck = () => {
|
|
274
|
+
if (rafPending) return;
|
|
275
|
+
rafPending = true;
|
|
276
|
+
window.requestAnimationFrame(onContentChange);
|
|
277
|
+
};
|
|
278
|
+
const onScroll = () => {
|
|
279
|
+
if (Date.now() < suppressUntil) return;
|
|
280
|
+
const atBottom = isAtBottom();
|
|
281
|
+
if (atBottom && !pinned) {
|
|
282
|
+
pinned = true;
|
|
283
|
+
unread = 0;
|
|
284
|
+
updateJump();
|
|
285
|
+
} else if (!atBottom && pinned) {
|
|
286
|
+
pinned = false;
|
|
287
|
+
updateJump();
|
|
288
|
+
}
|
|
289
|
+
};
|
|
290
|
+
const onJumpClick = () => {
|
|
291
|
+
pinned = true;
|
|
292
|
+
unread = 0;
|
|
293
|
+
updateJump();
|
|
294
|
+
scrollToBottom(true);
|
|
295
|
+
};
|
|
296
|
+
const onClick = (e) => {
|
|
297
|
+
const target = e.target;
|
|
298
|
+
if (!(target instanceof Element)) return;
|
|
299
|
+
const actionBtn = target.closest("[data-ai-msg-action]");
|
|
300
|
+
if (actionBtn) {
|
|
301
|
+
const msg = actionBtn.closest(".ai-msg");
|
|
302
|
+
if (!msg) return;
|
|
303
|
+
const action = actionBtn.dataset.aiMsgAction;
|
|
304
|
+
if (action === "copy") {
|
|
305
|
+
const bodyEl = msg.querySelector(".ai-msg-body");
|
|
306
|
+
const clone = bodyEl?.cloneNode(true);
|
|
307
|
+
if (clone instanceof HTMLElement) {
|
|
308
|
+
clone.querySelectorAll(".ai-msg-model").forEach((chip) => chip.remove());
|
|
309
|
+
copyText(clone.textContent ?? "");
|
|
310
|
+
} else {
|
|
311
|
+
copyText("");
|
|
312
|
+
}
|
|
313
|
+
emit(msg, "icen:ai-copy", { el: msg });
|
|
314
|
+
} else if (action === "retry") {
|
|
315
|
+
emit(msg, "icen:ai-retry", { el: msg });
|
|
316
|
+
}
|
|
317
|
+
return;
|
|
318
|
+
}
|
|
319
|
+
const head = target.closest(".ai-reasoning-head");
|
|
320
|
+
if (head) {
|
|
321
|
+
const reasoning = head.closest(".ai-reasoning");
|
|
322
|
+
if (!reasoning) return;
|
|
323
|
+
const body = reasoning.querySelector(".ai-reasoning-body");
|
|
324
|
+
const open = head.getAttribute("aria-expanded") !== "true";
|
|
325
|
+
head.setAttribute("aria-expanded", String(open));
|
|
326
|
+
reasoning.classList.toggle("is-open", open);
|
|
327
|
+
if (body) body.hidden = !open;
|
|
328
|
+
emit(reasoning, "icen:ai-toggle", { el: reasoning, open });
|
|
329
|
+
}
|
|
330
|
+
};
|
|
331
|
+
const disposers = [];
|
|
332
|
+
let resizeObs;
|
|
333
|
+
let mutationObs;
|
|
334
|
+
if (scrollEl) {
|
|
335
|
+
if (!jump) {
|
|
336
|
+
jump = document.createElement("button");
|
|
337
|
+
jump.type = "button";
|
|
338
|
+
jump.className = "ai-chat-jump";
|
|
339
|
+
jump.hidden = true;
|
|
340
|
+
chat.appendChild(jump);
|
|
341
|
+
}
|
|
342
|
+
lastMsgCount = scrollEl.querySelectorAll(".ai-msg").length;
|
|
343
|
+
scrollEl.addEventListener("scroll", onScroll, { passive: true });
|
|
344
|
+
jump.addEventListener("click", onJumpClick);
|
|
345
|
+
const jumpBtn = jump;
|
|
346
|
+
disposers.push(() => {
|
|
347
|
+
scrollEl.removeEventListener("scroll", onScroll);
|
|
348
|
+
jumpBtn.removeEventListener("click", onJumpClick);
|
|
349
|
+
});
|
|
350
|
+
if (typeof ResizeObserver !== "undefined") {
|
|
351
|
+
resizeObs = new ResizeObserver(scheduleContentCheck);
|
|
352
|
+
resizeObs.observe(scrollEl);
|
|
353
|
+
for (const child of Array.from(scrollEl.children)) resizeObs.observe(child);
|
|
354
|
+
disposers.push(() => resizeObs?.disconnect());
|
|
355
|
+
}
|
|
356
|
+
if (typeof MutationObserver !== "undefined") {
|
|
357
|
+
mutationObs = new MutationObserver((records) => {
|
|
358
|
+
if (resizeObs) {
|
|
359
|
+
for (const rec of records) {
|
|
360
|
+
for (const node of Array.from(rec.addedNodes)) {
|
|
361
|
+
if (node instanceof Element) resizeObs.observe(node);
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
scheduleContentCheck();
|
|
366
|
+
});
|
|
367
|
+
mutationObs.observe(scrollEl, { childList: true, subtree: true, characterData: true });
|
|
368
|
+
disposers.push(() => mutationObs?.disconnect());
|
|
369
|
+
}
|
|
370
|
+
scrollToBottom(false);
|
|
371
|
+
}
|
|
372
|
+
chat.addEventListener("click", onClick);
|
|
373
|
+
disposers.push(() => chat.removeEventListener("click", onClick));
|
|
374
|
+
return () => {
|
|
375
|
+
for (const dispose of disposers) dispose();
|
|
376
|
+
el.__icenAiChatInit = false;
|
|
377
|
+
};
|
|
378
|
+
}
|
|
379
|
+
function initAiChat(root) {
|
|
380
|
+
const cleanups = [];
|
|
381
|
+
if (isBrowser()) {
|
|
382
|
+
const scope = root ?? document;
|
|
383
|
+
const candidates = [];
|
|
384
|
+
if (scope instanceof Element && scope.matches(".ai-chat")) {
|
|
385
|
+
candidates.push(scope);
|
|
386
|
+
}
|
|
387
|
+
candidates.push(...Array.from(scope.querySelectorAll(".ai-chat")));
|
|
388
|
+
for (const chat of candidates) {
|
|
389
|
+
const cleanup = setupChat(chat);
|
|
390
|
+
if (cleanup) cleanups.push(cleanup);
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
return () => {
|
|
394
|
+
for (const fn of cleanups) fn();
|
|
395
|
+
cleanups.length = 0;
|
|
396
|
+
};
|
|
397
|
+
}
|
|
398
|
+
function createAiStream(el) {
|
|
399
|
+
const reasoning = el.closest(".ai-reasoning");
|
|
400
|
+
const startedAt = Date.now();
|
|
401
|
+
let state = "streaming";
|
|
402
|
+
el.classList.add("is-streaming");
|
|
403
|
+
el.setAttribute("aria-busy", "true");
|
|
404
|
+
reasoning?.classList.add("is-streaming");
|
|
405
|
+
const settle = (finished) => {
|
|
406
|
+
if (state !== "streaming") return;
|
|
407
|
+
state = finished ? "done" : "cancelled";
|
|
408
|
+
el.classList.remove("is-streaming");
|
|
409
|
+
el.removeAttribute("aria-busy");
|
|
410
|
+
if (!reasoning) return;
|
|
411
|
+
reasoning.classList.remove("is-streaming");
|
|
412
|
+
if (!finished) return;
|
|
413
|
+
const head = reasoning.querySelector(".ai-reasoning-head");
|
|
414
|
+
const body = reasoning.querySelector(".ai-reasoning-body");
|
|
415
|
+
head?.setAttribute("aria-expanded", "false");
|
|
416
|
+
if (body) body.hidden = true;
|
|
417
|
+
const time = reasoning.querySelector(".ai-reasoning-time");
|
|
418
|
+
if (time) time.textContent = formatDuration(Date.now() - startedAt);
|
|
419
|
+
};
|
|
420
|
+
return {
|
|
421
|
+
append(text) {
|
|
422
|
+
if (state !== "streaming" || !text) return;
|
|
423
|
+
el.textContent += text;
|
|
424
|
+
},
|
|
425
|
+
done() {
|
|
426
|
+
settle(true);
|
|
427
|
+
},
|
|
428
|
+
cancel() {
|
|
429
|
+
settle(false);
|
|
430
|
+
},
|
|
431
|
+
fail() {
|
|
432
|
+
if (state !== "streaming") return;
|
|
433
|
+
state = "error";
|
|
434
|
+
el.classList.remove("is-streaming");
|
|
435
|
+
el.removeAttribute("aria-busy");
|
|
436
|
+
el.classList.add("is-error");
|
|
437
|
+
reasoning?.classList.remove("is-streaming");
|
|
438
|
+
}
|
|
439
|
+
};
|
|
440
|
+
}
|
|
441
|
+
var ROLE_AVATAR = {
|
|
442
|
+
user: "\u6211",
|
|
443
|
+
assistant: "AI",
|
|
444
|
+
system: "\u7CFB",
|
|
445
|
+
tool: "\u5DE5"
|
|
446
|
+
};
|
|
447
|
+
function safeHref(uri) {
|
|
448
|
+
const s = uri.trim().toLowerCase();
|
|
449
|
+
if (s.startsWith("javascript:") || s.startsWith("data:text/html")) return void 0;
|
|
450
|
+
return uri;
|
|
451
|
+
}
|
|
452
|
+
function renderAiContentPart(part) {
|
|
453
|
+
if (part.type === "text") {
|
|
454
|
+
const span = h("span", "ai-msg-text");
|
|
455
|
+
span.textContent = part.text;
|
|
456
|
+
if (part.state === "streaming") span.classList.add("is-streaming");
|
|
457
|
+
return span;
|
|
458
|
+
}
|
|
459
|
+
if (part.type === "image") {
|
|
460
|
+
const src = aiContentUrl(part);
|
|
461
|
+
if (!src) return void 0;
|
|
462
|
+
const img = document.createElement("img");
|
|
463
|
+
img.className = "ai-msg-media ai-msg-media--image";
|
|
464
|
+
img.alt = part.alt ?? "";
|
|
465
|
+
img.setAttribute("loading", "lazy");
|
|
466
|
+
img.setAttribute("src", src);
|
|
467
|
+
return img;
|
|
468
|
+
}
|
|
469
|
+
if (part.type === "audio") {
|
|
470
|
+
const src = aiContentUrl(part);
|
|
471
|
+
if (!src) return void 0;
|
|
472
|
+
const audio = document.createElement("audio");
|
|
473
|
+
audio.className = "ai-msg-media ai-msg-media--audio";
|
|
474
|
+
audio.controls = true;
|
|
475
|
+
audio.setAttribute("src", src);
|
|
476
|
+
return audio;
|
|
477
|
+
}
|
|
478
|
+
if (part.type === "video") {
|
|
479
|
+
const src = aiContentUrl(part);
|
|
480
|
+
if (!src) return void 0;
|
|
481
|
+
const video = document.createElement("video");
|
|
482
|
+
video.className = "ai-msg-media ai-msg-media--video";
|
|
483
|
+
video.controls = true;
|
|
484
|
+
video.setAttribute("playsinline", "");
|
|
485
|
+
video.setAttribute("src", src);
|
|
486
|
+
return video;
|
|
487
|
+
}
|
|
488
|
+
if (part.type === "file") {
|
|
489
|
+
const chip = h("span", "ai-msg-file");
|
|
490
|
+
chip.appendChild(h("span", "ai-msg-file-name", part.filename ?? "\u6587\u4EF6"));
|
|
491
|
+
const href2 = part.url ? safeHref(part.url) : void 0;
|
|
492
|
+
if (href2 && /^(https?:|data:)/i.test(href2)) {
|
|
493
|
+
const a2 = document.createElement("a");
|
|
494
|
+
a2.className = "ai-msg-file-link";
|
|
495
|
+
a2.href = href2;
|
|
496
|
+
a2.download = part.filename ?? "";
|
|
497
|
+
a2.textContent = "\u4E0B\u8F7D";
|
|
498
|
+
chip.appendChild(a2);
|
|
499
|
+
}
|
|
500
|
+
return chip;
|
|
501
|
+
}
|
|
502
|
+
const a = document.createElement("a");
|
|
503
|
+
a.className = "ai-msg-link";
|
|
504
|
+
const href = safeHref(part.uri);
|
|
505
|
+
if (href) a.href = href;
|
|
506
|
+
a.textContent = part.name ?? part.uri;
|
|
507
|
+
a.title = part.uri;
|
|
508
|
+
return a;
|
|
509
|
+
}
|
|
510
|
+
function renderAiMessage(scrollEl, model) {
|
|
511
|
+
if (typeof document === "undefined") {
|
|
512
|
+
return {
|
|
513
|
+
el: scrollEl,
|
|
514
|
+
body: scrollEl,
|
|
515
|
+
setMeta: () => void 0,
|
|
516
|
+
setError: () => void 0,
|
|
517
|
+
stream: () => createAiStream(scrollEl)
|
|
518
|
+
};
|
|
519
|
+
}
|
|
520
|
+
const row = h("div", `ai-msg ai-msg--${model.role}`);
|
|
521
|
+
const avatar = h("span", "ai-msg-avatar", ROLE_AVATAR[model.role] ?? "");
|
|
522
|
+
const body = h("div", "ai-msg-body");
|
|
523
|
+
row.appendChild(avatar);
|
|
524
|
+
row.appendChild(body);
|
|
525
|
+
const parts = typeof model.content === "string" ? [{ type: "text", text: model.content }] : model.content;
|
|
526
|
+
for (const part of parts) {
|
|
527
|
+
const node = renderAiContentPart(part);
|
|
528
|
+
if (node) body.appendChild(node);
|
|
529
|
+
}
|
|
530
|
+
if (model.model) body.appendChild(h("span", "ai-msg-model", model.model));
|
|
531
|
+
const metaEl = h("div", "ai-msg-meta");
|
|
532
|
+
if (model.meta != null) metaEl.textContent = model.meta;
|
|
533
|
+
row.appendChild(metaEl);
|
|
534
|
+
const actions = h("div", "ai-msg-actions");
|
|
535
|
+
const copyBtn = h("button", "ai-msg-action");
|
|
536
|
+
copyBtn.type = "button";
|
|
537
|
+
copyBtn.setAttribute("data-ai-msg-action", "copy");
|
|
538
|
+
copyBtn.setAttribute("aria-label", "\u590D\u5236");
|
|
539
|
+
const copyIcon = svgIcon(ICON_COPY);
|
|
540
|
+
if (copyIcon) copyBtn.appendChild(copyIcon);
|
|
541
|
+
const retryBtn = h("button", "ai-msg-action");
|
|
542
|
+
retryBtn.type = "button";
|
|
543
|
+
retryBtn.setAttribute("data-ai-msg-action", "retry");
|
|
544
|
+
retryBtn.setAttribute("aria-label", "\u91CD\u8BD5");
|
|
545
|
+
const retryIcon = svgIcon(ICON_RETRY);
|
|
546
|
+
if (retryIcon) retryBtn.appendChild(retryIcon);
|
|
547
|
+
actions.append(copyBtn, retryBtn);
|
|
548
|
+
row.appendChild(actions);
|
|
549
|
+
const handle = {
|
|
550
|
+
el: row,
|
|
551
|
+
body,
|
|
552
|
+
setMeta(meta) {
|
|
553
|
+
metaEl.textContent = meta;
|
|
554
|
+
},
|
|
555
|
+
setError(message) {
|
|
556
|
+
row.classList.add("ai-msg--error");
|
|
557
|
+
let errEl = row.querySelector(".ai-msg-error-text");
|
|
558
|
+
if (!errEl) {
|
|
559
|
+
errEl = h("div", "ai-msg-error-text");
|
|
560
|
+
body.appendChild(errEl);
|
|
561
|
+
}
|
|
562
|
+
errEl.textContent = message;
|
|
563
|
+
},
|
|
564
|
+
stream() {
|
|
565
|
+
const span = h("span", "ai-msg-stream-text");
|
|
566
|
+
body.appendChild(span);
|
|
567
|
+
return createAiStream(span);
|
|
568
|
+
}
|
|
569
|
+
};
|
|
570
|
+
if (model.error != null) handle.setError(model.error);
|
|
571
|
+
if (model.streaming) row.classList.add("is-streaming");
|
|
572
|
+
scrollEl.appendChild(row);
|
|
573
|
+
return handle;
|
|
574
|
+
}
|
|
575
|
+
export {
|
|
576
|
+
createAiStream,
|
|
577
|
+
initAiChat,
|
|
578
|
+
renderAiContentPart,
|
|
579
|
+
renderAiMessage
|
|
580
|
+
};
|