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