@composy/chat-core 0.0.1 → 0.0.2
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/README.md +187 -123
- package/dist/index.js +2018 -1035
- package/dist/index.js.map +1 -1
- package/dist/index.min.js +1370 -1644
- package/dist/index.min.js.map +1 -0
- package/es/.dts-output-manifest.json +1 -0
- package/es/.dts.tsbuildinfo +1 -0
- package/es/constants/dom.d.ts +3 -19
- package/es/constants/dom.js +4 -4
- package/es/constants/dom.js.map +1 -1
- package/es/core/chat-manager.d.ts +33 -22
- package/es/core/chat-manager.js +174 -50
- package/es/core/chat-manager.js.map +1 -1
- package/es/core/event-emitter.d.ts +0 -1
- package/es/core/event-emitter.js +14 -7
- package/es/core/event-emitter.js.map +1 -1
- package/es/core/mention-engine.d.ts +1 -2
- package/es/core/mention-engine.js +9 -9
- package/es/core/mention-engine.js.map +1 -1
- package/es/core/message-schema.d.ts +19 -0
- package/es/core/message-schema.js +197 -0
- package/es/core/message-schema.js.map +1 -0
- package/es/core/outbox.d.ts +63 -0
- package/es/core/outbox.js +160 -0
- package/es/core/outbox.js.map +1 -0
- package/es/core/transport.d.ts +79 -0
- package/es/core/transport.js +102 -0
- package/es/core/transport.js.map +1 -0
- package/es/index.d.ts +12 -7
- package/es/index.js +167 -36
- package/es/index.js.map +1 -1
- package/es/renderer/dom-renderer.d.ts +16 -4
- package/es/renderer/dom-renderer.js +316 -61
- package/es/renderer/dom-renderer.js.map +1 -1
- package/es/renderer/style-manager.d.ts +14 -38
- package/es/renderer/style-manager.js +92 -63
- package/es/renderer/style-manager.js.map +1 -1
- package/es/theme/theme-manager.d.ts +4 -3
- package/es/theme/theme-manager.js +19 -9
- package/es/theme/theme-manager.js.map +1 -1
- package/es/theme/tokens.d.ts +0 -1
- package/es/theme/tokens.js +92 -92
- package/es/theme/tokens.js.map +1 -1
- package/es/types/index.d.ts +104 -24
- package/es/types/index.js +8 -3
- package/es/types/index.js.map +1 -0
- package/esm/constants/dom.d.ts +3 -19
- package/esm/constants/dom.js +4 -4
- package/esm/constants/dom.js.map +1 -1
- package/esm/core/chat-manager.d.ts +33 -22
- package/esm/core/chat-manager.js +174 -50
- package/esm/core/chat-manager.js.map +1 -1
- package/esm/core/event-emitter.d.ts +0 -1
- package/esm/core/event-emitter.js +14 -7
- package/esm/core/event-emitter.js.map +1 -1
- package/esm/core/mention-engine.d.ts +1 -2
- package/esm/core/mention-engine.js +9 -9
- package/esm/core/mention-engine.js.map +1 -1
- package/esm/core/message-schema.d.ts +19 -0
- package/esm/core/message-schema.js +197 -0
- package/esm/core/message-schema.js.map +1 -0
- package/esm/core/outbox.d.ts +63 -0
- package/esm/core/outbox.js +160 -0
- package/esm/core/outbox.js.map +1 -0
- package/esm/core/transport.d.ts +79 -0
- package/esm/core/transport.js +102 -0
- package/esm/core/transport.js.map +1 -0
- package/esm/index.d.ts +12 -7
- package/esm/index.js +169 -38
- package/esm/index.js.map +1 -1
- package/esm/renderer/dom-renderer.d.ts +16 -4
- package/esm/renderer/dom-renderer.js +316 -61
- package/esm/renderer/dom-renderer.js.map +1 -1
- package/esm/renderer/style-manager.d.ts +14 -38
- package/esm/renderer/style-manager.js +92 -63
- package/esm/renderer/style-manager.js.map +1 -1
- package/esm/theme/theme-manager.d.ts +4 -3
- package/esm/theme/theme-manager.js +19 -9
- package/esm/theme/theme-manager.js.map +1 -1
- package/esm/theme/tokens.d.ts +0 -1
- package/esm/theme/tokens.js +92 -92
- package/esm/theme/tokens.js.map +1 -1
- package/esm/types/index.d.ts +104 -24
- package/esm/types/index.js +8 -3
- package/esm/types/index.js.map +1 -0
- package/lib/constants/dom.cjs +4 -4
- package/lib/constants/dom.cjs.map +1 -1
- package/lib/constants/dom.d.cts +25 -0
- package/lib/constants/dom.d.ts +3 -19
- package/lib/core/chat-manager.cjs +174 -50
- package/lib/core/chat-manager.cjs.map +1 -1
- package/lib/core/chat-manager.d.cts +45 -0
- package/lib/core/chat-manager.d.ts +33 -22
- package/lib/core/event-emitter.cjs +14 -7
- package/lib/core/event-emitter.cjs.map +1 -1
- package/lib/core/event-emitter.d.cts +47 -0
- package/lib/core/event-emitter.d.ts +0 -1
- package/lib/core/mention-engine.cjs +9 -9
- package/lib/core/mention-engine.cjs.map +1 -1
- package/lib/core/mention-engine.d.cts +40 -0
- package/lib/core/mention-engine.d.ts +1 -2
- package/lib/core/message-schema.cjs +202 -0
- package/lib/core/message-schema.cjs.map +1 -0
- package/lib/core/message-schema.d.cts +19 -0
- package/lib/core/message-schema.d.ts +19 -0
- package/lib/core/outbox.cjs +162 -0
- package/lib/core/outbox.cjs.map +1 -0
- package/lib/core/outbox.d.cts +63 -0
- package/lib/core/outbox.d.ts +63 -0
- package/lib/core/transport.cjs +103 -0
- package/lib/core/transport.cjs.map +1 -0
- package/lib/core/transport.d.cts +79 -0
- package/lib/core/transport.d.ts +79 -0
- package/lib/index.cjs +175 -35
- package/lib/index.cjs.map +1 -1
- package/lib/index.d.cts +33 -0
- package/lib/index.d.ts +12 -7
- package/lib/renderer/dom-renderer.cjs +316 -61
- package/lib/renderer/dom-renderer.cjs.map +1 -1
- package/lib/renderer/dom-renderer.d.cts +91 -0
- package/lib/renderer/dom-renderer.d.ts +16 -4
- package/lib/renderer/style-manager.cjs +92 -62
- package/lib/renderer/style-manager.cjs.map +1 -1
- package/lib/renderer/style-manager.d.cts +34 -0
- package/lib/renderer/style-manager.d.ts +14 -38
- package/lib/theme/theme-manager.cjs +19 -9
- package/lib/theme/theme-manager.cjs.map +1 -1
- package/lib/theme/theme-manager.d.cts +34 -0
- package/lib/theme/theme-manager.d.ts +4 -3
- package/lib/theme/tokens.cjs +92 -92
- package/lib/theme/tokens.cjs.map +1 -1
- package/lib/theme/tokens.d.cts +56 -0
- package/lib/theme/tokens.d.ts +0 -1
- package/lib/types/index.cjs +8 -3
- package/lib/types/index.cjs.map +1 -0
- package/lib/types/index.d.cts +284 -0
- package/lib/types/index.d.ts +104 -24
- package/package.json +16 -13
- package/es/constants/dom.d.ts.map +0 -1
- package/es/core/chat-manager.d.ts.map +0 -1
- package/es/core/event-emitter.d.ts.map +0 -1
- package/es/core/mention-engine.d.ts.map +0 -1
- package/es/index.d.ts.map +0 -1
- package/es/renderer/dom-renderer.d.ts.map +0 -1
- package/es/renderer/style-manager.d.ts.map +0 -1
- package/es/theme/theme-manager.d.ts.map +0 -1
- package/es/theme/tokens.d.ts.map +0 -1
- package/es/types/index.d.ts.map +0 -1
- package/esm/constants/dom.d.ts.map +0 -1
- package/esm/core/chat-manager.d.ts.map +0 -1
- package/esm/core/event-emitter.d.ts.map +0 -1
- package/esm/core/mention-engine.d.ts.map +0 -1
- package/esm/index.d.ts.map +0 -1
- package/esm/renderer/dom-renderer.d.ts.map +0 -1
- package/esm/renderer/style-manager.d.ts.map +0 -1
- package/esm/theme/theme-manager.d.ts.map +0 -1
- package/esm/theme/tokens.d.ts.map +0 -1
- package/esm/types/index.d.ts.map +0 -1
- package/lib/constants/dom.d.ts.map +0 -1
- package/lib/core/chat-manager.d.ts.map +0 -1
- package/lib/core/event-emitter.d.ts.map +0 -1
- package/lib/core/mention-engine.d.ts.map +0 -1
- package/lib/index.d.ts.map +0 -1
- package/lib/renderer/dom-renderer.d.ts.map +0 -1
- package/lib/renderer/style-manager.d.ts.map +0 -1
- package/lib/theme/theme-manager.d.ts.map +0 -1
- package/lib/theme/tokens.d.ts.map +0 -1
- package/lib/types/index.d.ts.map +0 -1
|
@@ -1,13 +1,14 @@
|
|
|
1
1
|
/*!
|
|
2
2
|
* ***********************************
|
|
3
|
-
* @composy/chat-core v0.0.
|
|
3
|
+
* @composy/chat-core v0.0.2 *
|
|
4
4
|
* Built with rollup *
|
|
5
|
-
* Build time: 2024-
|
|
5
|
+
* Build time: 2024-10-12 21:52:18 *
|
|
6
6
|
* Build mode: production *
|
|
7
7
|
* Minified: No *
|
|
8
8
|
* ***********************************
|
|
9
9
|
*/
|
|
10
10
|
import { cx, normalizeClassPrefix } from "../constants/dom.js";
|
|
11
|
+
import { applyThemeTokens } from "./style-manager.js";
|
|
11
12
|
//#region src/renderer/dom-renderer.ts
|
|
12
13
|
const SEND_ICON_SVG = "<svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><line x1=\"22\" y1=\"2\" x2=\"11\" y2=\"13\"></line><polygon points=\"22 2 15 22 11 13 2 9 22 2\"></polygon></svg>";
|
|
13
14
|
const ICON_IMAGE = "<svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><rect width=\"18\" height=\"18\" x=\"3\" y=\"3\" rx=\"2\" ry=\"2\"/><circle cx=\"9\" cy=\"9\" r=\"2\"/><path d=\"m21 15-3.086-3.086a2 2 0 0 0-2.828 0L6 21\"/></svg>";
|
|
@@ -15,6 +16,48 @@ const ICON_PAPERCLIP = "<svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"curren
|
|
|
15
16
|
const ICON_MIC = "<svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"M12 2a3 3 0 0 0-3 3v7a3 3 0 0 0 6 0V5a3 3 0 0 0-3-3Z\"/><path d=\"M19 10v2a7 7 0 0 1-14 0v-2\"/><line x1=\"12\" x2=\"12\" y1=\"19\" y2=\"22\"/></svg>";
|
|
16
17
|
const ICON_MIC_OFF = "<svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><line x1=\"2\" x2=\"22\" y1=\"2\" y2=\"22\"/><path d=\"M18.89 13.23A7.12 7.12 0 0 0 19 12v-2\"/><path d=\"M5 10v2a7 7 0 0 0 12 5.29\"/><path d=\"M15 9.34V5a3 3 0 0 0-5.68-1.33\"/><path d=\"M9 9v3a3 3 0 0 0 5.12 2.12\"/><line x1=\"12\" x2=\"12\" y1=\"19\" y2=\"22\"/></svg>";
|
|
17
18
|
const FILE_ICON = "<svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><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\"/></svg>";
|
|
19
|
+
const ALLOWED_CUSTOM_TAGS = /* @__PURE__ */ new Set([
|
|
20
|
+
"a",
|
|
21
|
+
"b",
|
|
22
|
+
"blockquote",
|
|
23
|
+
"br",
|
|
24
|
+
"code",
|
|
25
|
+
"div",
|
|
26
|
+
"em",
|
|
27
|
+
"h1",
|
|
28
|
+
"h2",
|
|
29
|
+
"h3",
|
|
30
|
+
"h4",
|
|
31
|
+
"h5",
|
|
32
|
+
"h6",
|
|
33
|
+
"i",
|
|
34
|
+
"img",
|
|
35
|
+
"li",
|
|
36
|
+
"ol",
|
|
37
|
+
"p",
|
|
38
|
+
"pre",
|
|
39
|
+
"s",
|
|
40
|
+
"span",
|
|
41
|
+
"strong",
|
|
42
|
+
"u",
|
|
43
|
+
"ul"
|
|
44
|
+
]);
|
|
45
|
+
const DANGEROUS_CUSTOM_TAGS = /* @__PURE__ */ new Set([
|
|
46
|
+
"iframe",
|
|
47
|
+
"object",
|
|
48
|
+
"embed",
|
|
49
|
+
"script",
|
|
50
|
+
"style",
|
|
51
|
+
"svg",
|
|
52
|
+
"math",
|
|
53
|
+
"template",
|
|
54
|
+
"form",
|
|
55
|
+
"input",
|
|
56
|
+
"button",
|
|
57
|
+
"link",
|
|
58
|
+
"meta",
|
|
59
|
+
"base"
|
|
60
|
+
]);
|
|
18
61
|
const STATUS_MAP = {
|
|
19
62
|
sending: "...",
|
|
20
63
|
sent: "✓",
|
|
@@ -36,13 +79,17 @@ var DOMRenderer = class {
|
|
|
36
79
|
this.isRecording = false;
|
|
37
80
|
this.recognition = null;
|
|
38
81
|
this.lastVoiceTranscript = "";
|
|
82
|
+
this.destroyed = false;
|
|
83
|
+
this.pendingFileInputs = /* @__PURE__ */ new Set();
|
|
39
84
|
this.container = container;
|
|
85
|
+
this.document = container.ownerDocument;
|
|
40
86
|
this.config = config;
|
|
41
87
|
this.emitter = emitter;
|
|
42
88
|
this.mentionEngine = mentionEngine;
|
|
43
89
|
this.prefix = normalizeClassPrefix(config.classPrefix ?? "ldesign-chat");
|
|
44
90
|
}
|
|
45
91
|
init() {
|
|
92
|
+
if (this.destroyed) return;
|
|
46
93
|
this.container.innerHTML = "";
|
|
47
94
|
this.root = this.el("div", `${this.prefix}-container`);
|
|
48
95
|
this.messageListEl = this.el("div", `${this.prefix}-message-list`);
|
|
@@ -52,7 +99,7 @@ var DOMRenderer = class {
|
|
|
52
99
|
this.setupEventListeners();
|
|
53
100
|
}
|
|
54
101
|
updateMessageContent(id, content) {
|
|
55
|
-
const bubble = this.
|
|
102
|
+
const bubble = this.getMessageRow(id)?.querySelector(`.${this.prefix}-bubble`);
|
|
56
103
|
if (!bubble) return;
|
|
57
104
|
bubble.innerHTML = this.renderTextContent(content);
|
|
58
105
|
}
|
|
@@ -86,7 +133,7 @@ var DOMRenderer = class {
|
|
|
86
133
|
this.removeEmpty();
|
|
87
134
|
const scrollHeight = this.messageListEl.scrollHeight;
|
|
88
135
|
const scrollTop = this.messageListEl.scrollTop;
|
|
89
|
-
const fragment = document.createDocumentFragment();
|
|
136
|
+
const fragment = this.document.createDocumentFragment();
|
|
90
137
|
let lastTimestamp = 0;
|
|
91
138
|
const interval = this.config.timeGroupInterval ?? 3e5;
|
|
92
139
|
for (const message of messages) {
|
|
@@ -103,13 +150,18 @@ var DOMRenderer = class {
|
|
|
103
150
|
this.messageListEl.scrollTop = scrollTop + (this.messageListEl.scrollHeight - scrollHeight);
|
|
104
151
|
}
|
|
105
152
|
updateMessage(id, message) {
|
|
106
|
-
const rowEl = this.
|
|
153
|
+
const rowEl = this.getMessageRow(id);
|
|
107
154
|
if (!rowEl) return;
|
|
108
|
-
rowEl.replaceWith(this.createMessageRow(
|
|
155
|
+
rowEl.replaceWith(this.createMessageRow({
|
|
156
|
+
...message,
|
|
157
|
+
id
|
|
158
|
+
}));
|
|
109
159
|
}
|
|
110
160
|
removeMessage(id) {
|
|
111
|
-
this.
|
|
161
|
+
this.getMessageRow(id)?.remove();
|
|
162
|
+
this.refreshTimeDividers();
|
|
112
163
|
if (this.getMessageElements().length === 0) this.renderEmpty();
|
|
164
|
+
else this.removeEmpty();
|
|
113
165
|
}
|
|
114
166
|
scrollToBottom(smooth = true) {
|
|
115
167
|
if (typeof this.messageListEl.scrollTo === "function") {
|
|
@@ -130,6 +182,12 @@ var DOMRenderer = class {
|
|
|
130
182
|
this.loadMoreEl.textContent = loading ? "加载中..." : this.config.loadMoreText ?? "加载更多";
|
|
131
183
|
this.loadMoreEl.classList.toggle(`${this.prefix}-load-more--loading`, loading);
|
|
132
184
|
}
|
|
185
|
+
setThemeTokens(tokens) {
|
|
186
|
+
if (this.root) applyThemeTokens(this.root, tokens);
|
|
187
|
+
}
|
|
188
|
+
getRootElement() {
|
|
189
|
+
return this.root;
|
|
190
|
+
}
|
|
133
191
|
focusInput() {
|
|
134
192
|
this.textareaEl?.focus();
|
|
135
193
|
}
|
|
@@ -160,8 +218,8 @@ var DOMRenderer = class {
|
|
|
160
218
|
item.setAttribute("aria-selected", String(index === activeIndex));
|
|
161
219
|
const avatar = this.el("div", `${this.prefix}-mention-item-avatar`);
|
|
162
220
|
if (member.avatar) {
|
|
163
|
-
const img = document.createElement("img");
|
|
164
|
-
img.src = member.avatar;
|
|
221
|
+
const img = this.document.createElement("img");
|
|
222
|
+
img.src = this.safeResourceUrl(member.avatar);
|
|
165
223
|
img.alt = "";
|
|
166
224
|
avatar.appendChild(img);
|
|
167
225
|
} else avatar.textContent = member.name[0]?.toUpperCase() ?? "";
|
|
@@ -169,8 +227,12 @@ var DOMRenderer = class {
|
|
|
169
227
|
const name = this.el("span", `${this.prefix}-mention-item-name`);
|
|
170
228
|
name.textContent = member.name;
|
|
171
229
|
item.appendChild(name);
|
|
230
|
+
item.addEventListener("mousedown", (event) => event.preventDefault());
|
|
172
231
|
item.addEventListener("click", () => {
|
|
173
|
-
this.mentionEngine.
|
|
232
|
+
const triggerPosition = this.mentionEngine.getState().triggerPosition;
|
|
233
|
+
const selectionEnd = this.textareaEl?.selectionEnd;
|
|
234
|
+
const selected = this.mentionEngine.select(index);
|
|
235
|
+
if (selected) this.insertMention(selected, triggerPosition, selectionEnd);
|
|
174
236
|
});
|
|
175
237
|
this.mentionPanel?.appendChild(item);
|
|
176
238
|
});
|
|
@@ -179,8 +241,13 @@ var DOMRenderer = class {
|
|
|
179
241
|
if (this.mentionPanel) this.mentionPanel.style.display = "none";
|
|
180
242
|
}
|
|
181
243
|
destroy() {
|
|
244
|
+
if (this.destroyed) return;
|
|
245
|
+
this.destroyed = true;
|
|
182
246
|
this.stopVoice();
|
|
183
|
-
this.
|
|
247
|
+
this.finishVoice();
|
|
248
|
+
this.pendingFileInputs.forEach((input) => input.remove());
|
|
249
|
+
this.pendingFileInputs.clear();
|
|
250
|
+
this.root?.remove();
|
|
184
251
|
}
|
|
185
252
|
renderInputArea() {
|
|
186
253
|
this.inputArea = this.el("div", `${this.prefix}-input-area`);
|
|
@@ -190,7 +257,7 @@ var DOMRenderer = class {
|
|
|
190
257
|
this.mentionPanel.setAttribute("role", "listbox");
|
|
191
258
|
wrapper.appendChild(this.mentionPanel);
|
|
192
259
|
if (this.config.enableToolbar !== false) this.renderToolbar(wrapper);
|
|
193
|
-
this.textareaEl = document.createElement("textarea");
|
|
260
|
+
this.textareaEl = this.document.createElement("textarea");
|
|
194
261
|
this.textareaEl.className = `${this.prefix}-textarea`;
|
|
195
262
|
this.textareaEl.placeholder = this.config.inputPlaceholder ?? "输入消息...";
|
|
196
263
|
this.textareaEl.rows = 1;
|
|
@@ -201,7 +268,7 @@ var DOMRenderer = class {
|
|
|
201
268
|
this.dropOverlay.style.display = "none";
|
|
202
269
|
wrapper.appendChild(this.dropOverlay);
|
|
203
270
|
this.inputArea.appendChild(wrapper);
|
|
204
|
-
this.sendBtn = document.createElement("button");
|
|
271
|
+
this.sendBtn = this.document.createElement("button");
|
|
205
272
|
this.sendBtn.className = `${this.prefix}-send-btn`;
|
|
206
273
|
this.sendBtn.innerHTML = SEND_ICON_SVG;
|
|
207
274
|
this.sendBtn.title = "发送";
|
|
@@ -231,7 +298,7 @@ var DOMRenderer = class {
|
|
|
231
298
|
wrapper.insertBefore(this.toolbarEl, wrapper.firstChild);
|
|
232
299
|
}
|
|
233
300
|
createToolbarBtn(iconSvg, title) {
|
|
234
|
-
const btn = document.createElement("button");
|
|
301
|
+
const btn = this.document.createElement("button");
|
|
235
302
|
btn.className = `${this.prefix}-toolbar-btn`;
|
|
236
303
|
btn.innerHTML = iconSvg;
|
|
237
304
|
btn.title = title;
|
|
@@ -239,21 +306,27 @@ var DOMRenderer = class {
|
|
|
239
306
|
return btn;
|
|
240
307
|
}
|
|
241
308
|
openFilePicker(accept) {
|
|
242
|
-
const input = document.createElement("input");
|
|
309
|
+
const input = this.document.createElement("input");
|
|
243
310
|
input.type = "file";
|
|
244
311
|
input.accept = accept;
|
|
245
312
|
input.multiple = true;
|
|
246
313
|
input.style.display = "none";
|
|
314
|
+
this.pendingFileInputs.add(input);
|
|
315
|
+
const cleanup = () => {
|
|
316
|
+
input.remove();
|
|
317
|
+
this.pendingFileInputs.delete(input);
|
|
318
|
+
};
|
|
247
319
|
input.addEventListener("change", () => {
|
|
248
320
|
const files = Array.from(input.files ?? []);
|
|
249
|
-
|
|
321
|
+
cleanup();
|
|
250
322
|
if (!files.length) return;
|
|
251
323
|
const isImage = accept.startsWith("image");
|
|
252
324
|
this.emitter.emit(isImage ? "toolbar-image" : "toolbar-file", files);
|
|
253
325
|
if (isImage) this.config.onImageUpload?.(files);
|
|
254
326
|
else this.config.onFileUpload?.(files);
|
|
255
327
|
});
|
|
256
|
-
|
|
328
|
+
input.addEventListener("cancel", cleanup);
|
|
329
|
+
(this.document.body ?? this.document.documentElement).appendChild(input);
|
|
257
330
|
input.click();
|
|
258
331
|
}
|
|
259
332
|
toggleVoice() {
|
|
@@ -261,8 +334,8 @@ var DOMRenderer = class {
|
|
|
261
334
|
else this.startVoice();
|
|
262
335
|
}
|
|
263
336
|
startVoice() {
|
|
264
|
-
|
|
265
|
-
const SpeechRecognition =
|
|
337
|
+
const view = this.document.defaultView;
|
|
338
|
+
const SpeechRecognition = view?.SpeechRecognition || view?.webkitSpeechRecognition;
|
|
266
339
|
if (!SpeechRecognition) {
|
|
267
340
|
this.emitter.emit("toolbar-voice-unsupported");
|
|
268
341
|
this.config.onVoiceUnsupported?.();
|
|
@@ -270,22 +343,34 @@ var DOMRenderer = class {
|
|
|
270
343
|
}
|
|
271
344
|
try {
|
|
272
345
|
this.lastVoiceTranscript = "";
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
this.
|
|
281
|
-
|
|
346
|
+
const recognition = new SpeechRecognition();
|
|
347
|
+
const finalSegments = /* @__PURE__ */ new Map();
|
|
348
|
+
recognition.continuous = false;
|
|
349
|
+
recognition.interimResults = true;
|
|
350
|
+
recognition.lang = this.config.voiceLang ?? "zh-CN";
|
|
351
|
+
this.recognition = recognition;
|
|
352
|
+
recognition.onresult = (event) => {
|
|
353
|
+
if (this.destroyed || this.recognition !== recognition) return;
|
|
354
|
+
const firstChangedResult = Math.max(0, event.resultIndex ?? 0);
|
|
355
|
+
const newFinalSegments = [];
|
|
356
|
+
for (let index = firstChangedResult; index < event.results.length; index++) {
|
|
357
|
+
const result = event.results[index];
|
|
358
|
+
if (!result?.isFinal || finalSegments.has(index)) continue;
|
|
359
|
+
finalSegments.set(index, result[0].transcript);
|
|
360
|
+
newFinalSegments.push(result[0].transcript);
|
|
361
|
+
}
|
|
362
|
+
this.lastVoiceTranscript = [...finalSegments.entries()].sort(([left], [right]) => left - right).map(([, transcript]) => transcript).join("");
|
|
363
|
+
if (newFinalSegments.length && this.textareaEl) this.insertTextAtCursor(newFinalSegments.join(""));
|
|
282
364
|
};
|
|
283
|
-
|
|
365
|
+
recognition.onend = () => {
|
|
366
|
+
if (this.destroyed || this.recognition !== recognition) return;
|
|
367
|
+
const transcript = this.lastVoiceTranscript;
|
|
284
368
|
this.finishVoice();
|
|
285
|
-
this.emitter.emit("toolbar-voice-end",
|
|
286
|
-
this.config.onVoiceEnd?.(
|
|
369
|
+
this.emitter.emit("toolbar-voice-end", transcript);
|
|
370
|
+
this.config.onVoiceEnd?.(transcript);
|
|
287
371
|
};
|
|
288
|
-
|
|
372
|
+
recognition.onerror = (error) => {
|
|
373
|
+
if (this.destroyed || this.recognition !== recognition) return;
|
|
289
374
|
this.finishVoice();
|
|
290
375
|
this.emitter.emit("toolbar-voice-error", error);
|
|
291
376
|
this.config.onVoiceError?.(error);
|
|
@@ -293,7 +378,7 @@ var DOMRenderer = class {
|
|
|
293
378
|
this.isRecording = true;
|
|
294
379
|
this.voiceBtn?.classList.add(`${this.prefix}-toolbar-btn--active`);
|
|
295
380
|
if (this.voiceBtn) this.voiceBtn.innerHTML = ICON_MIC_OFF;
|
|
296
|
-
|
|
381
|
+
recognition.start();
|
|
297
382
|
this.emitter.emit("toolbar-voice-start");
|
|
298
383
|
this.config.onVoiceStart?.();
|
|
299
384
|
} catch (error) {
|
|
@@ -305,7 +390,6 @@ var DOMRenderer = class {
|
|
|
305
390
|
stopVoice() {
|
|
306
391
|
if (!this.recognition) return;
|
|
307
392
|
const recognition = this.recognition;
|
|
308
|
-
this.recognition = null;
|
|
309
393
|
try {
|
|
310
394
|
recognition.stop();
|
|
311
395
|
} catch {
|
|
@@ -329,7 +413,8 @@ var DOMRenderer = class {
|
|
|
329
413
|
if (this.config.enableMention && this.textareaEl) this.mentionEngine.handleInput(this.textareaEl.value, this.textareaEl.selectionStart);
|
|
330
414
|
});
|
|
331
415
|
this.textareaEl.addEventListener("keydown", (event) => {
|
|
332
|
-
|
|
416
|
+
const mentionState = this.mentionEngine.getState();
|
|
417
|
+
if (mentionState.active) {
|
|
333
418
|
if (event.key === "ArrowUp") {
|
|
334
419
|
event.preventDefault();
|
|
335
420
|
this.mentionEngine.moveUp();
|
|
@@ -338,8 +423,10 @@ var DOMRenderer = class {
|
|
|
338
423
|
this.mentionEngine.moveDown();
|
|
339
424
|
} else if (event.key === "Enter" && !event.shiftKey) {
|
|
340
425
|
event.preventDefault();
|
|
426
|
+
const triggerPosition = mentionState.triggerPosition;
|
|
427
|
+
const selectionEnd = this.textareaEl.selectionEnd;
|
|
341
428
|
const member = this.mentionEngine.selectCurrent();
|
|
342
|
-
if (member) this.insertMention(member);
|
|
429
|
+
if (member) this.insertMention(member, triggerPosition, selectionEnd);
|
|
343
430
|
} else if (event.key === "Escape") {
|
|
344
431
|
event.preventDefault();
|
|
345
432
|
this.mentionEngine.deactivate();
|
|
@@ -424,14 +511,14 @@ var DOMRenderer = class {
|
|
|
424
511
|
if (this.config.clearInputOnSend !== false) this.clearInput();
|
|
425
512
|
this.mentionEngine.resetSelected();
|
|
426
513
|
}
|
|
427
|
-
insertMention(member) {
|
|
514
|
+
insertMention(member, triggerPosition, selectionEnd) {
|
|
428
515
|
if (!this.textareaEl) return;
|
|
429
516
|
const state = this.mentionEngine.getState();
|
|
430
517
|
const text = this.textareaEl.value;
|
|
431
|
-
const
|
|
518
|
+
const end = selectionEnd ?? this.textareaEl.selectionEnd;
|
|
432
519
|
const insertText = this.mentionEngine.getInsertText(member);
|
|
433
|
-
const before = text.slice(0, state.triggerPosition);
|
|
434
|
-
const after = text.slice(
|
|
520
|
+
const before = text.slice(0, triggerPosition ?? state.triggerPosition);
|
|
521
|
+
const after = text.slice(end);
|
|
435
522
|
this.textareaEl.value = this.limitInput(before + insertText + after);
|
|
436
523
|
const newPos = Math.min(before.length + insertText.length, this.textareaEl.value.length);
|
|
437
524
|
this.textareaEl.setSelectionRange(newPos, newPos);
|
|
@@ -455,8 +542,7 @@ var DOMRenderer = class {
|
|
|
455
542
|
autoResize() {
|
|
456
543
|
if (!this.textareaEl) return;
|
|
457
544
|
this.textareaEl.style.height = "auto";
|
|
458
|
-
|
|
459
|
-
this.textareaEl.style.height = `${Math.min(this.textareaEl.scrollHeight, maxHeight)}px`;
|
|
545
|
+
this.textareaEl.style.height = `${this.textareaEl.scrollHeight}px`;
|
|
460
546
|
}
|
|
461
547
|
updateSendButtonState() {
|
|
462
548
|
if (!this.sendBtn) return;
|
|
@@ -466,21 +552,28 @@ var DOMRenderer = class {
|
|
|
466
552
|
this.loadMoreEl = this.el("div", `${this.prefix}-load-more`);
|
|
467
553
|
this.loadMoreEl.textContent = this.config.loadMoreText ?? "加载更多";
|
|
468
554
|
this.loadMoreEl.addEventListener("click", () => {
|
|
469
|
-
if (this.loadingMore) return;
|
|
555
|
+
if (this.destroyed || this.loadingMore) return;
|
|
470
556
|
this.setLoadingMore(true);
|
|
471
557
|
this.emitter.emit("load-more");
|
|
472
|
-
|
|
473
|
-
this.setLoadingMore(false);
|
|
474
|
-
});
|
|
558
|
+
this.loadMore();
|
|
475
559
|
});
|
|
476
560
|
this.messageListEl.appendChild(this.loadMoreEl);
|
|
477
561
|
}
|
|
562
|
+
async loadMore() {
|
|
563
|
+
try {
|
|
564
|
+
await this.config.onLoadMore?.();
|
|
565
|
+
} catch (error) {
|
|
566
|
+
this.emitter.emit("load-more-error", error);
|
|
567
|
+
} finally {
|
|
568
|
+
if (!this.destroyed) this.setLoadingMore(false);
|
|
569
|
+
}
|
|
570
|
+
}
|
|
478
571
|
renderTimeDivider(timestamp) {
|
|
479
572
|
this.messageListEl.appendChild(this.createTimeDivider(timestamp));
|
|
480
573
|
}
|
|
481
574
|
createTimeDivider(timestamp) {
|
|
482
575
|
const divider = this.el("div", `${this.prefix}-time-divider`);
|
|
483
|
-
const span = document.createElement("span");
|
|
576
|
+
const span = this.document.createElement("span");
|
|
484
577
|
span.textContent = this.formatTime(timestamp);
|
|
485
578
|
divider.appendChild(span);
|
|
486
579
|
return divider;
|
|
@@ -522,8 +615,8 @@ var DOMRenderer = class {
|
|
|
522
615
|
createAvatar(name, avatarUrl) {
|
|
523
616
|
const avatar = this.el("div", `${this.prefix}-avatar`);
|
|
524
617
|
if (avatarUrl) {
|
|
525
|
-
const img = document.createElement("img");
|
|
526
|
-
img.src = avatarUrl;
|
|
618
|
+
const img = this.document.createElement("img");
|
|
619
|
+
img.src = this.safeResourceUrl(avatarUrl);
|
|
527
620
|
img.alt = name;
|
|
528
621
|
avatar.appendChild(img);
|
|
529
622
|
} else avatar.textContent = name[0]?.toUpperCase() ?? "";
|
|
@@ -559,8 +652,8 @@ var DOMRenderer = class {
|
|
|
559
652
|
case "image": {
|
|
560
653
|
const bubble = this.el("div", cx(`${p}-bubble`, dirClass, `${p}-bubble--image`));
|
|
561
654
|
if (message.image) {
|
|
562
|
-
const img = document.createElement("img");
|
|
563
|
-
img.src = message.image.thumbUrl ?? message.image.url;
|
|
655
|
+
const img = this.document.createElement("img");
|
|
656
|
+
img.src = this.safeResourceUrl(message.image.thumbUrl ?? message.image.url);
|
|
564
657
|
img.alt = message.content ?? "";
|
|
565
658
|
if (message.image.width) img.width = Math.min(message.image.width, 240);
|
|
566
659
|
bubble.appendChild(img);
|
|
@@ -586,17 +679,89 @@ var DOMRenderer = class {
|
|
|
586
679
|
}
|
|
587
680
|
case "custom": {
|
|
588
681
|
const bubble = this.el("div", cx(`${p}-bubble`, dirClass));
|
|
589
|
-
if (message.customHtml) bubble
|
|
682
|
+
if (message.customHtml) this.setSanitizedCustomHtml(bubble, message.customHtml);
|
|
590
683
|
else bubble.textContent = message.content ?? "";
|
|
591
684
|
return bubble;
|
|
592
685
|
}
|
|
593
686
|
default: {
|
|
594
687
|
const bubble = this.el("div", cx(`${p}-bubble`, dirClass, message.streaming && `${p}-bubble--streaming`));
|
|
595
|
-
|
|
688
|
+
if (message.contentBlocks?.length) this.renderContentBlocks(bubble, message.contentBlocks);
|
|
689
|
+
else bubble.innerHTML = this.renderTextContent(message.content ?? "", message.mentions);
|
|
596
690
|
return bubble;
|
|
597
691
|
}
|
|
598
692
|
}
|
|
599
693
|
}
|
|
694
|
+
renderContentBlocks(target, blocks) {
|
|
695
|
+
target.replaceChildren();
|
|
696
|
+
for (const block of blocks) switch (block.type) {
|
|
697
|
+
case "text":
|
|
698
|
+
target.appendChild(this.document.createTextNode(block.text));
|
|
699
|
+
break;
|
|
700
|
+
case "mention": {
|
|
701
|
+
const mention = this.el("span", `${this.prefix}-mention-tag`);
|
|
702
|
+
mention.dataset.mentionId = block.id;
|
|
703
|
+
mention.textContent = `@${block.label}`;
|
|
704
|
+
target.appendChild(mention);
|
|
705
|
+
break;
|
|
706
|
+
}
|
|
707
|
+
case "link": {
|
|
708
|
+
const link = this.document.createElement("a");
|
|
709
|
+
const safeUrl = this.safeUrl(block.url, [
|
|
710
|
+
"http:",
|
|
711
|
+
"https:",
|
|
712
|
+
"mailto:"
|
|
713
|
+
]);
|
|
714
|
+
link.className = `${this.prefix}-link`;
|
|
715
|
+
link.textContent = block.label ?? block.url;
|
|
716
|
+
if (safeUrl) {
|
|
717
|
+
link.href = safeUrl;
|
|
718
|
+
link.target = "_blank";
|
|
719
|
+
link.rel = "noopener noreferrer";
|
|
720
|
+
}
|
|
721
|
+
target.appendChild(link);
|
|
722
|
+
break;
|
|
723
|
+
}
|
|
724
|
+
case "code": {
|
|
725
|
+
const code = this.document.createElement("code");
|
|
726
|
+
code.textContent = block.code;
|
|
727
|
+
if (block.language) code.dataset.language = block.language;
|
|
728
|
+
target.appendChild(code);
|
|
729
|
+
break;
|
|
730
|
+
}
|
|
731
|
+
case "image": {
|
|
732
|
+
const image = this.document.createElement("img");
|
|
733
|
+
const safeUrl = this.safeResourceUrl(block.src);
|
|
734
|
+
const width = block.width;
|
|
735
|
+
const height = block.height;
|
|
736
|
+
if (safeUrl) image.src = safeUrl;
|
|
737
|
+
image.alt = block.alt ?? "";
|
|
738
|
+
image.loading = "lazy";
|
|
739
|
+
if (Number.isFinite(width) && width > 0) image.width = width;
|
|
740
|
+
if (Number.isFinite(height) && height > 0) image.height = height;
|
|
741
|
+
target.appendChild(image);
|
|
742
|
+
break;
|
|
743
|
+
}
|
|
744
|
+
case "file": {
|
|
745
|
+
const file = this.document.createElement("a");
|
|
746
|
+
const safeUrl = this.safeResourceUrl(block.url);
|
|
747
|
+
file.className = `${this.prefix}-file-link`;
|
|
748
|
+
file.textContent = block.name;
|
|
749
|
+
if (safeUrl) {
|
|
750
|
+
file.href = safeUrl;
|
|
751
|
+
file.target = "_blank";
|
|
752
|
+
file.rel = "noopener noreferrer";
|
|
753
|
+
}
|
|
754
|
+
target.appendChild(file);
|
|
755
|
+
break;
|
|
756
|
+
}
|
|
757
|
+
case "break":
|
|
758
|
+
target.appendChild(this.document.createElement("br"));
|
|
759
|
+
break;
|
|
760
|
+
case "custom":
|
|
761
|
+
target.appendChild(this.document.createTextNode(block.fallback ?? `[${block.kind}]`));
|
|
762
|
+
break;
|
|
763
|
+
}
|
|
764
|
+
}
|
|
600
765
|
renderTextContent(content, mentions) {
|
|
601
766
|
let html = this.escapeHtml(content);
|
|
602
767
|
if (mentions?.length) for (const mention of mentions) {
|
|
@@ -605,6 +770,89 @@ var DOMRenderer = class {
|
|
|
605
770
|
}
|
|
606
771
|
return html.replace(/(https?:\/\/[^\s<]+)/g, `<a class="${this.prefix}-link" href="$1" target="_blank" rel="noopener noreferrer">$1</a>`);
|
|
607
772
|
}
|
|
773
|
+
setSanitizedCustomHtml(target, html) {
|
|
774
|
+
const template = this.document.createElement("template");
|
|
775
|
+
template.innerHTML = html;
|
|
776
|
+
const fragment = this.document.createDocumentFragment();
|
|
777
|
+
for (const node of Array.from(template.content.childNodes)) {
|
|
778
|
+
const sanitized = this.sanitizeCustomNode(node);
|
|
779
|
+
if (sanitized) fragment.appendChild(sanitized);
|
|
780
|
+
}
|
|
781
|
+
target.replaceChildren(fragment);
|
|
782
|
+
}
|
|
783
|
+
sanitizeCustomNode(node) {
|
|
784
|
+
if (node.nodeType === 3) return this.document.createTextNode(node.textContent ?? "");
|
|
785
|
+
if (node.nodeType !== 1) return null;
|
|
786
|
+
const source = node;
|
|
787
|
+
const tagName = source.tagName.toLowerCase();
|
|
788
|
+
if (DANGEROUS_CUSTOM_TAGS.has(tagName)) return null;
|
|
789
|
+
if (!ALLOWED_CUSTOM_TAGS.has(tagName)) {
|
|
790
|
+
const fragment = this.document.createDocumentFragment();
|
|
791
|
+
for (const child of Array.from(source.childNodes)) {
|
|
792
|
+
const sanitized = this.sanitizeCustomNode(child);
|
|
793
|
+
if (sanitized) fragment.appendChild(sanitized);
|
|
794
|
+
}
|
|
795
|
+
return fragment;
|
|
796
|
+
}
|
|
797
|
+
const element = this.document.createElement(tagName);
|
|
798
|
+
const title = source.getAttribute("title");
|
|
799
|
+
if (title) element.setAttribute("title", title);
|
|
800
|
+
const ariaLabel = source.getAttribute("aria-label");
|
|
801
|
+
if (ariaLabel) element.setAttribute("aria-label", ariaLabel);
|
|
802
|
+
if (tagName === "a") {
|
|
803
|
+
const href = source.getAttribute("href");
|
|
804
|
+
const safeHref = href ? this.safeUrl(href, [
|
|
805
|
+
"http:",
|
|
806
|
+
"https:",
|
|
807
|
+
"mailto:"
|
|
808
|
+
]) : null;
|
|
809
|
+
if (safeHref) element.setAttribute("href", safeHref);
|
|
810
|
+
const target = source.getAttribute("target");
|
|
811
|
+
if (target === "_blank" || target === "_self") element.setAttribute("target", target);
|
|
812
|
+
element.setAttribute("rel", "noopener noreferrer");
|
|
813
|
+
}
|
|
814
|
+
if (tagName === "img") {
|
|
815
|
+
const src = source.getAttribute("src");
|
|
816
|
+
const safeSrc = src ? this.safeUrl(src, [
|
|
817
|
+
"http:",
|
|
818
|
+
"https:",
|
|
819
|
+
"blob:"
|
|
820
|
+
]) : null;
|
|
821
|
+
if (safeSrc) element.setAttribute("src", safeSrc);
|
|
822
|
+
element.setAttribute("alt", source.getAttribute("alt") ?? "");
|
|
823
|
+
for (const dimension of ["width", "height"]) {
|
|
824
|
+
const value = Number(source.getAttribute(dimension));
|
|
825
|
+
if (Number.isFinite(value) && value > 0) element.setAttribute(dimension, String(value));
|
|
826
|
+
}
|
|
827
|
+
element.setAttribute("loading", "lazy");
|
|
828
|
+
}
|
|
829
|
+
for (const child of Array.from(source.childNodes)) {
|
|
830
|
+
const sanitized = this.sanitizeCustomNode(child);
|
|
831
|
+
if (sanitized) element.appendChild(sanitized);
|
|
832
|
+
}
|
|
833
|
+
return element;
|
|
834
|
+
}
|
|
835
|
+
safeResourceUrl(value) {
|
|
836
|
+
if (!value) return "";
|
|
837
|
+
return this.safeUrl(value, [
|
|
838
|
+
"http:",
|
|
839
|
+
"https:",
|
|
840
|
+
"blob:"
|
|
841
|
+
]) ?? "";
|
|
842
|
+
}
|
|
843
|
+
safeUrl(value, protocols) {
|
|
844
|
+
const trimmed = value.trim();
|
|
845
|
+
if (!trimmed || [...trimmed].some((character) => {
|
|
846
|
+
const code = character.charCodeAt(0);
|
|
847
|
+
return code < 32 || code === 127;
|
|
848
|
+
})) return null;
|
|
849
|
+
try {
|
|
850
|
+
const resolved = new URL(trimmed, this.document.baseURI);
|
|
851
|
+
return protocols.includes(resolved.protocol) ? trimmed : null;
|
|
852
|
+
} catch {
|
|
853
|
+
return null;
|
|
854
|
+
}
|
|
855
|
+
}
|
|
608
856
|
renderEmpty() {
|
|
609
857
|
if (this.messageListEl.querySelector(`.${this.prefix}-empty`)) return;
|
|
610
858
|
const empty = this.el("div", `${this.prefix}-empty`);
|
|
@@ -615,13 +863,26 @@ var DOMRenderer = class {
|
|
|
615
863
|
this.messageListEl.querySelector(`.${this.prefix}-empty`)?.remove();
|
|
616
864
|
}
|
|
617
865
|
el(tag, className) {
|
|
618
|
-
const el = document.createElement(tag);
|
|
866
|
+
const el = this.document.createElement(tag);
|
|
619
867
|
el.className = className;
|
|
620
868
|
return el;
|
|
621
869
|
}
|
|
622
870
|
getMessageElements() {
|
|
623
871
|
return Array.from(this.messageListEl.querySelectorAll(`.${this.prefix}-message-row`));
|
|
624
872
|
}
|
|
873
|
+
getMessageRow(id) {
|
|
874
|
+
return this.getMessageElements().find((element) => element.dataset.msgId === id) ?? null;
|
|
875
|
+
}
|
|
876
|
+
refreshTimeDividers() {
|
|
877
|
+
this.messageListEl.querySelectorAll(`.${this.prefix}-time-divider`).forEach((divider) => divider.remove());
|
|
878
|
+
const interval = this.config.timeGroupInterval ?? 3e5;
|
|
879
|
+
let previousTimestamp = 0;
|
|
880
|
+
for (const row of this.getMessageElements()) {
|
|
881
|
+
const timestamp = Number(row.dataset.timestamp ?? 0);
|
|
882
|
+
if (this.config.showTimestamp !== false && timestamp - previousTimestamp > interval) row.before(this.createTimeDivider(timestamp));
|
|
883
|
+
previousTimestamp = timestamp;
|
|
884
|
+
}
|
|
885
|
+
}
|
|
625
886
|
formatTime(timestamp) {
|
|
626
887
|
if (this.config.formatTime) return this.config.formatTime(timestamp);
|
|
627
888
|
const date = new Date(timestamp);
|
|
@@ -653,14 +914,8 @@ var DOMRenderer = class {
|
|
|
653
914
|
if (!max || max <= 0) return content;
|
|
654
915
|
return content.slice(0, max);
|
|
655
916
|
}
|
|
656
|
-
getCssSizeValue(name, fallback) {
|
|
657
|
-
if (typeof window === "undefined" || !this.root) return fallback;
|
|
658
|
-
const value = window.getComputedStyle(this.root).getPropertyValue(name).trim();
|
|
659
|
-
const parsed = Number.parseFloat(value);
|
|
660
|
-
return Number.isFinite(parsed) ? parsed : fallback;
|
|
661
|
-
}
|
|
662
917
|
escapeHtml(str) {
|
|
663
|
-
const div = document.createElement("div");
|
|
918
|
+
const div = this.document.createElement("div");
|
|
664
919
|
div.textContent = str;
|
|
665
920
|
return div.innerHTML;
|
|
666
921
|
}
|