@icen.ai/ui 0.5.0 → 0.8.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/AGENTS.md +256 -0
- package/README.md +47 -13
- package/dist/base.css +31 -4
- package/dist/behaviors/accordion.d.mts +4 -0
- package/dist/behaviors/accordion.mjs +4 -3
- package/dist/behaviors/ai-chat.d.mts +57 -0
- package/dist/behaviors/ai-chat.mjs +557 -0
- package/dist/behaviors/ai-composer.d.mts +112 -0
- package/dist/behaviors/ai-composer.mjs +2032 -0
- package/dist/behaviors/ai-core.d.mts +151 -0
- package/dist/behaviors/ai-core.mjs +491 -0
- package/dist/behaviors/ai-diff.d.mts +53 -0
- package/dist/behaviors/ai-diff.mjs +282 -0
- package/dist/behaviors/ai-panel.d.mts +98 -0
- package/dist/behaviors/ai-panel.mjs +1012 -0
- package/dist/behaviors/ai-provider.d.mts +198 -0
- package/dist/behaviors/ai-provider.mjs +1055 -0
- package/dist/behaviors/ai-tool.d.mts +18 -0
- package/dist/behaviors/ai-tool.mjs +2125 -0
- package/dist/behaviors/ai-tools.d.mts +81 -0
- package/dist/behaviors/ai-tools.mjs +1658 -0
- package/dist/behaviors/back-top.d.mts +20 -0
- package/dist/behaviors/back-top.mjs +49 -32
- package/dist/behaviors/carousel.d.mts +4 -0
- package/dist/behaviors/carousel.mjs +34 -16
- package/dist/behaviors/charts.d.mts +250 -0
- package/dist/behaviors/charts.mjs +1301 -448
- package/dist/behaviors/command-palette.d.mts +8 -0
- package/dist/behaviors/command-palette.mjs +24 -12
- package/dist/behaviors/context-menu.d.mts +36 -0
- package/dist/behaviors/context-menu.mjs +6 -2
- package/dist/behaviors/copy.d.mts +4 -0
- package/dist/behaviors/copy.mjs +7 -6
- package/dist/behaviors/datatable.d.mts +78 -0
- package/dist/behaviors/datatable.mjs +54 -11
- package/dist/behaviors/date-picker.d.mts +4 -0
- package/dist/behaviors/date-picker.mjs +104 -36
- package/dist/behaviors/dropdown.d.mts +4 -0
- package/dist/behaviors/dropdown.mjs +7 -3
- package/dist/behaviors/input.d.mts +4 -0
- package/dist/behaviors/input.mjs +29 -22
- package/dist/behaviors/modal.d.mts +11 -0
- package/dist/behaviors/modal.mjs +11 -6
- package/dist/behaviors/nav.d.mts +7 -0
- package/dist/behaviors/nav.mjs +25 -9
- package/dist/behaviors/notification.d.mts +172 -0
- package/dist/behaviors/notification.mjs +243 -89
- package/dist/behaviors/popover.d.mts +46 -0
- package/dist/behaviors/popover.mjs +2 -1
- package/dist/behaviors/select.d.mts +4 -0
- package/dist/behaviors/select.mjs +131 -26
- package/dist/behaviors/sidebar.d.mts +4 -0
- package/dist/behaviors/sidebar.mjs +4 -3
- package/dist/behaviors/slider.d.mts +4 -0
- package/dist/behaviors/slider.mjs +4 -3
- package/dist/behaviors/split-pane.d.mts +4 -0
- package/dist/behaviors/split-pane.mjs +13 -13
- package/dist/behaviors/tabs.d.mts +8 -0
- package/dist/behaviors/tabs.mjs +44 -19
- package/dist/behaviors/tag-input.d.mts +4 -0
- package/dist/behaviors/tag-input.mjs +12 -3
- package/dist/behaviors/theme.d.mts +25 -0
- package/dist/behaviors/theme.mjs +3 -1
- package/dist/behaviors/toast.d.mts +26 -0
- package/dist/behaviors/toast.mjs +24 -18
- package/dist/behaviors/tree.d.mts +4 -0
- package/dist/behaviors/tree.mjs +4 -3
- package/dist/behaviors/upload.d.mts +4 -0
- package/dist/behaviors/upload.mjs +63 -26
- 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 +4 -8
- package/dist/components/breadcrumb.css +1 -5
- 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 +5 -0
- package/dist/components/chart-gauge.css +1 -1
- package/dist/components/chart-heatmap.css +40 -0
- package/dist/components/chart-scatter.css +24 -0
- package/dist/components/chart-scatter.mjs +4 -0
- package/dist/components/charts.css +86 -1
- package/dist/components/charts.mjs +1 -0
- package/dist/components/command-palette.css +2 -2
- package/dist/components/content.css +8 -9
- package/dist/components/copy.css +8 -8
- package/dist/components/datatable.css +4 -8
- package/dist/components/date-picker.css +3 -7
- 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 +28 -28
- package/dist/components/pagination.css +1 -1
- package/dist/components/panel.css +16 -16
- package/dist/components/pill.css +15 -15
- package/dist/components/segmented.css +5 -5
- package/dist/components/segmented.mjs +1 -0
- package/dist/components/select.css +12 -20
- 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 +2 -2
- package/dist/components/switch.css +8 -12
- package/dist/components/table.css +46 -47
- package/dist/components/tabs.css +35 -46
- package/dist/components/tag-input.css +4 -4
- package/dist/components/toast.css +16 -13
- package/dist/components/tooltip.css +2 -2
- package/dist/index.d.mts +37 -0
- package/dist/index.mjs +7890 -1199
- package/dist/registry.json +113 -4
- package/dist/retro-effects.css +21 -17
- package/dist/tokens/colors.css +4 -1
- package/dist/tokens/retro-effects.css +21 -17
- package/dist/tokens/style-profiles.css +21 -0
- package/dist/tokens.css +25 -1
- package/dist/ui.css +3168 -433
- package/docs/spec/ai-native.md +659 -0
- package/package.json +15 -5
|
@@ -0,0 +1,2032 @@
|
|
|
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 normalizeUsage(raw) {
|
|
128
|
+
if (!isObj(raw)) return {};
|
|
129
|
+
const usage = {};
|
|
130
|
+
const input = pickNum(raw, ["input"]) ?? pickNum(raw, ["prompt_tokens"]) ?? pickNum(raw, ["promptTokenCount"]) ?? pickNum(raw, ["input_tokens"]);
|
|
131
|
+
const output = pickNum(raw, ["output"]) ?? pickNum(raw, ["completion_tokens"]) ?? pickNum(raw, ["candidatesTokenCount"]) ?? pickNum(raw, ["output_tokens"]);
|
|
132
|
+
const cacheRead = pickNum(raw, ["cacheRead", "cache_read"]) ?? pickNum(raw, ["cache_read_input_tokens"]) ?? pickNum(raw, ["prompt_cache_hit_tokens"]) ?? pickNum(raw, ["cached_tokens"]) ?? pickNum(raw, ["cachedContentTokenCount"]) ?? (isObj(raw.prompt_tokens_details) ? pickNum(raw.prompt_tokens_details, ["cached_tokens"]) : void 0);
|
|
133
|
+
const cacheWrite = pickNum(raw, ["cacheWrite", "cache_write"]) ?? pickNum(raw, ["cache_creation_input_tokens"]) ?? pickNum(raw, ["cacheCreationTokenCount"]) ?? (isObj(raw.prompt_tokens_details) ? pickNum(raw.prompt_tokens_details, ["cache_write_tokens"]) : void 0);
|
|
134
|
+
const reasoning = pickNum(raw, ["reasoning"]) ?? pickNum(raw, ["thoughtsTokenCount"]) ?? (isObj(raw.completion_tokens_details) ? pickNum(raw.completion_tokens_details, ["reasoning_tokens"]) : void 0);
|
|
135
|
+
const total = pickNum(raw, ["total"]) ?? pickNum(raw, ["total_tokens"]) ?? pickNum(raw, ["totalTokenCount"]) ?? (input != null || output != null || cacheRead != null || cacheWrite != null || reasoning != null ? (input ?? 0) + (output ?? 0) + (cacheRead ?? 0) + (cacheWrite ?? 0) + (reasoning ?? 0) : void 0);
|
|
136
|
+
if (input != null) usage.input = input;
|
|
137
|
+
if (output != null) usage.output = output;
|
|
138
|
+
if (cacheRead != null) usage.cacheRead = cacheRead;
|
|
139
|
+
if (cacheWrite != null) usage.cacheWrite = cacheWrite;
|
|
140
|
+
if (reasoning != null) usage.reasoning = reasoning;
|
|
141
|
+
if (total != null) usage.total = total;
|
|
142
|
+
return usage;
|
|
143
|
+
}
|
|
144
|
+
function trim1(v) {
|
|
145
|
+
const s = (Math.round(v * 10) / 10).toFixed(1);
|
|
146
|
+
return s.endsWith(".0") ? s.slice(0, -2) : s;
|
|
147
|
+
}
|
|
148
|
+
function formatTokens(n) {
|
|
149
|
+
if (!Number.isFinite(n)) return "0";
|
|
150
|
+
const abs = Math.abs(n);
|
|
151
|
+
if (abs < 1e3) return String(Math.round(n));
|
|
152
|
+
if (abs < 1e6) return `${trim1(n / 1e3)}k`;
|
|
153
|
+
return `${trim1(n / 1e6)}M`;
|
|
154
|
+
}
|
|
155
|
+
function formatDuration(ms) {
|
|
156
|
+
if (!Number.isFinite(ms) || ms <= 0) return "0ms";
|
|
157
|
+
if (ms < 1e3) return `${Math.round(ms)}ms`;
|
|
158
|
+
if (ms < 6e4) return `${trim1(ms / 1e3)}s`;
|
|
159
|
+
const m = Math.floor(ms / 6e4);
|
|
160
|
+
const s = Math.round(ms % 6e4 / 1e3);
|
|
161
|
+
if (ms < 36e5) return s > 0 ? `${m}m${s}s` : `${m}m`;
|
|
162
|
+
const h = Math.floor(ms / 36e5);
|
|
163
|
+
const rm = Math.round(ms % 36e5 / 6e4);
|
|
164
|
+
return rm > 0 ? `${h}h${rm}m` : `${h}h`;
|
|
165
|
+
}
|
|
166
|
+
function aiContentUrl(part) {
|
|
167
|
+
if (part.type === "text" || part.type === "resource-link") return void 0;
|
|
168
|
+
if (typeof part.url === "string" && part.url) return part.url;
|
|
169
|
+
if (typeof part.data === "string" && part.data && typeof part.mimeType === "string") {
|
|
170
|
+
return `data:${part.mimeType};base64,${part.data}`;
|
|
171
|
+
}
|
|
172
|
+
return void 0;
|
|
173
|
+
}
|
|
174
|
+
function contextEstimate(usage) {
|
|
175
|
+
const u = normalizeUsage(usage);
|
|
176
|
+
return (u.input ?? 0) + (u.cacheRead ?? 0) + (u.cacheWrite ?? 0) + (u.output ?? 0) + (u.reasoning ?? 0);
|
|
177
|
+
}
|
|
178
|
+
function svgIcon(svg2) {
|
|
179
|
+
if (typeof document === "undefined" || typeof DOMParser === "undefined") return null;
|
|
180
|
+
let parsed;
|
|
181
|
+
try {
|
|
182
|
+
parsed = new DOMParser().parseFromString(svg2, "image/svg+xml");
|
|
183
|
+
} catch {
|
|
184
|
+
return null;
|
|
185
|
+
}
|
|
186
|
+
const root = parsed.documentElement;
|
|
187
|
+
if (!root || root.tagName.toLowerCase() !== "svg" || parsed.querySelector("parsererror")) {
|
|
188
|
+
return null;
|
|
189
|
+
}
|
|
190
|
+
const sanitize = (el3) => {
|
|
191
|
+
for (const attr of Array.from(el3.attributes)) {
|
|
192
|
+
const name = attr.name.toLowerCase();
|
|
193
|
+
if (name.startsWith("on")) el3.removeAttribute(attr.name);
|
|
194
|
+
if ((name === "href" || name === "xlink:href") && attr.value.trim().toLowerCase().startsWith("javascript:")) {
|
|
195
|
+
el3.removeAttribute(attr.name);
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
for (const child of Array.from(el3.children)) {
|
|
199
|
+
const tag = child.tagName.toLowerCase();
|
|
200
|
+
if (tag === "script" || tag === "foreignobject") {
|
|
201
|
+
child.remove();
|
|
202
|
+
continue;
|
|
203
|
+
}
|
|
204
|
+
sanitize(child);
|
|
205
|
+
}
|
|
206
|
+
};
|
|
207
|
+
sanitize(root);
|
|
208
|
+
return document.importNode(root, true);
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
// src/behaviors/popover.ts
|
|
212
|
+
var DEFAULT_MIN_WIDTH = 220;
|
|
213
|
+
var DEFAULT_MAX_WIDTH = 420;
|
|
214
|
+
var DEFAULT_MIN_HEIGHT = 120;
|
|
215
|
+
var DEFAULT_MAX_HEIGHT = 420;
|
|
216
|
+
function clamp(value, min, max) {
|
|
217
|
+
if (max < min) return min;
|
|
218
|
+
return Math.min(max, Math.max(min, value));
|
|
219
|
+
}
|
|
220
|
+
function chooseSide(preferredSide, availableTop, availableBottom, desiredHeight) {
|
|
221
|
+
const preferredSpace = preferredSide === "top" ? availableTop : availableBottom;
|
|
222
|
+
const oppositeSpace = preferredSide === "top" ? availableBottom : availableTop;
|
|
223
|
+
if (preferredSpace >= desiredHeight) return preferredSide;
|
|
224
|
+
if (oppositeSpace >= desiredHeight) return preferredSide === "top" ? "bottom" : "top";
|
|
225
|
+
if (preferredSpace >= oppositeSpace) return preferredSide;
|
|
226
|
+
return preferredSide === "top" ? "bottom" : "top";
|
|
227
|
+
}
|
|
228
|
+
function horizontalAnchor(anchor, width, align) {
|
|
229
|
+
if (align === "center") return anchor.left + anchor.width / 2 - width / 2;
|
|
230
|
+
if (align === "end") return anchor.right - width;
|
|
231
|
+
return anchor.left;
|
|
232
|
+
}
|
|
233
|
+
function computePopoverLayout(anchor, viewport, opts = {}) {
|
|
234
|
+
const margin = opts.margin ?? 12;
|
|
235
|
+
const offset = opts.offset ?? 8;
|
|
236
|
+
const availableWidth = Math.max(80, viewport.width - margin * 2);
|
|
237
|
+
const requestedMinWidth = Math.max(1, opts.minWidth ?? DEFAULT_MIN_WIDTH);
|
|
238
|
+
const requestedMaxWidth = Math.max(requestedMinWidth, opts.maxWidth ?? DEFAULT_MAX_WIDTH);
|
|
239
|
+
const maxWidth = Math.min(requestedMaxWidth, availableWidth);
|
|
240
|
+
const minWidth = Math.min(requestedMinWidth, maxWidth);
|
|
241
|
+
const preferredSide = opts.side ?? "top";
|
|
242
|
+
const align = opts.align ?? "start";
|
|
243
|
+
const availableTop = Math.max(0, anchor.top - margin - offset);
|
|
244
|
+
const availableBottom = Math.max(0, viewport.height - anchor.bottom - margin - offset);
|
|
245
|
+
const availableHeight = Math.max(80, viewport.height - margin * 2);
|
|
246
|
+
const minHeight = Math.min(Math.max(80, opts.minHeight ?? DEFAULT_MIN_HEIGHT), availableHeight);
|
|
247
|
+
const requestedMaxHeight = Math.max(minHeight, opts.maxHeight ?? DEFAULT_MAX_HEIGHT);
|
|
248
|
+
const fallbackHeight = Math.min(260, requestedMaxHeight);
|
|
249
|
+
const contentHeight = Math.max(opts.contentHeightHint ?? 0, fallbackHeight);
|
|
250
|
+
const desiredHeight = clamp(contentHeight, minHeight, Math.min(requestedMaxHeight, availableHeight));
|
|
251
|
+
const side = chooseSide(preferredSide, availableTop, availableBottom, desiredHeight);
|
|
252
|
+
const sideSpace = side === "top" ? availableTop : availableBottom;
|
|
253
|
+
const maxHeight = Math.min(
|
|
254
|
+
requestedMaxHeight,
|
|
255
|
+
Math.max(80, sideSpace || Math.max(availableTop, availableBottom)),
|
|
256
|
+
availableHeight
|
|
257
|
+
);
|
|
258
|
+
const positionedHeight = clamp(desiredHeight, 40, maxHeight);
|
|
259
|
+
const width = align === "stretch" ? clamp(anchor.width, minWidth, maxWidth) : maxWidth;
|
|
260
|
+
const left = clamp(horizontalAnchor(anchor, width, align), margin, viewport.width - margin - width);
|
|
261
|
+
const desiredTop = side === "top" ? anchor.top - offset - positionedHeight : anchor.bottom + offset;
|
|
262
|
+
const top = clamp(desiredTop, margin, viewport.height - margin - positionedHeight);
|
|
263
|
+
return { side, left, top, width, maxHeight };
|
|
264
|
+
}
|
|
265
|
+
var openPanels = /* @__PURE__ */ new Map();
|
|
266
|
+
function rectFromElement(el3) {
|
|
267
|
+
const r = el3.getBoundingClientRect();
|
|
268
|
+
return { left: r.left, top: r.top, right: r.right, bottom: r.bottom, width: r.width, height: r.height };
|
|
269
|
+
}
|
|
270
|
+
function resolveAnchor(state) {
|
|
271
|
+
if (state.anchor instanceof Element) {
|
|
272
|
+
if (!state.anchor.isConnected) return null;
|
|
273
|
+
return rectFromElement(state.anchor);
|
|
274
|
+
}
|
|
275
|
+
return state.anchor;
|
|
276
|
+
}
|
|
277
|
+
function relayout(state) {
|
|
278
|
+
const anchor = resolveAnchor(state);
|
|
279
|
+
if (!anchor) {
|
|
280
|
+
closePopover(state.panel);
|
|
281
|
+
return;
|
|
282
|
+
}
|
|
283
|
+
const layout = computePopoverLayout(
|
|
284
|
+
anchor,
|
|
285
|
+
{ width: window.innerWidth, height: window.innerHeight },
|
|
286
|
+
state.opts
|
|
287
|
+
);
|
|
288
|
+
const { panel } = state;
|
|
289
|
+
panel.style.left = `${layout.left}px`;
|
|
290
|
+
panel.style.top = `${layout.top}px`;
|
|
291
|
+
panel.style.width = `${layout.width}px`;
|
|
292
|
+
panel.style.maxHeight = `${layout.maxHeight}px`;
|
|
293
|
+
panel.dataset.side = layout.side;
|
|
294
|
+
panel.style.visibility = state.prevVisibility;
|
|
295
|
+
}
|
|
296
|
+
function onWindowChange() {
|
|
297
|
+
openPanels.forEach(relayout);
|
|
298
|
+
}
|
|
299
|
+
function onOutsidePointerDown(e) {
|
|
300
|
+
const target = e.target;
|
|
301
|
+
if (!(target instanceof Node)) return;
|
|
302
|
+
openPanels.forEach((state) => {
|
|
303
|
+
if (state.panel.contains(target)) return;
|
|
304
|
+
if (state.anchor instanceof Element && state.anchor.contains(target)) return;
|
|
305
|
+
closePopover(state.panel);
|
|
306
|
+
});
|
|
307
|
+
}
|
|
308
|
+
function onKeyDown(e) {
|
|
309
|
+
if (e.key !== "Escape") return;
|
|
310
|
+
const states = Array.from(openPanels.values());
|
|
311
|
+
const top = states[states.length - 1];
|
|
312
|
+
if (top) {
|
|
313
|
+
e.preventDefault();
|
|
314
|
+
closePopover(top.panel);
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
function openPopover(panel, options) {
|
|
318
|
+
if (typeof document === "undefined") return;
|
|
319
|
+
if (openPanels.has(panel)) closePopover(panel);
|
|
320
|
+
const { anchor, onClose, ...layoutOpts } = options;
|
|
321
|
+
const placeholder = document.createComment("popover-anchor");
|
|
322
|
+
panel.parentNode?.insertBefore(placeholder, panel);
|
|
323
|
+
const wasHidden = panel.hidden;
|
|
324
|
+
const prevVisibility = panel.style.visibility;
|
|
325
|
+
const state = {
|
|
326
|
+
panel,
|
|
327
|
+
anchor,
|
|
328
|
+
opts: layoutOpts,
|
|
329
|
+
onClose,
|
|
330
|
+
placeholder,
|
|
331
|
+
wasHidden,
|
|
332
|
+
prevVisibility
|
|
333
|
+
};
|
|
334
|
+
openPanels.set(panel, state);
|
|
335
|
+
document.body.appendChild(panel);
|
|
336
|
+
panel.hidden = false;
|
|
337
|
+
panel.style.visibility = "hidden";
|
|
338
|
+
relayout(state);
|
|
339
|
+
if (openPanels.size === 1) {
|
|
340
|
+
window.addEventListener("resize", onWindowChange);
|
|
341
|
+
window.addEventListener("scroll", onWindowChange, true);
|
|
342
|
+
document.addEventListener("pointerdown", onOutsidePointerDown, true);
|
|
343
|
+
window.addEventListener("keydown", onKeyDown);
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
function closePopover(panel) {
|
|
347
|
+
if (typeof document === "undefined") return;
|
|
348
|
+
const state = openPanels.get(panel);
|
|
349
|
+
if (!state) return;
|
|
350
|
+
openPanels.delete(panel);
|
|
351
|
+
panel.style.visibility = state.prevVisibility;
|
|
352
|
+
if (typeof state.wasHidden === "string") panel.setAttribute("hidden", state.wasHidden);
|
|
353
|
+
else panel.hidden = state.wasHidden;
|
|
354
|
+
panel.style.left = "";
|
|
355
|
+
panel.style.top = "";
|
|
356
|
+
panel.style.width = "";
|
|
357
|
+
panel.style.maxHeight = "";
|
|
358
|
+
delete panel.dataset.side;
|
|
359
|
+
if (state.placeholder.parentNode) {
|
|
360
|
+
state.placeholder.parentNode.insertBefore(panel, state.placeholder);
|
|
361
|
+
state.placeholder.remove();
|
|
362
|
+
}
|
|
363
|
+
if (openPanels.size === 0) {
|
|
364
|
+
window.removeEventListener("resize", onWindowChange);
|
|
365
|
+
window.removeEventListener("scroll", onWindowChange, true);
|
|
366
|
+
document.removeEventListener("pointerdown", onOutsidePointerDown, true);
|
|
367
|
+
window.removeEventListener("keydown", onKeyDown);
|
|
368
|
+
}
|
|
369
|
+
state.onClose?.();
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
// src/behaviors/ai-panel.ts
|
|
373
|
+
function el(tag, className, text) {
|
|
374
|
+
const node = document.createElement(tag);
|
|
375
|
+
if (className) node.className = className;
|
|
376
|
+
if (text !== void 0) node.textContent = text;
|
|
377
|
+
return node;
|
|
378
|
+
}
|
|
379
|
+
function buildTodoHead(done, total) {
|
|
380
|
+
const head = el("div", "ai-todo-head");
|
|
381
|
+
head.appendChild(el("span", "ai-todo-progress", `${done}/${total}`));
|
|
382
|
+
const bar = el("div", "ai-todo-bar");
|
|
383
|
+
bar.setAttribute("role", "progressbar");
|
|
384
|
+
bar.setAttribute("aria-valuemin", "0");
|
|
385
|
+
bar.setAttribute("aria-valuemax", String(total));
|
|
386
|
+
bar.setAttribute("aria-valuenow", String(done));
|
|
387
|
+
bar.setAttribute("aria-label", "\u4EFB\u52A1\u8FDB\u5EA6");
|
|
388
|
+
const fill = document.createElement("i");
|
|
389
|
+
fill.style.width = total > 0 ? `${Math.round(done / total * 100)}%` : "0%";
|
|
390
|
+
bar.appendChild(fill);
|
|
391
|
+
head.appendChild(bar);
|
|
392
|
+
return head;
|
|
393
|
+
}
|
|
394
|
+
function buildTodoItem(item, index) {
|
|
395
|
+
const row = el("div", `ai-todo-item is-${item.status}`);
|
|
396
|
+
row.dataset.index = String(index);
|
|
397
|
+
row.appendChild(el("span", "ai-item-status"));
|
|
398
|
+
row.appendChild(el("span", "ai-todo-text", item.content));
|
|
399
|
+
row.appendChild(el("span", "ai-todo-active", item.activeForm ?? item.content));
|
|
400
|
+
return row;
|
|
401
|
+
}
|
|
402
|
+
function renderAiTodo(elm, items) {
|
|
403
|
+
if (typeof document === "undefined") return;
|
|
404
|
+
elm.textContent = "";
|
|
405
|
+
const root = el("div", "ai-todo");
|
|
406
|
+
const list = Array.isArray(items) ? items : [];
|
|
407
|
+
const done = list.filter((i) => i.status === "done").length;
|
|
408
|
+
root.appendChild(buildTodoHead(done, list.length));
|
|
409
|
+
list.forEach((item, i) => root.appendChild(buildTodoItem(item, i)));
|
|
410
|
+
if (list.length > 0 && done === list.length) root.classList.add("is-complete");
|
|
411
|
+
elm.appendChild(root);
|
|
412
|
+
}
|
|
413
|
+
function renderAiUsage(elm, usage, opts = {}) {
|
|
414
|
+
if (typeof document === "undefined") return;
|
|
415
|
+
const u = normalizeUsage(usage);
|
|
416
|
+
const segs = [
|
|
417
|
+
{ key: "input", className: "ai-usage-seg--input", label: "\u8F93\u5165", value: u.input ?? 0 },
|
|
418
|
+
{ key: "output", className: "ai-usage-seg--output", label: "\u8F93\u51FA", value: u.output ?? 0 },
|
|
419
|
+
{ key: "cacheRead", className: "ai-usage-seg--cache-read", label: "\u7F13\u5B58\u8BFB", value: u.cacheRead ?? 0 },
|
|
420
|
+
{ key: "cacheWrite", className: "ai-usage-seg--cache-write", label: "\u7F13\u5B58\u5199", value: u.cacheWrite ?? 0 },
|
|
421
|
+
{ key: "reasoning", className: "ai-usage-seg--reasoning", label: "\u63A8\u7406", value: u.reasoning ?? 0 }
|
|
422
|
+
];
|
|
423
|
+
const activeSegs = segs.filter((s) => s.value > 0);
|
|
424
|
+
const sum2 = activeSegs.reduce((acc, s) => acc + s.value, 0);
|
|
425
|
+
const total = u.total ?? sum2;
|
|
426
|
+
elm.textContent = "";
|
|
427
|
+
const root = el("div", "ai-usage");
|
|
428
|
+
const bar = el("div", "ai-usage-bar");
|
|
429
|
+
const summary = activeSegs.length > 0 ? activeSegs.map((s) => `${s.label} ${formatTokens(s.value)}`).join("\uFF0C") : "\u6682\u65E0\u7528\u91CF";
|
|
430
|
+
bar.setAttribute("role", "img");
|
|
431
|
+
bar.setAttribute("aria-label", `\u7528\u91CF\uFF1A${summary}`);
|
|
432
|
+
for (const s of activeSegs) {
|
|
433
|
+
const seg = document.createElement("i");
|
|
434
|
+
seg.className = `ai-usage-seg ${s.className}`;
|
|
435
|
+
seg.style.width = sum2 > 0 ? `${s.value / sum2 * 100}%` : "0%";
|
|
436
|
+
bar.appendChild(seg);
|
|
437
|
+
}
|
|
438
|
+
root.appendChild(bar);
|
|
439
|
+
if (activeSegs.length > 0) {
|
|
440
|
+
const legend = el("div", "ai-usage-legend");
|
|
441
|
+
for (const s of activeSegs) {
|
|
442
|
+
const item = el("span", "ai-usage-legend-item");
|
|
443
|
+
const dot = document.createElement("i");
|
|
444
|
+
dot.className = `ai-usage-dot ${s.className}`;
|
|
445
|
+
dot.setAttribute("aria-hidden", "true");
|
|
446
|
+
item.appendChild(dot);
|
|
447
|
+
item.appendChild(el("span", "ai-usage-legend-label", s.label));
|
|
448
|
+
item.appendChild(el("span", "ai-usage-legend-value", formatTokens(s.value)));
|
|
449
|
+
legend.appendChild(item);
|
|
450
|
+
}
|
|
451
|
+
root.appendChild(legend);
|
|
452
|
+
}
|
|
453
|
+
const totalParts = [formatTokens(total)];
|
|
454
|
+
if (typeof opts.total === "number" && opts.total > 0) {
|
|
455
|
+
const pct = Math.min(100, Math.round(total / opts.total * 100));
|
|
456
|
+
totalParts.push(` / ${formatTokens(opts.total)} \xB7 ${pct}%`);
|
|
457
|
+
}
|
|
458
|
+
if (typeof opts.cost === "number" && Number.isFinite(opts.cost)) {
|
|
459
|
+
totalParts.push(` \xB7 $${opts.cost.toFixed(4)}`);
|
|
460
|
+
}
|
|
461
|
+
root.appendChild(el("div", "ai-usage-total", totalParts.join("")));
|
|
462
|
+
elm.appendChild(root);
|
|
463
|
+
}
|
|
464
|
+
function renderAiUsageRing(elm, usage, opts = {}) {
|
|
465
|
+
if (typeof document === "undefined") return;
|
|
466
|
+
const u = normalizeUsage(usage);
|
|
467
|
+
const used = u.total ?? (u.input ?? 0) + (u.output ?? 0) + (u.cacheRead ?? 0) + (u.cacheWrite ?? 0) + (u.reasoning ?? 0);
|
|
468
|
+
const hasLimit = typeof opts.total === "number" && opts.total > 0;
|
|
469
|
+
const pct = hasLimit ? Math.min(100, Math.max(0, Math.round(used / opts.total * 100))) : 0;
|
|
470
|
+
elm.textContent = "";
|
|
471
|
+
const btn = el("button", "ai-usage-ring");
|
|
472
|
+
btn.type = "button";
|
|
473
|
+
btn.classList.add(pct >= 85 ? "is-hot" : pct >= 60 ? "is-warn" : "is-ok");
|
|
474
|
+
btn.setAttribute("aria-haspopup", "true");
|
|
475
|
+
btn.setAttribute(
|
|
476
|
+
"aria-label",
|
|
477
|
+
hasLimit ? `\u4E0A\u4E0B\u6587\u7A97\u53E3\u5360\u7528 ${pct}%\uFF0C\u70B9\u51FB\u67E5\u770B\u8BE6\u60C5` : `\u5DF2\u7528 ${formatTokens(used)}\uFF0C\u70B9\u51FB\u67E5\u770B\u8BE6\u60C5`
|
|
478
|
+
);
|
|
479
|
+
const NS = "http://www.w3.org/2000/svg";
|
|
480
|
+
const svg2 = document.createElementNS(NS, "svg");
|
|
481
|
+
svg2.setAttribute("viewBox", "0 0 36 36");
|
|
482
|
+
svg2.setAttribute("aria-hidden", "true");
|
|
483
|
+
const track = document.createElementNS(NS, "circle");
|
|
484
|
+
track.setAttribute("class", "ai-usage-ring-track");
|
|
485
|
+
const fill = document.createElementNS(NS, "circle");
|
|
486
|
+
fill.setAttribute("class", "ai-usage-ring-fill");
|
|
487
|
+
fill.setAttribute("stroke-dasharray", `${pct} 100`);
|
|
488
|
+
for (const c of [track, fill]) {
|
|
489
|
+
c.setAttribute("cx", "18");
|
|
490
|
+
c.setAttribute("cy", "18");
|
|
491
|
+
c.setAttribute("r", "15.9155");
|
|
492
|
+
svg2.appendChild(c);
|
|
493
|
+
}
|
|
494
|
+
btn.appendChild(svg2);
|
|
495
|
+
btn.appendChild(el("span", "ai-usage-ring-pct", hasLimit ? `${pct}%` : formatTokens(used)));
|
|
496
|
+
let panel = null;
|
|
497
|
+
const closePanel = () => {
|
|
498
|
+
if (!panel) return;
|
|
499
|
+
const p = panel;
|
|
500
|
+
panel = null;
|
|
501
|
+
closePopover(p);
|
|
502
|
+
p.remove();
|
|
503
|
+
};
|
|
504
|
+
btn.addEventListener("click", () => {
|
|
505
|
+
if (panel) {
|
|
506
|
+
closePanel();
|
|
507
|
+
return;
|
|
508
|
+
}
|
|
509
|
+
const p = el("div", "popover ai-usage-popover");
|
|
510
|
+
p.hidden = true;
|
|
511
|
+
p.appendChild(el("div", "ai-usage-popover-title", opts.title ?? "\u4E0A\u4E0B\u6587\u7A97\u53E3"));
|
|
512
|
+
const body = el("div", "ai-usage-popover-body");
|
|
513
|
+
renderAiUsage(body, usage, opts);
|
|
514
|
+
p.appendChild(body);
|
|
515
|
+
openPopover(p, {
|
|
516
|
+
anchor: btn,
|
|
517
|
+
side: "bottom",
|
|
518
|
+
align: "end",
|
|
519
|
+
onClose: () => {
|
|
520
|
+
p.remove();
|
|
521
|
+
if (panel === p) panel = null;
|
|
522
|
+
}
|
|
523
|
+
});
|
|
524
|
+
panel = p;
|
|
525
|
+
});
|
|
526
|
+
elm.appendChild(btn);
|
|
527
|
+
}
|
|
528
|
+
var svgWrap = (inner) => `<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">${inner}</svg>`;
|
|
529
|
+
var FILE_OUTLINE = '<path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/><path d="M14 2v6h6"/>';
|
|
530
|
+
var ICON_FILE2 = svgWrap(FILE_OUTLINE);
|
|
531
|
+
var ICON_FILE_CODE = svgWrap(`${FILE_OUTLINE}<path d="m10 13-2 2 2 2"/><path d="m14 17 2-2-2-2"/>`);
|
|
532
|
+
var ICON_BRACES = svgWrap(
|
|
533
|
+
'<path d="M8 3H7a2 2 0 0 0-2 2v4a2 2 0 0 1-2 2 2 2 0 0 1 2 2v4a2 2 0 0 0 2 2h1"/><path d="M16 3h1a2 2 0 0 1 2 2v4a2 2 0 0 0 2 2 2 2 0 0 0-2 2v4a2 2 0 0 1-2 2h-1"/>'
|
|
534
|
+
);
|
|
535
|
+
var ICON_FILE_TEXT2 = svgWrap(`${FILE_OUTLINE}<path d="M16 13H8"/><path d="M16 17H8"/>`);
|
|
536
|
+
var ICON_IMAGE = svgWrap(
|
|
537
|
+
'<rect width="18" height="18" x="3" y="3" rx="2"/><circle cx="9" cy="9" r="2"/><path d="m21 15-3.1-3.1a2 2 0 0 0-2.8 0L6 21"/>'
|
|
538
|
+
);
|
|
539
|
+
var ICON_SERVER = svgWrap(
|
|
540
|
+
'<rect width="20" height="8" x="2" y="2" rx="2"/><rect width="20" height="8" x="2" y="14" rx="2"/><path d="M6 6h.01"/><path d="M6 18h.01"/>'
|
|
541
|
+
);
|
|
542
|
+
var ICON_SPARKLES = svgWrap(
|
|
543
|
+
'<path d="m12 3-1.9 5.8a2 2 0 0 1-1.3 1.3L3 12l5.8 1.9a2 2 0 0 1 1.3 1.3L12 21l1.9-5.8a2 2 0 0 1 1.3-1.3L21 12l-5.8-1.9a2 2 0 0 1-1.3-1.3z"/>'
|
|
544
|
+
);
|
|
545
|
+
|
|
546
|
+
// src/behaviors/ai-chat.ts
|
|
547
|
+
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>';
|
|
548
|
+
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>';
|
|
549
|
+
function createAiStream(el3) {
|
|
550
|
+
const reasoning = el3.closest(".ai-reasoning");
|
|
551
|
+
const startedAt = Date.now();
|
|
552
|
+
let state = "streaming";
|
|
553
|
+
el3.classList.add("is-streaming");
|
|
554
|
+
el3.setAttribute("aria-busy", "true");
|
|
555
|
+
reasoning?.classList.add("is-streaming");
|
|
556
|
+
const settle = (finished) => {
|
|
557
|
+
if (state !== "streaming") return;
|
|
558
|
+
state = finished ? "done" : "cancelled";
|
|
559
|
+
el3.classList.remove("is-streaming");
|
|
560
|
+
el3.removeAttribute("aria-busy");
|
|
561
|
+
if (!reasoning) return;
|
|
562
|
+
reasoning.classList.remove("is-streaming");
|
|
563
|
+
if (!finished) return;
|
|
564
|
+
const head = reasoning.querySelector(".ai-reasoning-head");
|
|
565
|
+
const body = reasoning.querySelector(".ai-reasoning-body");
|
|
566
|
+
head?.setAttribute("aria-expanded", "false");
|
|
567
|
+
if (body) body.hidden = true;
|
|
568
|
+
const time = reasoning.querySelector(".ai-reasoning-time");
|
|
569
|
+
if (time) time.textContent = formatDuration(Date.now() - startedAt);
|
|
570
|
+
};
|
|
571
|
+
return {
|
|
572
|
+
append(text) {
|
|
573
|
+
if (state !== "streaming" || !text) return;
|
|
574
|
+
el3.textContent += text;
|
|
575
|
+
},
|
|
576
|
+
done() {
|
|
577
|
+
settle(true);
|
|
578
|
+
},
|
|
579
|
+
cancel() {
|
|
580
|
+
settle(false);
|
|
581
|
+
},
|
|
582
|
+
fail() {
|
|
583
|
+
if (state !== "streaming") return;
|
|
584
|
+
state = "error";
|
|
585
|
+
el3.classList.remove("is-streaming");
|
|
586
|
+
el3.removeAttribute("aria-busy");
|
|
587
|
+
el3.classList.add("is-error");
|
|
588
|
+
reasoning?.classList.remove("is-streaming");
|
|
589
|
+
}
|
|
590
|
+
};
|
|
591
|
+
}
|
|
592
|
+
var ROLE_AVATAR = {
|
|
593
|
+
user: "\u6211",
|
|
594
|
+
assistant: "AI",
|
|
595
|
+
system: "\u7CFB",
|
|
596
|
+
tool: "\u5DE5"
|
|
597
|
+
};
|
|
598
|
+
function safeHref(uri) {
|
|
599
|
+
const s = uri.trim().toLowerCase();
|
|
600
|
+
if (s.startsWith("javascript:") || s.startsWith("data:text/html")) return void 0;
|
|
601
|
+
return uri;
|
|
602
|
+
}
|
|
603
|
+
function el2(tag, className, text) {
|
|
604
|
+
const node = document.createElement(tag);
|
|
605
|
+
if (className) node.className = className;
|
|
606
|
+
if (text != null) node.textContent = text;
|
|
607
|
+
return node;
|
|
608
|
+
}
|
|
609
|
+
function renderAiContentPart(part) {
|
|
610
|
+
if (part.type === "text") {
|
|
611
|
+
const span = el2("span", "ai-msg-text");
|
|
612
|
+
span.textContent = part.text;
|
|
613
|
+
if (part.state === "streaming") span.classList.add("is-streaming");
|
|
614
|
+
return span;
|
|
615
|
+
}
|
|
616
|
+
if (part.type === "image") {
|
|
617
|
+
const src = aiContentUrl(part);
|
|
618
|
+
if (!src) return void 0;
|
|
619
|
+
const img = document.createElement("img");
|
|
620
|
+
img.className = "ai-msg-media ai-msg-media--image";
|
|
621
|
+
img.alt = part.alt ?? "";
|
|
622
|
+
img.setAttribute("loading", "lazy");
|
|
623
|
+
img.setAttribute("src", src);
|
|
624
|
+
return img;
|
|
625
|
+
}
|
|
626
|
+
if (part.type === "audio") {
|
|
627
|
+
const src = aiContentUrl(part);
|
|
628
|
+
if (!src) return void 0;
|
|
629
|
+
const audio = document.createElement("audio");
|
|
630
|
+
audio.className = "ai-msg-media ai-msg-media--audio";
|
|
631
|
+
audio.controls = true;
|
|
632
|
+
audio.setAttribute("src", src);
|
|
633
|
+
return audio;
|
|
634
|
+
}
|
|
635
|
+
if (part.type === "video") {
|
|
636
|
+
const src = aiContentUrl(part);
|
|
637
|
+
if (!src) return void 0;
|
|
638
|
+
const video = document.createElement("video");
|
|
639
|
+
video.className = "ai-msg-media ai-msg-media--video";
|
|
640
|
+
video.controls = true;
|
|
641
|
+
video.setAttribute("playsinline", "");
|
|
642
|
+
video.setAttribute("src", src);
|
|
643
|
+
return video;
|
|
644
|
+
}
|
|
645
|
+
if (part.type === "file") {
|
|
646
|
+
const chip = el2("span", "ai-msg-file");
|
|
647
|
+
chip.appendChild(el2("span", "ai-msg-file-name", part.filename ?? "\u6587\u4EF6"));
|
|
648
|
+
const href2 = part.url ? safeHref(part.url) : void 0;
|
|
649
|
+
if (href2 && /^(https?:|data:)/i.test(href2)) {
|
|
650
|
+
const a2 = document.createElement("a");
|
|
651
|
+
a2.className = "ai-msg-file-link";
|
|
652
|
+
a2.href = href2;
|
|
653
|
+
a2.download = part.filename ?? "";
|
|
654
|
+
a2.textContent = "\u4E0B\u8F7D";
|
|
655
|
+
chip.appendChild(a2);
|
|
656
|
+
}
|
|
657
|
+
return chip;
|
|
658
|
+
}
|
|
659
|
+
const a = document.createElement("a");
|
|
660
|
+
a.className = "ai-msg-link";
|
|
661
|
+
const href = safeHref(part.uri);
|
|
662
|
+
if (href) a.href = href;
|
|
663
|
+
a.textContent = part.name ?? part.uri;
|
|
664
|
+
a.title = part.uri;
|
|
665
|
+
return a;
|
|
666
|
+
}
|
|
667
|
+
function renderAiMessage(scrollEl, model) {
|
|
668
|
+
const row = el2("div", `ai-msg ai-msg--${model.role}`);
|
|
669
|
+
const avatar = el2("span", "ai-msg-avatar", ROLE_AVATAR[model.role] ?? "");
|
|
670
|
+
const body = el2("div", "ai-msg-body");
|
|
671
|
+
row.appendChild(avatar);
|
|
672
|
+
row.appendChild(body);
|
|
673
|
+
const parts = typeof model.content === "string" ? [{ type: "text", text: model.content }] : model.content;
|
|
674
|
+
for (const part of parts) {
|
|
675
|
+
const node = renderAiContentPart(part);
|
|
676
|
+
if (node) body.appendChild(node);
|
|
677
|
+
}
|
|
678
|
+
if (model.model) body.appendChild(el2("span", "ai-msg-model", model.model));
|
|
679
|
+
const metaEl = el2("div", "ai-msg-meta");
|
|
680
|
+
if (model.meta != null) metaEl.textContent = model.meta;
|
|
681
|
+
row.appendChild(metaEl);
|
|
682
|
+
const actions = el2("div", "ai-msg-actions");
|
|
683
|
+
const copyBtn = el2("button", "ai-msg-action");
|
|
684
|
+
copyBtn.type = "button";
|
|
685
|
+
copyBtn.setAttribute("data-ai-msg-action", "copy");
|
|
686
|
+
copyBtn.setAttribute("aria-label", "\u590D\u5236");
|
|
687
|
+
const copyIcon = svgIcon(ICON_COPY);
|
|
688
|
+
if (copyIcon) copyBtn.appendChild(copyIcon);
|
|
689
|
+
const retryBtn = el2("button", "ai-msg-action");
|
|
690
|
+
retryBtn.type = "button";
|
|
691
|
+
retryBtn.setAttribute("data-ai-msg-action", "retry");
|
|
692
|
+
retryBtn.setAttribute("aria-label", "\u91CD\u8BD5");
|
|
693
|
+
const retryIcon = svgIcon(ICON_RETRY);
|
|
694
|
+
if (retryIcon) retryBtn.appendChild(retryIcon);
|
|
695
|
+
actions.append(copyBtn, retryBtn);
|
|
696
|
+
row.appendChild(actions);
|
|
697
|
+
const handle = {
|
|
698
|
+
el: row,
|
|
699
|
+
body,
|
|
700
|
+
setMeta(meta) {
|
|
701
|
+
metaEl.textContent = meta;
|
|
702
|
+
},
|
|
703
|
+
setError(message) {
|
|
704
|
+
row.classList.add("ai-msg--error");
|
|
705
|
+
let errEl = row.querySelector(".ai-msg-error-text");
|
|
706
|
+
if (!errEl) {
|
|
707
|
+
errEl = el2("div", "ai-msg-error-text");
|
|
708
|
+
body.appendChild(errEl);
|
|
709
|
+
}
|
|
710
|
+
errEl.textContent = message;
|
|
711
|
+
},
|
|
712
|
+
stream() {
|
|
713
|
+
const span = el2("span", "ai-msg-stream-text");
|
|
714
|
+
body.appendChild(span);
|
|
715
|
+
return createAiStream(span);
|
|
716
|
+
}
|
|
717
|
+
};
|
|
718
|
+
if (model.error != null) handle.setError(model.error);
|
|
719
|
+
if (model.streaming) row.classList.add("is-streaming");
|
|
720
|
+
scrollEl.appendChild(row);
|
|
721
|
+
return handle;
|
|
722
|
+
}
|
|
723
|
+
|
|
724
|
+
// src/behaviors/ai-composer.ts
|
|
725
|
+
var MAX_ROWS_DEFAULT = 8;
|
|
726
|
+
var HISTORY_LIMIT = 50;
|
|
727
|
+
var CACHE_WARN_RATIO = 0.4;
|
|
728
|
+
var SEND_ICON = '<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="m22 2-7 20-4-9-9-4z"/><path d="M22 2 11 13"/></svg>';
|
|
729
|
+
var STOP_ICON = '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><rect x="6" y="6" width="12" height="12" rx="2"/></svg>';
|
|
730
|
+
var ATTACH_ICON = '<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="m21.44 11.05-9.19 9.19a6 6 0 0 1-8.49-8.49l8.57-8.57A4 4 0 1 1 18 8.84l-8.59 8.57a2 2 0 0 1-2.83-2.83l8.49-8.48"/></svg>';
|
|
731
|
+
var svgWrap2 = (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>`;
|
|
732
|
+
var ICON_CHEVRON = svgWrap2('<path d="m6 9 6 6 6-6"/>');
|
|
733
|
+
var ICON_CHECK = svgWrap2('<path d="M20 6 9 17l-5-5"/>');
|
|
734
|
+
var ICON_INFO = svgWrap2('<circle cx="12" cy="12" r="10"/><path d="M12 16v-4"/><path d="M12 8h.01"/>');
|
|
735
|
+
var ICON_FILE3 = svgWrap2(
|
|
736
|
+
'<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"/>'
|
|
737
|
+
);
|
|
738
|
+
var ICON_FOLDER = svgWrap2(
|
|
739
|
+
'<path d="M20 20a2 2 0 0 0 2-2V8a2 2 0 0 0-2-2h-7.9a2 2 0 0 1-1.69-.9L9.6 3.9A2 2 0 0 0 7.93 3H4a2 2 0 0 0-2 2v13a2 2 0 0 0 2 2Z"/>'
|
|
740
|
+
);
|
|
741
|
+
var ICON_DOC = svgWrap2(
|
|
742
|
+
'<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="M16 13H8"/><path d="M16 17H8"/>'
|
|
743
|
+
);
|
|
744
|
+
var ICON_BOT2 = svgWrap2(
|
|
745
|
+
'<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"/>'
|
|
746
|
+
);
|
|
747
|
+
var REF_KIND_LABEL = {
|
|
748
|
+
file: "\u6587\u4EF6",
|
|
749
|
+
folder: "\u6587\u4EF6\u5939",
|
|
750
|
+
doc: "\u6587\u6863",
|
|
751
|
+
agent: "\u4EE3\u7406"
|
|
752
|
+
};
|
|
753
|
+
var REF_KIND_ICON = {
|
|
754
|
+
file: ICON_FILE3,
|
|
755
|
+
folder: ICON_FOLDER,
|
|
756
|
+
doc: ICON_DOC,
|
|
757
|
+
agent: ICON_BOT2
|
|
758
|
+
};
|
|
759
|
+
var REF_KIND_ORDER = ["file", "folder", "doc", "agent"];
|
|
760
|
+
var RUNNING = /* @__PURE__ */ new WeakMap();
|
|
761
|
+
var QUEUES = /* @__PURE__ */ new WeakMap();
|
|
762
|
+
var MODELS = /* @__PURE__ */ new WeakMap();
|
|
763
|
+
var MODEL_CTX = /* @__PURE__ */ new WeakMap();
|
|
764
|
+
var COMMANDS = /* @__PURE__ */ new WeakMap();
|
|
765
|
+
var REF_SOURCES = /* @__PURE__ */ new WeakMap();
|
|
766
|
+
var ACTIVE_REFS = /* @__PURE__ */ new WeakMap();
|
|
767
|
+
var USAGE = /* @__PURE__ */ new WeakMap();
|
|
768
|
+
var TODO = /* @__PURE__ */ new WeakMap();
|
|
769
|
+
var HISTORIES = /* @__PURE__ */ new WeakMap();
|
|
770
|
+
var POPUP = /* @__PURE__ */ new WeakMap();
|
|
771
|
+
var SUBMITS = /* @__PURE__ */ new WeakMap();
|
|
772
|
+
function isBrowser() {
|
|
773
|
+
return typeof document !== "undefined" && typeof window !== "undefined";
|
|
774
|
+
}
|
|
775
|
+
function emit(target, name, detail) {
|
|
776
|
+
target.dispatchEvent(new CustomEvent(name, { detail, bubbles: true }));
|
|
777
|
+
}
|
|
778
|
+
function resolveComposer(el3) {
|
|
779
|
+
return el3.closest("[data-ai-composer]");
|
|
780
|
+
}
|
|
781
|
+
function getTextarea(composer) {
|
|
782
|
+
return composer.querySelector(".ai-composer-input");
|
|
783
|
+
}
|
|
784
|
+
function autosizeTa(ta) {
|
|
785
|
+
const composer = ta.closest("[data-ai-composer]");
|
|
786
|
+
const maxRows = Number(composer?.getAttribute("data-max-rows")) || MAX_ROWS_DEFAULT;
|
|
787
|
+
const cs = window.getComputedStyle(ta);
|
|
788
|
+
const lineHeight = parseFloat(cs.lineHeight) || 20;
|
|
789
|
+
const padY = (parseFloat(cs.paddingTop) || 0) + (parseFloat(cs.paddingBottom) || 0);
|
|
790
|
+
const minRows = Number(ta.getAttribute("rows")) || 1;
|
|
791
|
+
ta.style.height = "auto";
|
|
792
|
+
const minH = minRows * lineHeight + padY;
|
|
793
|
+
const maxH = maxRows * lineHeight + padY;
|
|
794
|
+
ta.style.height = `${Math.max(minH, Math.min(ta.scrollHeight, maxH))}px`;
|
|
795
|
+
}
|
|
796
|
+
function ensureIcons(composer) {
|
|
797
|
+
const sendBtn = composer.querySelector("[data-ai-send]");
|
|
798
|
+
if (sendBtn && !sendBtn.hasChildNodes()) {
|
|
799
|
+
const sendWrap = document.createElement("span");
|
|
800
|
+
sendWrap.className = "ai-composer-send-icon";
|
|
801
|
+
sendWrap.dataset.aiSendIcon = "send";
|
|
802
|
+
const stopWrap = document.createElement("span");
|
|
803
|
+
stopWrap.className = "ai-composer-send-icon";
|
|
804
|
+
stopWrap.dataset.aiSendIcon = "stop";
|
|
805
|
+
const sendSvg = svgIcon(SEND_ICON);
|
|
806
|
+
const stopSvg = svgIcon(STOP_ICON);
|
|
807
|
+
if (sendSvg) sendWrap.appendChild(sendSvg);
|
|
808
|
+
if (stopSvg) stopWrap.appendChild(stopSvg);
|
|
809
|
+
sendBtn.append(sendWrap, stopWrap);
|
|
810
|
+
}
|
|
811
|
+
const attachBtn = composer.querySelector("[data-ai-attach]");
|
|
812
|
+
if (attachBtn && !attachBtn.hasChildNodes()) {
|
|
813
|
+
const icon = svgIcon(ATTACH_ICON);
|
|
814
|
+
if (icon) attachBtn.appendChild(icon);
|
|
815
|
+
}
|
|
816
|
+
const sendIcon = sendBtn?.querySelector('[data-ai-send-icon="send"]') ?? null;
|
|
817
|
+
const stopIcon = sendBtn?.querySelector('[data-ai-send-icon="stop"]') ?? null;
|
|
818
|
+
return { sendBtn, sendIcon, stopIcon };
|
|
819
|
+
}
|
|
820
|
+
function makeChip(text, removable, ariaLabel) {
|
|
821
|
+
const chip = document.createElement("span");
|
|
822
|
+
chip.className = "ai-composer-chip";
|
|
823
|
+
const label = document.createElement("span");
|
|
824
|
+
label.className = "ai-composer-chip-text";
|
|
825
|
+
label.textContent = text;
|
|
826
|
+
chip.appendChild(label);
|
|
827
|
+
if (removable) {
|
|
828
|
+
const remove = document.createElement("button");
|
|
829
|
+
remove.type = "button";
|
|
830
|
+
remove.className = "ai-composer-chip-remove";
|
|
831
|
+
remove.setAttribute("aria-label", ariaLabel);
|
|
832
|
+
remove.textContent = "\xD7";
|
|
833
|
+
chip.appendChild(remove);
|
|
834
|
+
}
|
|
835
|
+
return chip;
|
|
836
|
+
}
|
|
837
|
+
function renderQueueChips(composer) {
|
|
838
|
+
const queueEl = composer.querySelector(".ai-composer-queue");
|
|
839
|
+
if (!queueEl) return;
|
|
840
|
+
const queue = QUEUES.get(composer) ?? [];
|
|
841
|
+
queueEl.replaceChildren(...queue.map((text, i) => makeChip(text, true, `\u79FB\u9664\u6392\u961F\u6D88\u606F ${i + 1}`)));
|
|
842
|
+
queueEl.hidden = queue.length === 0;
|
|
843
|
+
}
|
|
844
|
+
function setComposerRunning(el3, running) {
|
|
845
|
+
const composer = resolveComposer(el3);
|
|
846
|
+
if (!composer) return;
|
|
847
|
+
RUNNING.set(composer, running);
|
|
848
|
+
composer.classList.toggle("is-running", running);
|
|
849
|
+
if (running) {
|
|
850
|
+
composer.setAttribute("aria-busy", "true");
|
|
851
|
+
} else {
|
|
852
|
+
composer.removeAttribute("aria-busy");
|
|
853
|
+
}
|
|
854
|
+
const sendBtn = composer.querySelector("[data-ai-send]");
|
|
855
|
+
if (!sendBtn) return;
|
|
856
|
+
sendBtn.classList.toggle("is-stop", running);
|
|
857
|
+
sendBtn.setAttribute("aria-label", running ? "\u505C\u6B62\u751F\u6210" : "\u53D1\u9001");
|
|
858
|
+
const sendIcon = sendBtn.querySelector('[data-ai-send-icon="send"]');
|
|
859
|
+
const stopIcon = sendBtn.querySelector('[data-ai-send-icon="stop"]');
|
|
860
|
+
if (sendIcon) sendIcon.hidden = running;
|
|
861
|
+
if (stopIcon) stopIcon.hidden = !running;
|
|
862
|
+
}
|
|
863
|
+
function providerMark(provider, badgeClass) {
|
|
864
|
+
const slot = document.createElement("span");
|
|
865
|
+
slot.className = badgeClass;
|
|
866
|
+
const svg2 = provider.icon ? svgIcon(provider.icon) : null;
|
|
867
|
+
if (svg2) {
|
|
868
|
+
slot.appendChild(svg2);
|
|
869
|
+
} else {
|
|
870
|
+
const badge = document.createElement("span");
|
|
871
|
+
badge.className = "ai-composer-model-badge";
|
|
872
|
+
badge.textContent = (provider.label.trim()[0] ?? "?").toUpperCase();
|
|
873
|
+
slot.appendChild(badge);
|
|
874
|
+
}
|
|
875
|
+
return slot;
|
|
876
|
+
}
|
|
877
|
+
function buildModelButton(composer) {
|
|
878
|
+
const btn = document.createElement("button");
|
|
879
|
+
btn.type = "button";
|
|
880
|
+
btn.className = "ai-composer-model";
|
|
881
|
+
btn.setAttribute("data-ai-model-open", "");
|
|
882
|
+
btn.setAttribute("aria-haspopup", "listbox");
|
|
883
|
+
btn.setAttribute("aria-expanded", "false");
|
|
884
|
+
btn.addEventListener("click", () => {
|
|
885
|
+
if (POPUP.get(composer)) closePopup(composer, false);
|
|
886
|
+
else openModelPopup(composer);
|
|
887
|
+
});
|
|
888
|
+
return btn;
|
|
889
|
+
}
|
|
890
|
+
function updateModelButton(composer) {
|
|
891
|
+
const cfg = MODELS.get(composer);
|
|
892
|
+
const btn = composer.querySelector("[data-ai-model-open]");
|
|
893
|
+
if (!cfg || !btn) return;
|
|
894
|
+
const provider = cfg.providers.find((p) => p.id === cfg.current.provider) ?? cfg.providers[0];
|
|
895
|
+
if (!provider) return;
|
|
896
|
+
const model = provider.models.find((m) => m.id === cfg.current.model) ?? provider.models[0];
|
|
897
|
+
if (!model) return;
|
|
898
|
+
const iconSlot = btn.querySelector(".ai-composer-model-icon") ?? (() => {
|
|
899
|
+
const slot = document.createElement("span");
|
|
900
|
+
slot.className = "ai-composer-model-icon";
|
|
901
|
+
btn.prepend(slot);
|
|
902
|
+
return slot;
|
|
903
|
+
})();
|
|
904
|
+
iconSlot.replaceChildren(providerMark(provider, "ai-composer-model-provider-icon"));
|
|
905
|
+
let name = btn.querySelector(".ai-composer-model-name");
|
|
906
|
+
if (!name) {
|
|
907
|
+
name = document.createElement("span");
|
|
908
|
+
name.className = "ai-composer-model-name";
|
|
909
|
+
btn.insertBefore(name, btn.querySelector(".ai-composer-model-chevron"));
|
|
910
|
+
}
|
|
911
|
+
name.textContent = model.label;
|
|
912
|
+
btn.setAttribute("aria-label", `\u6A21\u578B ${model.label}\uFF08${provider.label}\uFF09`);
|
|
913
|
+
if (!btn.querySelector(".ai-composer-model-chevron")) {
|
|
914
|
+
const chev = svgIcon(ICON_CHEVRON);
|
|
915
|
+
if (chev) {
|
|
916
|
+
const wrap = document.createElement("span");
|
|
917
|
+
wrap.className = "ai-composer-model-chevron";
|
|
918
|
+
wrap.appendChild(chev);
|
|
919
|
+
btn.appendChild(wrap);
|
|
920
|
+
}
|
|
921
|
+
}
|
|
922
|
+
}
|
|
923
|
+
function syncToolbar(composer) {
|
|
924
|
+
const box = composer.querySelector(".ai-composer-box");
|
|
925
|
+
if (!box || !getTextarea(composer)) return;
|
|
926
|
+
const modelsCfg = MODELS.get(composer);
|
|
927
|
+
const usageCfg = USAGE.get(composer);
|
|
928
|
+
if (!modelsCfg && !usageCfg) return;
|
|
929
|
+
let bar = composer.querySelector(".ai-composer-toolbar");
|
|
930
|
+
let modelBtn = composer.querySelector("[data-ai-model-open]");
|
|
931
|
+
if (modelsCfg && !modelBtn) {
|
|
932
|
+
modelBtn = buildModelButton(composer);
|
|
933
|
+
modelBtn.dataset.aiModelOpen = "auto";
|
|
934
|
+
updateModelButton(composer);
|
|
935
|
+
}
|
|
936
|
+
if (!modelsCfg && modelBtn?.dataset.aiModelOpen === "auto") {
|
|
937
|
+
modelBtn.remove();
|
|
938
|
+
modelBtn = null;
|
|
939
|
+
}
|
|
940
|
+
let usageMount = composer.querySelector(".ai-composer-usage");
|
|
941
|
+
if (usageCfg && !usageMount) {
|
|
942
|
+
usageMount = document.createElement("span");
|
|
943
|
+
usageMount.className = "ai-composer-usage";
|
|
944
|
+
usageMount.dataset.aiUsage = "auto";
|
|
945
|
+
}
|
|
946
|
+
if (!usageCfg && usageMount?.dataset.aiUsage === "auto") {
|
|
947
|
+
usageMount.remove();
|
|
948
|
+
usageMount = null;
|
|
949
|
+
}
|
|
950
|
+
const attach = composer.querySelector("[data-ai-attach]");
|
|
951
|
+
const send = composer.querySelector("[data-ai-send]");
|
|
952
|
+
if (bar) {
|
|
953
|
+
if (modelBtn && modelBtn.parentElement !== bar) bar.insertBefore(modelBtn, bar.firstChild);
|
|
954
|
+
if (usageMount && usageMount.parentElement !== bar) {
|
|
955
|
+
bar.insertBefore(usageMount, attach && attach.parentElement === bar ? attach : null);
|
|
956
|
+
}
|
|
957
|
+
} else {
|
|
958
|
+
bar = document.createElement("div");
|
|
959
|
+
bar.className = "ai-composer-toolbar";
|
|
960
|
+
bar.appendChild(modelBtn ?? document.createComment("model"));
|
|
961
|
+
const spacer = document.createElement("span");
|
|
962
|
+
spacer.className = "ai-composer-spacer";
|
|
963
|
+
bar.appendChild(spacer);
|
|
964
|
+
if (usageMount) bar.appendChild(usageMount);
|
|
965
|
+
if (attach) bar.appendChild(attach);
|
|
966
|
+
if (send) bar.appendChild(send);
|
|
967
|
+
box.appendChild(bar);
|
|
968
|
+
}
|
|
969
|
+
const legacy = composer.querySelector(".ai-composer-actions");
|
|
970
|
+
if (legacy && legacy.childElementCount === 0 && !(legacy.textContent ?? "").trim()) {
|
|
971
|
+
legacy.remove();
|
|
972
|
+
}
|
|
973
|
+
}
|
|
974
|
+
function refreshUsageRing(composer) {
|
|
975
|
+
const cfg = USAGE.get(composer);
|
|
976
|
+
const mount = composer.querySelector(".ai-composer-usage");
|
|
977
|
+
if (!cfg || !mount) return;
|
|
978
|
+
const explicit = typeof cfg.opts.total === "number" && cfg.opts.total > 0;
|
|
979
|
+
renderAiUsageRing(mount, cfg.usage, {
|
|
980
|
+
...cfg.opts,
|
|
981
|
+
total: explicit ? cfg.opts.total : MODEL_CTX.get(composer)
|
|
982
|
+
});
|
|
983
|
+
}
|
|
984
|
+
var visibleOptions = (s) => s.options.filter((o) => !o.hidden);
|
|
985
|
+
function setPopupFocus(s, idx) {
|
|
986
|
+
const vis = visibleOptions(s);
|
|
987
|
+
s.options.forEach((o) => o.classList.remove("is-focused"));
|
|
988
|
+
if (vis.length === 0) {
|
|
989
|
+
s.focused = -1;
|
|
990
|
+
return;
|
|
991
|
+
}
|
|
992
|
+
s.focused = Math.max(0, Math.min(idx, vis.length - 1));
|
|
993
|
+
const cur = vis[s.focused];
|
|
994
|
+
cur.classList.add("is-focused");
|
|
995
|
+
cur.scrollIntoView({ block: "nearest" });
|
|
996
|
+
}
|
|
997
|
+
function movePopupFocus(s, delta) {
|
|
998
|
+
const vis = visibleOptions(s);
|
|
999
|
+
if (vis.length === 0) return;
|
|
1000
|
+
setPopupFocus(s, (s.focused + delta + vis.length) % vis.length);
|
|
1001
|
+
}
|
|
1002
|
+
function settlePopup(s) {
|
|
1003
|
+
for (const g of s.groups) {
|
|
1004
|
+
g.hidden = !Array.from(g.querySelectorAll(".ai-composer-popup-option")).some((o) => !o.hidden);
|
|
1005
|
+
}
|
|
1006
|
+
s.emptyEl.hidden = visibleOptions(s).length > 0;
|
|
1007
|
+
setPopupFocus(s, 0);
|
|
1008
|
+
}
|
|
1009
|
+
function popupKeydown(composer, s, e) {
|
|
1010
|
+
if (e.isComposing) return;
|
|
1011
|
+
switch (e.key) {
|
|
1012
|
+
case "ArrowDown":
|
|
1013
|
+
e.preventDefault();
|
|
1014
|
+
movePopupFocus(s, 1);
|
|
1015
|
+
return;
|
|
1016
|
+
case "ArrowUp":
|
|
1017
|
+
e.preventDefault();
|
|
1018
|
+
movePopupFocus(s, -1);
|
|
1019
|
+
return;
|
|
1020
|
+
case "Enter":
|
|
1021
|
+
case "Tab": {
|
|
1022
|
+
e.preventDefault();
|
|
1023
|
+
const vis = visibleOptions(s);
|
|
1024
|
+
const opt = vis[s.focused] ?? vis[0];
|
|
1025
|
+
if (opt) {
|
|
1026
|
+
s.pick(opt);
|
|
1027
|
+
return;
|
|
1028
|
+
}
|
|
1029
|
+
closePopup(composer, false);
|
|
1030
|
+
if (e.key === "Enter") SUBMITS.get(composer)?.();
|
|
1031
|
+
return;
|
|
1032
|
+
}
|
|
1033
|
+
case "Escape":
|
|
1034
|
+
e.preventDefault();
|
|
1035
|
+
closePopup(composer);
|
|
1036
|
+
return;
|
|
1037
|
+
}
|
|
1038
|
+
}
|
|
1039
|
+
function closePopup(composer, restoreFocus = true) {
|
|
1040
|
+
const s = POPUP.get(composer);
|
|
1041
|
+
if (!s) return;
|
|
1042
|
+
POPUP.delete(composer);
|
|
1043
|
+
if (s.searchInput && s.keydownHandler) {
|
|
1044
|
+
s.searchInput.removeEventListener("keydown", s.keydownHandler);
|
|
1045
|
+
}
|
|
1046
|
+
if (s.mode === "model") {
|
|
1047
|
+
composer.querySelector("[data-ai-model-open]")?.setAttribute("aria-expanded", "false");
|
|
1048
|
+
}
|
|
1049
|
+
if (s.mode !== "model") {
|
|
1050
|
+
getTextarea(composer)?.setAttribute("aria-expanded", "false");
|
|
1051
|
+
}
|
|
1052
|
+
closePopover(s.panel);
|
|
1053
|
+
if (restoreFocus && s.returnFocus && s.returnFocus.isConnected) s.returnFocus.focus();
|
|
1054
|
+
}
|
|
1055
|
+
function mountPopup(composer, init) {
|
|
1056
|
+
closePopup(composer, false);
|
|
1057
|
+
const panel = document.createElement("div");
|
|
1058
|
+
panel.className = "ai-composer-popup";
|
|
1059
|
+
panel.hidden = true;
|
|
1060
|
+
const session = {
|
|
1061
|
+
mode: init.mode,
|
|
1062
|
+
panel,
|
|
1063
|
+
groups: [],
|
|
1064
|
+
options: [],
|
|
1065
|
+
emptyEl: document.createElement("div"),
|
|
1066
|
+
focused: -1,
|
|
1067
|
+
searchInput: null,
|
|
1068
|
+
refRange: init.refRange ?? null,
|
|
1069
|
+
returnFocus: init.returnFocus,
|
|
1070
|
+
keydownHandler: null,
|
|
1071
|
+
pick: init.pick
|
|
1072
|
+
};
|
|
1073
|
+
init.build(session);
|
|
1074
|
+
openPopover(panel, {
|
|
1075
|
+
anchor: init.anchor,
|
|
1076
|
+
side: "top",
|
|
1077
|
+
align: "start",
|
|
1078
|
+
offset: 8,
|
|
1079
|
+
minWidth: init.minWidth,
|
|
1080
|
+
maxWidth: init.maxWidth,
|
|
1081
|
+
maxHeight: init.maxHeight,
|
|
1082
|
+
onClose: () => {
|
|
1083
|
+
if (POPUP.get(composer) === session) POPUP.delete(composer);
|
|
1084
|
+
panel.remove();
|
|
1085
|
+
}
|
|
1086
|
+
});
|
|
1087
|
+
POPUP.set(composer, session);
|
|
1088
|
+
panel.addEventListener("click", (e) => {
|
|
1089
|
+
const t = e.target;
|
|
1090
|
+
if (!(t instanceof Element)) return;
|
|
1091
|
+
const opt = t.closest(".ai-composer-popup-option");
|
|
1092
|
+
if (opt && panel.contains(opt) && !opt.hidden) session.pick(opt);
|
|
1093
|
+
});
|
|
1094
|
+
panel.addEventListener("mouseover", (e) => {
|
|
1095
|
+
const t = e.target;
|
|
1096
|
+
if (!(t instanceof Element)) return;
|
|
1097
|
+
const opt = t.closest(".ai-composer-popup-option");
|
|
1098
|
+
if (!opt || opt.hidden) return;
|
|
1099
|
+
const idx = visibleOptions(session).indexOf(opt);
|
|
1100
|
+
if (idx >= 0) setPopupFocus(session, idx);
|
|
1101
|
+
});
|
|
1102
|
+
return session;
|
|
1103
|
+
}
|
|
1104
|
+
function openModelPopup(composer) {
|
|
1105
|
+
const cfg = MODELS.get(composer);
|
|
1106
|
+
if (!cfg) return;
|
|
1107
|
+
const btn = composer.querySelector("[data-ai-model-open]");
|
|
1108
|
+
const anchor = btn ?? composer.querySelector(".ai-composer-box") ?? composer;
|
|
1109
|
+
const ta = getTextarea(composer);
|
|
1110
|
+
const session = mountPopup(composer, {
|
|
1111
|
+
mode: "model",
|
|
1112
|
+
anchor,
|
|
1113
|
+
minWidth: 220,
|
|
1114
|
+
maxWidth: 320,
|
|
1115
|
+
maxHeight: 340,
|
|
1116
|
+
returnFocus: btn ?? ta,
|
|
1117
|
+
pick: (opt) => {
|
|
1118
|
+
const provider = opt.dataset.provider ?? "";
|
|
1119
|
+
const model = opt.dataset.model ?? "";
|
|
1120
|
+
const label = opt.dataset.label ?? model;
|
|
1121
|
+
const context = typeof opt.dataset.context === "string" ? Number(opt.dataset.context) : void 0;
|
|
1122
|
+
MODELS.set(composer, { providers: cfg.providers, current: { provider, model } });
|
|
1123
|
+
if (typeof context === "number" && Number.isFinite(context) && context > 0) {
|
|
1124
|
+
MODEL_CTX.set(composer, context);
|
|
1125
|
+
} else {
|
|
1126
|
+
MODEL_CTX.delete(composer);
|
|
1127
|
+
}
|
|
1128
|
+
updateModelButton(composer);
|
|
1129
|
+
emit(composer, "icen:ai-model-change", { provider, model, label, context });
|
|
1130
|
+
refreshUsageRing(composer);
|
|
1131
|
+
closePopup(composer);
|
|
1132
|
+
},
|
|
1133
|
+
build: (s) => {
|
|
1134
|
+
const search = document.createElement("div");
|
|
1135
|
+
search.className = "ai-composer-popup-search";
|
|
1136
|
+
const input = document.createElement("input");
|
|
1137
|
+
input.type = "search";
|
|
1138
|
+
input.className = "ai-composer-popup-search-input";
|
|
1139
|
+
input.placeholder = "\u641C\u7D22\u6A21\u578B";
|
|
1140
|
+
input.setAttribute("aria-label", "\u641C\u7D22\u6A21\u578B");
|
|
1141
|
+
search.appendChild(input);
|
|
1142
|
+
s.panel.appendChild(search);
|
|
1143
|
+
s.searchInput = input;
|
|
1144
|
+
const list = document.createElement("div");
|
|
1145
|
+
list.className = "ai-composer-popup-list";
|
|
1146
|
+
list.setAttribute("role", "listbox");
|
|
1147
|
+
list.setAttribute("aria-label", "\u9009\u62E9\u6A21\u578B");
|
|
1148
|
+
for (const p of cfg.providers) {
|
|
1149
|
+
const group = document.createElement("div");
|
|
1150
|
+
group.className = "ai-composer-popup-group";
|
|
1151
|
+
const glabel = document.createElement("div");
|
|
1152
|
+
glabel.className = "ai-composer-popup-group-label";
|
|
1153
|
+
glabel.appendChild(providerMark(p, "ai-composer-popup-group-icon"));
|
|
1154
|
+
const gtext = document.createElement("span");
|
|
1155
|
+
gtext.textContent = p.label;
|
|
1156
|
+
glabel.appendChild(gtext);
|
|
1157
|
+
group.appendChild(glabel);
|
|
1158
|
+
for (const m of p.models) {
|
|
1159
|
+
const opt = document.createElement("button");
|
|
1160
|
+
opt.type = "button";
|
|
1161
|
+
opt.className = "ai-composer-popup-option";
|
|
1162
|
+
opt.setAttribute("role", "option");
|
|
1163
|
+
opt.dataset.provider = p.id;
|
|
1164
|
+
opt.dataset.providerLabel = p.label;
|
|
1165
|
+
opt.dataset.model = m.id;
|
|
1166
|
+
opt.dataset.label = m.label;
|
|
1167
|
+
if (typeof m.context === "number") opt.dataset.context = String(m.context);
|
|
1168
|
+
const selected = cfg.current.provider === p.id && cfg.current.model === m.id;
|
|
1169
|
+
opt.setAttribute("aria-selected", String(selected));
|
|
1170
|
+
const icon = document.createElement("span");
|
|
1171
|
+
icon.className = "ai-composer-popup-option-icon";
|
|
1172
|
+
icon.appendChild(providerMark(p, "ai-composer-popup-option-provider-icon"));
|
|
1173
|
+
const label = document.createElement("span");
|
|
1174
|
+
label.className = "ai-composer-popup-option-label";
|
|
1175
|
+
label.textContent = m.label;
|
|
1176
|
+
const meta = document.createElement("span");
|
|
1177
|
+
meta.className = "ai-composer-popup-option-meta";
|
|
1178
|
+
meta.textContent = typeof m.context === "number" ? formatTokens(m.context) : "";
|
|
1179
|
+
const check = document.createElement("span");
|
|
1180
|
+
check.className = "ai-composer-popup-option-check";
|
|
1181
|
+
const checkSvg = svgIcon(ICON_CHECK);
|
|
1182
|
+
if (checkSvg) check.appendChild(checkSvg);
|
|
1183
|
+
opt.append(icon, label, meta, check);
|
|
1184
|
+
group.appendChild(opt);
|
|
1185
|
+
s.options.push(opt);
|
|
1186
|
+
}
|
|
1187
|
+
list.appendChild(group);
|
|
1188
|
+
s.groups.push(group);
|
|
1189
|
+
}
|
|
1190
|
+
s.panel.appendChild(list);
|
|
1191
|
+
s.emptyEl.className = "ai-composer-popup-empty";
|
|
1192
|
+
s.emptyEl.textContent = "\u65E0\u5339\u914D\u6A21\u578B";
|
|
1193
|
+
s.emptyEl.hidden = true;
|
|
1194
|
+
s.panel.appendChild(s.emptyEl);
|
|
1195
|
+
const foot = document.createElement("div");
|
|
1196
|
+
foot.className = "ai-composer-popup-foot";
|
|
1197
|
+
const usageCfg = USAGE.get(composer);
|
|
1198
|
+
if (usageCfg) {
|
|
1199
|
+
const u = usageCfg.usage;
|
|
1200
|
+
const used = (u.input ?? 0) + (u.output ?? 0) + (u.cacheRead ?? 0) + (u.cacheWrite ?? 0) + (u.reasoning ?? 0);
|
|
1201
|
+
if (used > 0 && (u.cacheRead ?? 0) / used >= CACHE_WARN_RATIO) {
|
|
1202
|
+
foot.classList.add("is-warn");
|
|
1203
|
+
}
|
|
1204
|
+
}
|
|
1205
|
+
const info = svgIcon(ICON_INFO);
|
|
1206
|
+
if (info) foot.appendChild(info);
|
|
1207
|
+
const footText = document.createElement("span");
|
|
1208
|
+
footText.textContent = "\u5207\u6362\u6A21\u578B\u4F1A\u4F7F prompt \u7F13\u5B58\u5931\u6548";
|
|
1209
|
+
foot.appendChild(footText);
|
|
1210
|
+
s.panel.appendChild(foot);
|
|
1211
|
+
s.keydownHandler = (e) => popupKeydown(composer, s, e);
|
|
1212
|
+
input.addEventListener("keydown", s.keydownHandler);
|
|
1213
|
+
input.addEventListener("input", () => {
|
|
1214
|
+
const q = input.value.trim().toLowerCase();
|
|
1215
|
+
for (const opt of s.options) {
|
|
1216
|
+
const hay = `${opt.dataset.label ?? ""} ${opt.dataset.providerLabel ?? ""}`.toLowerCase();
|
|
1217
|
+
opt.hidden = q !== "" && !hay.includes(q);
|
|
1218
|
+
}
|
|
1219
|
+
settlePopup(s);
|
|
1220
|
+
});
|
|
1221
|
+
}
|
|
1222
|
+
});
|
|
1223
|
+
btn?.setAttribute("aria-expanded", "true");
|
|
1224
|
+
session.searchInput?.focus();
|
|
1225
|
+
}
|
|
1226
|
+
function openCommandPopup(composer, token) {
|
|
1227
|
+
const commands = COMMANDS.get(composer);
|
|
1228
|
+
if (!commands || commands.length === 0) return;
|
|
1229
|
+
const box = composer.querySelector(".ai-composer-box") ?? composer;
|
|
1230
|
+
const ta = getTextarea(composer);
|
|
1231
|
+
const boxRect = box.getBoundingClientRect();
|
|
1232
|
+
const session = mountPopup(composer, {
|
|
1233
|
+
mode: "command",
|
|
1234
|
+
anchor: box,
|
|
1235
|
+
minWidth: Math.min(Math.max(boxRect.width, 200), 320),
|
|
1236
|
+
maxWidth: 360,
|
|
1237
|
+
maxHeight: 320,
|
|
1238
|
+
returnFocus: ta,
|
|
1239
|
+
pick: (opt) => {
|
|
1240
|
+
const name = opt.dataset.name ?? "";
|
|
1241
|
+
const m = /^\/\S*\s+([\s\S]*)$/.exec(ta?.value ?? "");
|
|
1242
|
+
const args = m ? m[1] : "";
|
|
1243
|
+
if (ta) {
|
|
1244
|
+
ta.value = "";
|
|
1245
|
+
autosizeTa(ta);
|
|
1246
|
+
}
|
|
1247
|
+
emit(composer, "icen:ai-command", { name, args });
|
|
1248
|
+
closePopup(composer);
|
|
1249
|
+
},
|
|
1250
|
+
build: (s) => {
|
|
1251
|
+
const list = document.createElement("div");
|
|
1252
|
+
list.className = "ai-composer-popup-list";
|
|
1253
|
+
list.setAttribute("role", "listbox");
|
|
1254
|
+
list.setAttribute("aria-label", "\u547D\u4EE4");
|
|
1255
|
+
const group = document.createElement("div");
|
|
1256
|
+
group.className = "ai-composer-popup-group";
|
|
1257
|
+
for (const c of commands) {
|
|
1258
|
+
const opt = document.createElement("button");
|
|
1259
|
+
opt.type = "button";
|
|
1260
|
+
opt.className = "ai-composer-popup-option ai-composer-popup-cmd";
|
|
1261
|
+
opt.setAttribute("role", "option");
|
|
1262
|
+
opt.dataset.name = c.name;
|
|
1263
|
+
opt.dataset.hay = `${c.name} ${c.description ?? ""}`.toLowerCase();
|
|
1264
|
+
const line = document.createElement("span");
|
|
1265
|
+
line.className = "ai-composer-popup-cmd-line";
|
|
1266
|
+
const nameEl = document.createElement("span");
|
|
1267
|
+
nameEl.className = "ai-composer-popup-cmd-name";
|
|
1268
|
+
nameEl.textContent = `/${c.name}`;
|
|
1269
|
+
line.appendChild(nameEl);
|
|
1270
|
+
if (c.argsHint) {
|
|
1271
|
+
const argsEl = document.createElement("span");
|
|
1272
|
+
argsEl.className = "ai-composer-popup-cmd-args";
|
|
1273
|
+
argsEl.textContent = c.argsHint;
|
|
1274
|
+
line.appendChild(argsEl);
|
|
1275
|
+
}
|
|
1276
|
+
opt.appendChild(line);
|
|
1277
|
+
if (c.description) {
|
|
1278
|
+
opt.appendChild(Object.assign(document.createElement("span"), {
|
|
1279
|
+
className: "ai-composer-popup-cmd-desc",
|
|
1280
|
+
textContent: c.description
|
|
1281
|
+
}));
|
|
1282
|
+
}
|
|
1283
|
+
group.appendChild(opt);
|
|
1284
|
+
s.options.push(opt);
|
|
1285
|
+
}
|
|
1286
|
+
list.appendChild(group);
|
|
1287
|
+
s.groups.push(group);
|
|
1288
|
+
s.panel.appendChild(list);
|
|
1289
|
+
s.emptyEl.className = "ai-composer-popup-empty";
|
|
1290
|
+
s.emptyEl.textContent = "\u65E0\u5339\u914D\u547D\u4EE4";
|
|
1291
|
+
s.emptyEl.hidden = true;
|
|
1292
|
+
s.panel.appendChild(s.emptyEl);
|
|
1293
|
+
}
|
|
1294
|
+
});
|
|
1295
|
+
ta?.setAttribute("aria-expanded", "true");
|
|
1296
|
+
filterCommandOptions(session, token);
|
|
1297
|
+
}
|
|
1298
|
+
function commandToken(value) {
|
|
1299
|
+
return value.slice(1).split(/\s/, 1)[0] ?? "";
|
|
1300
|
+
}
|
|
1301
|
+
function filterCommandOptions(s, token) {
|
|
1302
|
+
const q = token.trim().toLowerCase();
|
|
1303
|
+
for (const opt of s.options) {
|
|
1304
|
+
opt.hidden = q !== "" && !(opt.dataset.hay ?? "").includes(q);
|
|
1305
|
+
}
|
|
1306
|
+
settlePopup(s);
|
|
1307
|
+
}
|
|
1308
|
+
function updateCommandPopup(composer, s) {
|
|
1309
|
+
const ta = getTextarea(composer);
|
|
1310
|
+
const value = ta?.value ?? "";
|
|
1311
|
+
if (!value.startsWith("/")) {
|
|
1312
|
+
closePopup(composer, false);
|
|
1313
|
+
maybeOpenTriggerPopups(composer);
|
|
1314
|
+
return;
|
|
1315
|
+
}
|
|
1316
|
+
filterCommandOptions(s, commandToken(value));
|
|
1317
|
+
}
|
|
1318
|
+
function openRefPopup(composer, range, filter) {
|
|
1319
|
+
const sources = REF_SOURCES.get(composer);
|
|
1320
|
+
if (!sources || sources.length === 0) return;
|
|
1321
|
+
const box = composer.querySelector(".ai-composer-box") ?? composer;
|
|
1322
|
+
const ta = getTextarea(composer);
|
|
1323
|
+
const boxRect = box.getBoundingClientRect();
|
|
1324
|
+
const session = mountPopup(composer, {
|
|
1325
|
+
mode: "ref",
|
|
1326
|
+
anchor: box,
|
|
1327
|
+
minWidth: Math.min(Math.max(boxRect.width, 200), 320),
|
|
1328
|
+
maxWidth: 360,
|
|
1329
|
+
maxHeight: 320,
|
|
1330
|
+
returnFocus: ta,
|
|
1331
|
+
refRange: range,
|
|
1332
|
+
pick: (opt) => {
|
|
1333
|
+
const source = (REF_SOURCES.get(composer) ?? []).find((r2) => r2.id === opt.dataset.refId);
|
|
1334
|
+
const cur = POPUP.get(composer);
|
|
1335
|
+
const r = cur?.refRange ?? (ta ? findRefToken(ta) : null);
|
|
1336
|
+
if (source && ta && r) {
|
|
1337
|
+
const before = ta.value.slice(0, r.start + 1);
|
|
1338
|
+
const after = ta.value.slice(r.end);
|
|
1339
|
+
ta.value = before + source.label + " " + after;
|
|
1340
|
+
const caret = before.length + source.label.length + 1;
|
|
1341
|
+
ta.setSelectionRange(caret, caret);
|
|
1342
|
+
autosizeTa(ta);
|
|
1343
|
+
addActiveRef(composer, source);
|
|
1344
|
+
}
|
|
1345
|
+
closePopup(composer);
|
|
1346
|
+
},
|
|
1347
|
+
build: (s) => {
|
|
1348
|
+
const list = document.createElement("div");
|
|
1349
|
+
list.className = "ai-composer-popup-list";
|
|
1350
|
+
list.setAttribute("role", "listbox");
|
|
1351
|
+
list.setAttribute("aria-label", "\u5F15\u7528");
|
|
1352
|
+
for (const kind of REF_KIND_ORDER) {
|
|
1353
|
+
const items = sources.filter((r) => r.kind === kind);
|
|
1354
|
+
if (items.length === 0) continue;
|
|
1355
|
+
const group = document.createElement("div");
|
|
1356
|
+
group.className = "ai-composer-popup-group";
|
|
1357
|
+
group.dataset.kind = kind;
|
|
1358
|
+
group.appendChild(Object.assign(document.createElement("div"), {
|
|
1359
|
+
className: "ai-composer-popup-group-label",
|
|
1360
|
+
textContent: REF_KIND_LABEL[kind]
|
|
1361
|
+
}));
|
|
1362
|
+
for (const r of items) {
|
|
1363
|
+
const opt = document.createElement("button");
|
|
1364
|
+
opt.type = "button";
|
|
1365
|
+
opt.className = "ai-composer-popup-option";
|
|
1366
|
+
opt.setAttribute("role", "option");
|
|
1367
|
+
opt.dataset.refId = r.id;
|
|
1368
|
+
opt.dataset.hay = `${r.label} ${r.sub ?? ""}`.toLowerCase();
|
|
1369
|
+
const icon = document.createElement("span");
|
|
1370
|
+
icon.className = "ai-composer-popup-option-icon";
|
|
1371
|
+
const svg2 = svgIcon(REF_KIND_ICON[kind]);
|
|
1372
|
+
if (svg2) icon.appendChild(svg2);
|
|
1373
|
+
const label = document.createElement("span");
|
|
1374
|
+
label.className = "ai-composer-popup-option-label";
|
|
1375
|
+
label.textContent = r.label;
|
|
1376
|
+
if (r.sub) {
|
|
1377
|
+
const sub = document.createElement("span");
|
|
1378
|
+
sub.className = "ai-composer-popup-option-sub";
|
|
1379
|
+
sub.textContent = r.sub;
|
|
1380
|
+
label.appendChild(sub);
|
|
1381
|
+
}
|
|
1382
|
+
opt.append(icon, label);
|
|
1383
|
+
group.appendChild(opt);
|
|
1384
|
+
s.options.push(opt);
|
|
1385
|
+
}
|
|
1386
|
+
list.appendChild(group);
|
|
1387
|
+
s.groups.push(group);
|
|
1388
|
+
}
|
|
1389
|
+
s.panel.appendChild(list);
|
|
1390
|
+
s.emptyEl.className = "ai-composer-popup-empty";
|
|
1391
|
+
s.emptyEl.textContent = "\u65E0\u5339\u914D\u5F15\u7528";
|
|
1392
|
+
s.emptyEl.hidden = true;
|
|
1393
|
+
s.panel.appendChild(s.emptyEl);
|
|
1394
|
+
}
|
|
1395
|
+
});
|
|
1396
|
+
ta?.setAttribute("aria-expanded", "true");
|
|
1397
|
+
filterRefOptions(session, filter);
|
|
1398
|
+
}
|
|
1399
|
+
function filterRefOptions(s, filter) {
|
|
1400
|
+
const q = filter.trim().toLowerCase();
|
|
1401
|
+
for (const opt of s.options) {
|
|
1402
|
+
opt.hidden = q !== "" && !(opt.dataset.hay ?? "").includes(q);
|
|
1403
|
+
}
|
|
1404
|
+
settlePopup(s);
|
|
1405
|
+
}
|
|
1406
|
+
function updateRefPopup(composer, s) {
|
|
1407
|
+
const ta = getTextarea(composer);
|
|
1408
|
+
const t = ta ? findRefToken(ta) : null;
|
|
1409
|
+
if (!t) {
|
|
1410
|
+
closePopup(composer, false);
|
|
1411
|
+
maybeOpenTriggerPopups(composer);
|
|
1412
|
+
return;
|
|
1413
|
+
}
|
|
1414
|
+
s.refRange = { start: t.start, end: t.end };
|
|
1415
|
+
filterRefOptions(s, t.filter);
|
|
1416
|
+
}
|
|
1417
|
+
function findRefToken(ta) {
|
|
1418
|
+
const value = ta.value;
|
|
1419
|
+
const end = ta.selectionStart ?? value.length;
|
|
1420
|
+
const at = value.lastIndexOf("@", end - 1);
|
|
1421
|
+
if (at === -1) return null;
|
|
1422
|
+
if (at > 0 && !/\s/.test(value[at - 1] ?? "")) return null;
|
|
1423
|
+
const filter = value.slice(at + 1, end);
|
|
1424
|
+
if (/\s/.test(filter)) return null;
|
|
1425
|
+
return { start: at, end, filter };
|
|
1426
|
+
}
|
|
1427
|
+
function maybeOpenTriggerPopups(composer) {
|
|
1428
|
+
const ta = getTextarea(composer);
|
|
1429
|
+
if (!ta) return;
|
|
1430
|
+
const commands = COMMANDS.get(composer);
|
|
1431
|
+
if (commands && commands.length > 0 && ta.value.startsWith("/")) {
|
|
1432
|
+
openCommandPopup(composer, commandToken(ta.value));
|
|
1433
|
+
return;
|
|
1434
|
+
}
|
|
1435
|
+
const sources = REF_SOURCES.get(composer);
|
|
1436
|
+
if (sources && sources.length > 0) {
|
|
1437
|
+
const t = findRefToken(ta);
|
|
1438
|
+
if (t) openRefPopup(composer, { start: t.start, end: t.end }, t.filter);
|
|
1439
|
+
}
|
|
1440
|
+
}
|
|
1441
|
+
function ensureRefsRow(composer) {
|
|
1442
|
+
const existing = composer.querySelector(".ai-composer-refs");
|
|
1443
|
+
if (existing) return existing;
|
|
1444
|
+
const row = document.createElement("div");
|
|
1445
|
+
row.className = "ai-composer-refs";
|
|
1446
|
+
row.dataset.aiRefs = "auto";
|
|
1447
|
+
row.hidden = true;
|
|
1448
|
+
const attach = composer.querySelector(".ai-composer-attach");
|
|
1449
|
+
const box = composer.querySelector(".ai-composer-box");
|
|
1450
|
+
composer.insertBefore(row, attach ?? box ?? null);
|
|
1451
|
+
return row;
|
|
1452
|
+
}
|
|
1453
|
+
function renderRefs(composer) {
|
|
1454
|
+
const row = composer.querySelector(".ai-composer-refs");
|
|
1455
|
+
const actives = ACTIVE_REFS.get(composer) ?? [];
|
|
1456
|
+
if (!row) return;
|
|
1457
|
+
row.replaceChildren(
|
|
1458
|
+
...actives.map((src) => {
|
|
1459
|
+
const chip = makeChip(src.label, true, `\u79FB\u9664\u5F15\u7528 ${src.label}`);
|
|
1460
|
+
chip.dataset.refId = src.id;
|
|
1461
|
+
return chip;
|
|
1462
|
+
})
|
|
1463
|
+
);
|
|
1464
|
+
row.hidden = actives.length === 0;
|
|
1465
|
+
}
|
|
1466
|
+
function addActiveRef(composer, source) {
|
|
1467
|
+
const actives = ACTIVE_REFS.get(composer) ?? [];
|
|
1468
|
+
if (actives.some((r) => r.id === source.id)) return;
|
|
1469
|
+
actives.push(source);
|
|
1470
|
+
ACTIVE_REFS.set(composer, actives);
|
|
1471
|
+
renderRefs(composer);
|
|
1472
|
+
emit(composer, "icen:ai-ref", { action: "add", ref: source });
|
|
1473
|
+
}
|
|
1474
|
+
function removeActiveRef(composer, source) {
|
|
1475
|
+
const actives = ACTIVE_REFS.get(composer) ?? [];
|
|
1476
|
+
const idx = actives.findIndex((r) => r.id === source.id);
|
|
1477
|
+
if (idx === -1) return;
|
|
1478
|
+
actives.splice(idx, 1);
|
|
1479
|
+
ACTIVE_REFS.set(composer, actives);
|
|
1480
|
+
renderRefs(composer);
|
|
1481
|
+
const ta = getTextarea(composer);
|
|
1482
|
+
if (ta) {
|
|
1483
|
+
const needle = `@${source.label}`;
|
|
1484
|
+
const at = ta.value.indexOf(needle);
|
|
1485
|
+
if (at !== -1) {
|
|
1486
|
+
let endIdx = at + needle.length;
|
|
1487
|
+
if (ta.value[endIdx] === " ") endIdx += 1;
|
|
1488
|
+
ta.value = ta.value.slice(0, at) + ta.value.slice(endIdx);
|
|
1489
|
+
autosizeTa(ta);
|
|
1490
|
+
}
|
|
1491
|
+
}
|
|
1492
|
+
emit(composer, "icen:ai-ref", { action: "remove", ref: source });
|
|
1493
|
+
}
|
|
1494
|
+
function pushHistory(composer, text) {
|
|
1495
|
+
const h = HISTORIES.get(composer) ?? { items: [], index: -1 };
|
|
1496
|
+
if (h.items[h.items.length - 1] !== text) {
|
|
1497
|
+
h.items.push(text);
|
|
1498
|
+
if (h.items.length > HISTORY_LIMIT) h.items.shift();
|
|
1499
|
+
}
|
|
1500
|
+
h.index = -1;
|
|
1501
|
+
HISTORIES.set(composer, h);
|
|
1502
|
+
}
|
|
1503
|
+
function setupComposer(composer) {
|
|
1504
|
+
const el3 = composer;
|
|
1505
|
+
if (el3.__icenAiComposerInit) return void 0;
|
|
1506
|
+
el3.__icenAiComposerInit = true;
|
|
1507
|
+
const ta = getTextarea(composer);
|
|
1508
|
+
if (!ta) {
|
|
1509
|
+
el3.__icenAiComposerInit = false;
|
|
1510
|
+
return void 0;
|
|
1511
|
+
}
|
|
1512
|
+
const queueEl = composer.querySelector(".ai-composer-queue");
|
|
1513
|
+
const attachEl = composer.querySelector(".ai-composer-attach");
|
|
1514
|
+
const { sendBtn, sendIcon, stopIcon } = ensureIcons(composer);
|
|
1515
|
+
if (!ta.hasAttribute("placeholder")) {
|
|
1516
|
+
ta.setAttribute("placeholder", "\u8F93\u5165\u6D88\u606F\uFF0C/ \u547D\u4EE4 \xB7 @ \u5F15\u7528 \xB7 Enter \u53D1\u9001");
|
|
1517
|
+
}
|
|
1518
|
+
const renderQueue = () => renderQueueChips(composer);
|
|
1519
|
+
const enqueue = (text) => {
|
|
1520
|
+
const queue = QUEUES.get(composer) ?? [];
|
|
1521
|
+
queue.push(text);
|
|
1522
|
+
QUEUES.set(composer, queue);
|
|
1523
|
+
renderQueue();
|
|
1524
|
+
};
|
|
1525
|
+
const isRunning = () => RUNNING.get(composer) ?? false;
|
|
1526
|
+
const submit = () => {
|
|
1527
|
+
const text = ta.value;
|
|
1528
|
+
if (!text.trim()) return;
|
|
1529
|
+
ta.value = "";
|
|
1530
|
+
autosizeTa(ta);
|
|
1531
|
+
if (isRunning()) {
|
|
1532
|
+
enqueue(text);
|
|
1533
|
+
emit(composer, "icen:ai-queue", { text });
|
|
1534
|
+
} else {
|
|
1535
|
+
pushHistory(composer, text);
|
|
1536
|
+
emit(composer, "icen:ai-send", { text });
|
|
1537
|
+
}
|
|
1538
|
+
};
|
|
1539
|
+
SUBMITS.set(composer, submit);
|
|
1540
|
+
const takeBackLastQueued = () => {
|
|
1541
|
+
const queue = QUEUES.get(composer) ?? [];
|
|
1542
|
+
if (queue.length === 0) return;
|
|
1543
|
+
const index = queue.length - 1;
|
|
1544
|
+
const text = queue.pop();
|
|
1545
|
+
QUEUES.set(composer, queue);
|
|
1546
|
+
renderQueue();
|
|
1547
|
+
ta.value = text;
|
|
1548
|
+
autosizeTa(ta);
|
|
1549
|
+
emit(composer, "icen:ai-dequeue", { index });
|
|
1550
|
+
};
|
|
1551
|
+
const browseHistory = (delta) => {
|
|
1552
|
+
const h = HISTORIES.get(composer);
|
|
1553
|
+
if (!h || h.items.length === 0) return;
|
|
1554
|
+
if (delta === -1) {
|
|
1555
|
+
h.index = h.index === -1 ? h.items.length - 1 : Math.max(0, h.index - 1);
|
|
1556
|
+
} else {
|
|
1557
|
+
h.index += 1;
|
|
1558
|
+
if (h.index >= h.items.length) {
|
|
1559
|
+
h.index = -1;
|
|
1560
|
+
ta.value = "";
|
|
1561
|
+
autosizeTa(ta);
|
|
1562
|
+
return;
|
|
1563
|
+
}
|
|
1564
|
+
}
|
|
1565
|
+
ta.value = h.items[h.index];
|
|
1566
|
+
autosizeTa(ta);
|
|
1567
|
+
};
|
|
1568
|
+
const onKeydown = (e) => {
|
|
1569
|
+
if (composing || e.isComposing) return;
|
|
1570
|
+
const session = POPUP.get(composer);
|
|
1571
|
+
if (session) {
|
|
1572
|
+
popupKeydown(composer, session, e);
|
|
1573
|
+
return;
|
|
1574
|
+
}
|
|
1575
|
+
if (e.key === "Enter" && !e.shiftKey) {
|
|
1576
|
+
e.preventDefault();
|
|
1577
|
+
submit();
|
|
1578
|
+
return;
|
|
1579
|
+
}
|
|
1580
|
+
if (e.key === "ArrowUp") {
|
|
1581
|
+
const h = HISTORIES.get(composer);
|
|
1582
|
+
const browsing = (h?.index ?? -1) >= 0;
|
|
1583
|
+
if (ta.value === "") {
|
|
1584
|
+
if (isRunning() && (QUEUES.get(composer) ?? []).length > 0) {
|
|
1585
|
+
e.preventDefault();
|
|
1586
|
+
takeBackLastQueued();
|
|
1587
|
+
return;
|
|
1588
|
+
}
|
|
1589
|
+
if (h && h.items.length > 0) {
|
|
1590
|
+
e.preventDefault();
|
|
1591
|
+
browseHistory(-1);
|
|
1592
|
+
}
|
|
1593
|
+
} else if (browsing) {
|
|
1594
|
+
e.preventDefault();
|
|
1595
|
+
browseHistory(-1);
|
|
1596
|
+
}
|
|
1597
|
+
return;
|
|
1598
|
+
}
|
|
1599
|
+
if (e.key === "ArrowDown") {
|
|
1600
|
+
const h = HISTORIES.get(composer);
|
|
1601
|
+
if (h && h.index >= 0) {
|
|
1602
|
+
e.preventDefault();
|
|
1603
|
+
browseHistory(1);
|
|
1604
|
+
}
|
|
1605
|
+
}
|
|
1606
|
+
};
|
|
1607
|
+
const onInput = () => {
|
|
1608
|
+
autosizeTa(ta);
|
|
1609
|
+
const h = HISTORIES.get(composer);
|
|
1610
|
+
if (h && h.index >= 0 && ta.value !== h.items[h.index]) h.index = -1;
|
|
1611
|
+
const session = POPUP.get(composer);
|
|
1612
|
+
if (session) {
|
|
1613
|
+
if (session.mode === "command") updateCommandPopup(composer, session);
|
|
1614
|
+
else if (session.mode === "ref") updateRefPopup(composer, session);
|
|
1615
|
+
} else {
|
|
1616
|
+
maybeOpenTriggerPopups(composer);
|
|
1617
|
+
}
|
|
1618
|
+
};
|
|
1619
|
+
let composing = false;
|
|
1620
|
+
const onCompositionStart = () => {
|
|
1621
|
+
composing = true;
|
|
1622
|
+
};
|
|
1623
|
+
const onCompositionEnd = () => {
|
|
1624
|
+
composing = false;
|
|
1625
|
+
};
|
|
1626
|
+
const onSendClick = () => {
|
|
1627
|
+
if (isRunning()) {
|
|
1628
|
+
emit(composer, "icen:ai-stop", {});
|
|
1629
|
+
} else {
|
|
1630
|
+
submit();
|
|
1631
|
+
}
|
|
1632
|
+
};
|
|
1633
|
+
const onQueueClick = (e) => {
|
|
1634
|
+
const btn = e.target instanceof Element ? e.target.closest(".ai-composer-chip-remove") : null;
|
|
1635
|
+
if (!btn) return;
|
|
1636
|
+
const index = Array.from(queueEl?.querySelectorAll(".ai-composer-chip-remove") ?? []).indexOf(btn);
|
|
1637
|
+
const queue = QUEUES.get(composer) ?? [];
|
|
1638
|
+
if (index >= 0 && index < queue.length) {
|
|
1639
|
+
queue.splice(index, 1);
|
|
1640
|
+
QUEUES.set(composer, queue);
|
|
1641
|
+
renderQueue();
|
|
1642
|
+
emit(composer, "icen:ai-dequeue", { index });
|
|
1643
|
+
}
|
|
1644
|
+
};
|
|
1645
|
+
const onRefsClick = (e) => {
|
|
1646
|
+
const btn = e.target instanceof Element ? e.target.closest(".ai-composer-chip-remove") : null;
|
|
1647
|
+
if (!btn || !btn.closest(".ai-composer-refs")) return;
|
|
1648
|
+
const id = btn.closest(".ai-composer-chip")?.dataset.refId;
|
|
1649
|
+
const source = (ACTIVE_REFS.get(composer) ?? []).find((r) => r.id === id);
|
|
1650
|
+
if (source) removeActiveRef(composer, source);
|
|
1651
|
+
};
|
|
1652
|
+
const fileInput = document.createElement("input");
|
|
1653
|
+
fileInput.type = "file";
|
|
1654
|
+
fileInput.hidden = true;
|
|
1655
|
+
fileInput.multiple = true;
|
|
1656
|
+
composer.appendChild(fileInput);
|
|
1657
|
+
const onAttachClick = () => {
|
|
1658
|
+
fileInput.click();
|
|
1659
|
+
};
|
|
1660
|
+
const addFiles = (files) => {
|
|
1661
|
+
if (files.length === 0) return;
|
|
1662
|
+
if (attachEl) {
|
|
1663
|
+
for (const f of files) {
|
|
1664
|
+
const chip = makeChip(f.name, true, `\u79FB\u9664\u9644\u4EF6 ${f.name}`);
|
|
1665
|
+
const remove = chip.querySelector(".ai-composer-chip-remove");
|
|
1666
|
+
remove?.addEventListener("click", () => chip.remove(), { once: true });
|
|
1667
|
+
attachEl.appendChild(chip);
|
|
1668
|
+
}
|
|
1669
|
+
attachEl.hidden = false;
|
|
1670
|
+
}
|
|
1671
|
+
emit(composer, "icen:ai-attach", { files });
|
|
1672
|
+
};
|
|
1673
|
+
const onFileChange = () => {
|
|
1674
|
+
const files = Array.from(fileInput.files ?? []);
|
|
1675
|
+
fileInput.value = "";
|
|
1676
|
+
addFiles(files);
|
|
1677
|
+
};
|
|
1678
|
+
const onPaste = (e) => {
|
|
1679
|
+
const files = Array.from(e.clipboardData?.files ?? []);
|
|
1680
|
+
if (files.length === 0) return;
|
|
1681
|
+
e.preventDefault();
|
|
1682
|
+
addFiles(files);
|
|
1683
|
+
};
|
|
1684
|
+
const boxEl = composer.querySelector(".ai-composer-box");
|
|
1685
|
+
const onDragOver = (e) => {
|
|
1686
|
+
if (!e.dataTransfer?.types.includes("Files")) return;
|
|
1687
|
+
e.preventDefault();
|
|
1688
|
+
boxEl?.classList.add("is-dragover");
|
|
1689
|
+
};
|
|
1690
|
+
const onDragLeave = () => {
|
|
1691
|
+
boxEl?.classList.remove("is-dragover");
|
|
1692
|
+
};
|
|
1693
|
+
const onDrop = (e) => {
|
|
1694
|
+
boxEl?.classList.remove("is-dragover");
|
|
1695
|
+
const files = Array.from(e.dataTransfer?.files ?? []);
|
|
1696
|
+
if (files.length === 0) return;
|
|
1697
|
+
e.preventDefault();
|
|
1698
|
+
addFiles(files);
|
|
1699
|
+
};
|
|
1700
|
+
ta.addEventListener("keydown", onKeydown);
|
|
1701
|
+
ta.addEventListener("input", onInput);
|
|
1702
|
+
ta.addEventListener("compositionstart", onCompositionStart);
|
|
1703
|
+
ta.addEventListener("compositionend", onCompositionEnd);
|
|
1704
|
+
ta.addEventListener("paste", onPaste);
|
|
1705
|
+
boxEl?.addEventListener("dragover", onDragOver);
|
|
1706
|
+
boxEl?.addEventListener("dragleave", onDragLeave);
|
|
1707
|
+
boxEl?.addEventListener("drop", onDrop);
|
|
1708
|
+
sendBtn?.addEventListener("click", onSendClick);
|
|
1709
|
+
queueEl?.addEventListener("click", onQueueClick);
|
|
1710
|
+
composer.addEventListener("click", onRefsClick);
|
|
1711
|
+
composer.querySelector("[data-ai-attach]")?.addEventListener("click", onAttachClick);
|
|
1712
|
+
fileInput.addEventListener("change", onFileChange);
|
|
1713
|
+
const initialRunning = RUNNING.get(composer) ?? composer.classList.contains("is-running");
|
|
1714
|
+
if (initialRunning) setComposerRunning(composer, true);
|
|
1715
|
+
else {
|
|
1716
|
+
if (sendIcon) sendIcon.hidden = false;
|
|
1717
|
+
if (stopIcon) stopIcon.hidden = true;
|
|
1718
|
+
}
|
|
1719
|
+
autosizeTa(ta);
|
|
1720
|
+
renderQueue();
|
|
1721
|
+
renderRefs(composer);
|
|
1722
|
+
return () => {
|
|
1723
|
+
closePopup(composer, false);
|
|
1724
|
+
ta.removeEventListener("keydown", onKeydown);
|
|
1725
|
+
ta.removeEventListener("input", onInput);
|
|
1726
|
+
ta.removeEventListener("compositionstart", onCompositionStart);
|
|
1727
|
+
ta.removeEventListener("compositionend", onCompositionEnd);
|
|
1728
|
+
ta.removeEventListener("paste", onPaste);
|
|
1729
|
+
boxEl?.removeEventListener("dragover", onDragOver);
|
|
1730
|
+
boxEl?.removeEventListener("dragleave", onDragLeave);
|
|
1731
|
+
boxEl?.removeEventListener("drop", onDrop);
|
|
1732
|
+
sendBtn?.removeEventListener("click", onSendClick);
|
|
1733
|
+
queueEl?.removeEventListener("click", onQueueClick);
|
|
1734
|
+
composer.removeEventListener("click", onRefsClick);
|
|
1735
|
+
composer.querySelector("[data-ai-attach]")?.removeEventListener("click", onAttachClick);
|
|
1736
|
+
fileInput.removeEventListener("change", onFileChange);
|
|
1737
|
+
fileInput.remove();
|
|
1738
|
+
el3.__icenAiComposerInit = false;
|
|
1739
|
+
};
|
|
1740
|
+
}
|
|
1741
|
+
function initAiComposer(root) {
|
|
1742
|
+
const cleanups = [];
|
|
1743
|
+
if (isBrowser()) {
|
|
1744
|
+
const scope = root ?? document;
|
|
1745
|
+
const candidates = [];
|
|
1746
|
+
if (scope instanceof Element && scope.matches("[data-ai-composer]")) {
|
|
1747
|
+
candidates.push(scope);
|
|
1748
|
+
}
|
|
1749
|
+
candidates.push(...Array.from(scope.querySelectorAll("[data-ai-composer]")));
|
|
1750
|
+
for (const composer of candidates) {
|
|
1751
|
+
const cleanup = setupComposer(composer);
|
|
1752
|
+
if (cleanup) cleanups.push(cleanup);
|
|
1753
|
+
}
|
|
1754
|
+
}
|
|
1755
|
+
return () => {
|
|
1756
|
+
for (const fn of cleanups) fn();
|
|
1757
|
+
cleanups.length = 0;
|
|
1758
|
+
};
|
|
1759
|
+
}
|
|
1760
|
+
function setComposerModels(el3, providers, current) {
|
|
1761
|
+
if (!isBrowser()) return;
|
|
1762
|
+
const composer = resolveComposer(el3);
|
|
1763
|
+
if (!composer) return;
|
|
1764
|
+
const list = (Array.isArray(providers) ? providers : []).filter(
|
|
1765
|
+
(p) => p && Array.isArray(p.models) && p.models.length > 0
|
|
1766
|
+
);
|
|
1767
|
+
if (list.length === 0) {
|
|
1768
|
+
MODELS.delete(composer);
|
|
1769
|
+
MODEL_CTX.delete(composer);
|
|
1770
|
+
const btn = composer.querySelector("[data-ai-model-open]");
|
|
1771
|
+
if (btn?.dataset.aiModelOpen === "auto") btn.remove();
|
|
1772
|
+
syncToolbar(composer);
|
|
1773
|
+
refreshUsageRing(composer);
|
|
1774
|
+
return;
|
|
1775
|
+
}
|
|
1776
|
+
const cur = current ?? { provider: list[0].id, model: list[0].models[0].id };
|
|
1777
|
+
MODELS.set(composer, { providers: list, current: cur });
|
|
1778
|
+
const provider = list.find((p) => p.id === cur.provider) ?? list[0];
|
|
1779
|
+
const model = provider.models.find((m) => m.id === cur.model) ?? provider.models[0];
|
|
1780
|
+
if (typeof model.context === "number" && model.context > 0) {
|
|
1781
|
+
MODEL_CTX.set(composer, model.context);
|
|
1782
|
+
} else {
|
|
1783
|
+
MODEL_CTX.delete(composer);
|
|
1784
|
+
}
|
|
1785
|
+
syncToolbar(composer);
|
|
1786
|
+
updateModelButton(composer);
|
|
1787
|
+
refreshUsageRing(composer);
|
|
1788
|
+
}
|
|
1789
|
+
function setComposerCommands(el3, commands) {
|
|
1790
|
+
if (!isBrowser()) return;
|
|
1791
|
+
const composer = resolveComposer(el3);
|
|
1792
|
+
if (!composer) return;
|
|
1793
|
+
const list = (Array.isArray(commands) ? commands : []).filter((c) => c && typeof c.name === "string" && c.name);
|
|
1794
|
+
if (list.length === 0) {
|
|
1795
|
+
COMMANDS.delete(composer);
|
|
1796
|
+
const session = POPUP.get(composer);
|
|
1797
|
+
if (session?.mode === "command") closePopup(composer);
|
|
1798
|
+
return;
|
|
1799
|
+
}
|
|
1800
|
+
COMMANDS.set(composer, list);
|
|
1801
|
+
}
|
|
1802
|
+
function setComposerRefs(el3, sources) {
|
|
1803
|
+
if (!isBrowser()) return;
|
|
1804
|
+
const composer = resolveComposer(el3);
|
|
1805
|
+
if (!composer) return;
|
|
1806
|
+
const list = (Array.isArray(sources) ? sources : []).filter((s) => s && typeof s.label === "string" && s.label);
|
|
1807
|
+
REF_SOURCES.set(composer, list);
|
|
1808
|
+
if (list.length === 0) {
|
|
1809
|
+
const session = POPUP.get(composer);
|
|
1810
|
+
if (session?.mode === "ref") closePopup(composer);
|
|
1811
|
+
}
|
|
1812
|
+
ensureRefsRow(composer);
|
|
1813
|
+
renderRefs(composer);
|
|
1814
|
+
}
|
|
1815
|
+
function setComposerUsage(el3, usage, opts = {}) {
|
|
1816
|
+
if (!isBrowser()) return;
|
|
1817
|
+
const composer = resolveComposer(el3);
|
|
1818
|
+
if (!composer) return;
|
|
1819
|
+
if (!usage) {
|
|
1820
|
+
USAGE.delete(composer);
|
|
1821
|
+
const mount = composer.querySelector(".ai-composer-usage");
|
|
1822
|
+
if (mount?.dataset.aiUsage === "auto") mount.remove();
|
|
1823
|
+
syncToolbar(composer);
|
|
1824
|
+
return;
|
|
1825
|
+
}
|
|
1826
|
+
USAGE.set(composer, { usage, opts });
|
|
1827
|
+
syncToolbar(composer);
|
|
1828
|
+
refreshUsageRing(composer);
|
|
1829
|
+
}
|
|
1830
|
+
function setComposerTodo(el3, items) {
|
|
1831
|
+
if (!isBrowser()) return;
|
|
1832
|
+
const composer = resolveComposer(el3);
|
|
1833
|
+
if (!composer) return;
|
|
1834
|
+
let chip = composer.querySelector(".ai-composer-todo");
|
|
1835
|
+
if (!items || items.length === 0) {
|
|
1836
|
+
TODO.delete(composer);
|
|
1837
|
+
chip?.remove();
|
|
1838
|
+
return;
|
|
1839
|
+
}
|
|
1840
|
+
TODO.set(composer, items);
|
|
1841
|
+
if (!chip) {
|
|
1842
|
+
chip = document.createElement("button");
|
|
1843
|
+
chip.type = "button";
|
|
1844
|
+
chip.className = "ai-composer-todo";
|
|
1845
|
+
chip.setAttribute("aria-haspopup", "true");
|
|
1846
|
+
chip.addEventListener("click", () => {
|
|
1847
|
+
const cur = TODO.get(composer);
|
|
1848
|
+
if (!cur) return;
|
|
1849
|
+
const p = document.createElement("div");
|
|
1850
|
+
p.className = "popover ai-composer-todo-pop";
|
|
1851
|
+
p.hidden = true;
|
|
1852
|
+
const title = document.createElement("div");
|
|
1853
|
+
title.className = "ai-usage-popover-title";
|
|
1854
|
+
title.textContent = "\u5F53\u524D\u5F85\u529E";
|
|
1855
|
+
const body = document.createElement("div");
|
|
1856
|
+
body.className = "ai-usage-popover-body";
|
|
1857
|
+
renderAiTodo(body, cur);
|
|
1858
|
+
p.append(title, body);
|
|
1859
|
+
openPopover(p, {
|
|
1860
|
+
anchor: chip,
|
|
1861
|
+
side: "top",
|
|
1862
|
+
align: "end",
|
|
1863
|
+
onClose: () => p.remove()
|
|
1864
|
+
});
|
|
1865
|
+
});
|
|
1866
|
+
const icon = svgIcon(
|
|
1867
|
+
'<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 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"/></svg>'
|
|
1868
|
+
);
|
|
1869
|
+
if (icon) chip.appendChild(icon);
|
|
1870
|
+
chip.appendChild(document.createElement("span")).className = "ai-composer-todo-label";
|
|
1871
|
+
const toolbar = composer.querySelector(".ai-composer-toolbar");
|
|
1872
|
+
const usage = composer.querySelector(".ai-composer-usage");
|
|
1873
|
+
if (toolbar) toolbar.insertBefore(chip, usage ?? null);
|
|
1874
|
+
else {
|
|
1875
|
+
const box = composer.querySelector(".ai-composer-box");
|
|
1876
|
+
box?.before(chip);
|
|
1877
|
+
}
|
|
1878
|
+
}
|
|
1879
|
+
const done = items.filter((t) => t.status === "done").length;
|
|
1880
|
+
const failed = items.some((t) => t.status === "error" || t.status === "cancelled");
|
|
1881
|
+
chip.classList.toggle("is-done", done === items.length && !failed);
|
|
1882
|
+
chip.classList.toggle("is-error", failed);
|
|
1883
|
+
chip.setAttribute("aria-label", `\u5F53\u524D\u5F85\u529E ${done}/${items.length}${failed ? "\uFF08\u6709\u5931\u8D25\u9879\uFF09" : ""}`);
|
|
1884
|
+
const label = chip.querySelector(".ai-composer-todo-label");
|
|
1885
|
+
if (label) label.textContent = `${done}/${items.length}`;
|
|
1886
|
+
}
|
|
1887
|
+
var BOUND = /* @__PURE__ */ new WeakSet();
|
|
1888
|
+
var USAGE_KEYS = ["input", "output", "cacheRead", "cacheWrite", "reasoning", "total"];
|
|
1889
|
+
function accumulateUsage(acc, add) {
|
|
1890
|
+
if (!add) return;
|
|
1891
|
+
for (const k of USAGE_KEYS) {
|
|
1892
|
+
const v = add[k];
|
|
1893
|
+
if (typeof v === "number" && Number.isFinite(v)) acc[k] = (acc[k] ?? 0) + v;
|
|
1894
|
+
}
|
|
1895
|
+
}
|
|
1896
|
+
function bindComposer(el3, opts = {}) {
|
|
1897
|
+
const noop = () => void 0;
|
|
1898
|
+
if (!isBrowser()) return { unbind: noop };
|
|
1899
|
+
const composer = resolveComposer(el3);
|
|
1900
|
+
if (!composer) return { unbind: noop };
|
|
1901
|
+
if (BOUND.has(composer)) return { unbind: noop };
|
|
1902
|
+
BOUND.add(composer);
|
|
1903
|
+
const disposers = [];
|
|
1904
|
+
const usageCfg = opts.usage;
|
|
1905
|
+
const manageRunning = opts.running ?? true;
|
|
1906
|
+
if (usageCfg) {
|
|
1907
|
+
let cost = 0;
|
|
1908
|
+
let billing = {};
|
|
1909
|
+
const onDone = (e) => {
|
|
1910
|
+
const detail = e.detail;
|
|
1911
|
+
if (!detail?.usage) return;
|
|
1912
|
+
if (typeof detail.cost === "number") cost += detail.cost;
|
|
1913
|
+
let usage;
|
|
1914
|
+
if (usageCfg.from === "context") {
|
|
1915
|
+
const u = detail.usage;
|
|
1916
|
+
usage = {
|
|
1917
|
+
input: u.input,
|
|
1918
|
+
cacheRead: u.cacheRead,
|
|
1919
|
+
cacheWrite: u.cacheWrite,
|
|
1920
|
+
output: u.output,
|
|
1921
|
+
reasoning: u.reasoning,
|
|
1922
|
+
total: contextEstimate(u)
|
|
1923
|
+
};
|
|
1924
|
+
} else if (usageCfg.from === "billing") {
|
|
1925
|
+
accumulateUsage(billing, detail.usage);
|
|
1926
|
+
usage = billing;
|
|
1927
|
+
} else {
|
|
1928
|
+
usage = usageCfg.from.summary();
|
|
1929
|
+
}
|
|
1930
|
+
setComposerUsage(composer, usage, {
|
|
1931
|
+
total: usageCfg.total,
|
|
1932
|
+
cost: cost > 0 ? Math.round(cost * 1e6) / 1e6 : void 0
|
|
1933
|
+
});
|
|
1934
|
+
};
|
|
1935
|
+
document.addEventListener("icen:ai-done", onDone);
|
|
1936
|
+
disposers.push(() => document.removeEventListener("icen:ai-done", onDone));
|
|
1937
|
+
}
|
|
1938
|
+
if (opts.client && opts.messages) {
|
|
1939
|
+
const client = opts.client;
|
|
1940
|
+
const messages = opts.messages;
|
|
1941
|
+
const history = [];
|
|
1942
|
+
let session = null;
|
|
1943
|
+
const metaOf = (r) => {
|
|
1944
|
+
const tok = r.usage.total ?? 0;
|
|
1945
|
+
const parts = [tok > 0 ? `${formatTokens(tok)} tok` : "\u5B8C\u6210"];
|
|
1946
|
+
if (typeof r.cost === "number") parts.push(`$${r.cost.toFixed(4)}`);
|
|
1947
|
+
return `\u521A\u521A \xB7 ${parts.join(" \xB7 ")}`;
|
|
1948
|
+
};
|
|
1949
|
+
const startTurn = () => {
|
|
1950
|
+
const assistant = renderAiMessage(messages, {
|
|
1951
|
+
role: "assistant",
|
|
1952
|
+
content: "",
|
|
1953
|
+
streaming: true,
|
|
1954
|
+
model: client.config.model
|
|
1955
|
+
});
|
|
1956
|
+
const streamH = assistant.stream();
|
|
1957
|
+
setComposerRunning(composer, true);
|
|
1958
|
+
const s = client.stream({ messages: [...history] });
|
|
1959
|
+
session = s;
|
|
1960
|
+
let failed = false;
|
|
1961
|
+
void (async () => {
|
|
1962
|
+
for await (const chunk of s) {
|
|
1963
|
+
if (chunk.type === "text") streamH.append(chunk.delta);
|
|
1964
|
+
else if (chunk.type === "error") {
|
|
1965
|
+
failed = true;
|
|
1966
|
+
streamH.fail();
|
|
1967
|
+
assistant.setError(chunk.message);
|
|
1968
|
+
}
|
|
1969
|
+
}
|
|
1970
|
+
const result = await s.done;
|
|
1971
|
+
session = null;
|
|
1972
|
+
if (!failed) streamH.done();
|
|
1973
|
+
assistant.setMeta(metaOf(result));
|
|
1974
|
+
if (result.text) history.push({ role: "assistant", content: result.text });
|
|
1975
|
+
setComposerRunning(composer, false);
|
|
1976
|
+
const queue = QUEUES.get(composer) ?? [];
|
|
1977
|
+
if (queue.length > 0) {
|
|
1978
|
+
const next = queue.shift();
|
|
1979
|
+
QUEUES.set(composer, queue);
|
|
1980
|
+
renderQueueChips(composer);
|
|
1981
|
+
emit(composer, "icen:ai-dequeue", { index: 0 });
|
|
1982
|
+
send(next);
|
|
1983
|
+
}
|
|
1984
|
+
})();
|
|
1985
|
+
};
|
|
1986
|
+
const send = (text) => {
|
|
1987
|
+
renderAiMessage(messages, { role: "user", content: text });
|
|
1988
|
+
history.push({ role: "user", content: text });
|
|
1989
|
+
startTurn();
|
|
1990
|
+
};
|
|
1991
|
+
const onSend = (e) => {
|
|
1992
|
+
const text = e.detail?.text;
|
|
1993
|
+
if (typeof text === "string" && text) send(text);
|
|
1994
|
+
};
|
|
1995
|
+
const onStop = () => {
|
|
1996
|
+
session?.cancel();
|
|
1997
|
+
};
|
|
1998
|
+
composer.addEventListener("icen:ai-send", onSend);
|
|
1999
|
+
composer.addEventListener("icen:ai-stop", onStop);
|
|
2000
|
+
disposers.push(() => {
|
|
2001
|
+
composer.removeEventListener("icen:ai-send", onSend);
|
|
2002
|
+
composer.removeEventListener("icen:ai-stop", onStop);
|
|
2003
|
+
session?.cancel();
|
|
2004
|
+
});
|
|
2005
|
+
} else if (manageRunning) {
|
|
2006
|
+
const onSend = () => setComposerRunning(composer, true);
|
|
2007
|
+
const onDone = () => setComposerRunning(composer, false);
|
|
2008
|
+
composer.addEventListener("icen:ai-send", onSend);
|
|
2009
|
+
document.addEventListener("icen:ai-done", onDone);
|
|
2010
|
+
disposers.push(() => {
|
|
2011
|
+
composer.removeEventListener("icen:ai-send", onSend);
|
|
2012
|
+
document.removeEventListener("icen:ai-done", onDone);
|
|
2013
|
+
});
|
|
2014
|
+
}
|
|
2015
|
+
return {
|
|
2016
|
+
unbind() {
|
|
2017
|
+
for (const dispose of disposers) dispose();
|
|
2018
|
+
disposers.length = 0;
|
|
2019
|
+
BOUND.delete(composer);
|
|
2020
|
+
}
|
|
2021
|
+
};
|
|
2022
|
+
}
|
|
2023
|
+
export {
|
|
2024
|
+
bindComposer,
|
|
2025
|
+
initAiComposer,
|
|
2026
|
+
setComposerCommands,
|
|
2027
|
+
setComposerModels,
|
|
2028
|
+
setComposerRefs,
|
|
2029
|
+
setComposerRunning,
|
|
2030
|
+
setComposerTodo,
|
|
2031
|
+
setComposerUsage
|
|
2032
|
+
};
|