@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.
Files changed (168) hide show
  1. package/README.md +187 -123
  2. package/dist/index.js +2018 -1035
  3. package/dist/index.js.map +1 -1
  4. package/dist/index.min.js +1370 -1644
  5. package/dist/index.min.js.map +1 -0
  6. package/es/.dts-output-manifest.json +1 -0
  7. package/es/.dts.tsbuildinfo +1 -0
  8. package/es/constants/dom.d.ts +3 -19
  9. package/es/constants/dom.js +4 -4
  10. package/es/constants/dom.js.map +1 -1
  11. package/es/core/chat-manager.d.ts +33 -22
  12. package/es/core/chat-manager.js +174 -50
  13. package/es/core/chat-manager.js.map +1 -1
  14. package/es/core/event-emitter.d.ts +0 -1
  15. package/es/core/event-emitter.js +14 -7
  16. package/es/core/event-emitter.js.map +1 -1
  17. package/es/core/mention-engine.d.ts +1 -2
  18. package/es/core/mention-engine.js +9 -9
  19. package/es/core/mention-engine.js.map +1 -1
  20. package/es/core/message-schema.d.ts +19 -0
  21. package/es/core/message-schema.js +197 -0
  22. package/es/core/message-schema.js.map +1 -0
  23. package/es/core/outbox.d.ts +63 -0
  24. package/es/core/outbox.js +160 -0
  25. package/es/core/outbox.js.map +1 -0
  26. package/es/core/transport.d.ts +79 -0
  27. package/es/core/transport.js +102 -0
  28. package/es/core/transport.js.map +1 -0
  29. package/es/index.d.ts +12 -7
  30. package/es/index.js +167 -36
  31. package/es/index.js.map +1 -1
  32. package/es/renderer/dom-renderer.d.ts +16 -4
  33. package/es/renderer/dom-renderer.js +316 -61
  34. package/es/renderer/dom-renderer.js.map +1 -1
  35. package/es/renderer/style-manager.d.ts +14 -38
  36. package/es/renderer/style-manager.js +92 -63
  37. package/es/renderer/style-manager.js.map +1 -1
  38. package/es/theme/theme-manager.d.ts +4 -3
  39. package/es/theme/theme-manager.js +19 -9
  40. package/es/theme/theme-manager.js.map +1 -1
  41. package/es/theme/tokens.d.ts +0 -1
  42. package/es/theme/tokens.js +92 -92
  43. package/es/theme/tokens.js.map +1 -1
  44. package/es/types/index.d.ts +104 -24
  45. package/es/types/index.js +8 -3
  46. package/es/types/index.js.map +1 -0
  47. package/esm/constants/dom.d.ts +3 -19
  48. package/esm/constants/dom.js +4 -4
  49. package/esm/constants/dom.js.map +1 -1
  50. package/esm/core/chat-manager.d.ts +33 -22
  51. package/esm/core/chat-manager.js +174 -50
  52. package/esm/core/chat-manager.js.map +1 -1
  53. package/esm/core/event-emitter.d.ts +0 -1
  54. package/esm/core/event-emitter.js +14 -7
  55. package/esm/core/event-emitter.js.map +1 -1
  56. package/esm/core/mention-engine.d.ts +1 -2
  57. package/esm/core/mention-engine.js +9 -9
  58. package/esm/core/mention-engine.js.map +1 -1
  59. package/esm/core/message-schema.d.ts +19 -0
  60. package/esm/core/message-schema.js +197 -0
  61. package/esm/core/message-schema.js.map +1 -0
  62. package/esm/core/outbox.d.ts +63 -0
  63. package/esm/core/outbox.js +160 -0
  64. package/esm/core/outbox.js.map +1 -0
  65. package/esm/core/transport.d.ts +79 -0
  66. package/esm/core/transport.js +102 -0
  67. package/esm/core/transport.js.map +1 -0
  68. package/esm/index.d.ts +12 -7
  69. package/esm/index.js +169 -38
  70. package/esm/index.js.map +1 -1
  71. package/esm/renderer/dom-renderer.d.ts +16 -4
  72. package/esm/renderer/dom-renderer.js +316 -61
  73. package/esm/renderer/dom-renderer.js.map +1 -1
  74. package/esm/renderer/style-manager.d.ts +14 -38
  75. package/esm/renderer/style-manager.js +92 -63
  76. package/esm/renderer/style-manager.js.map +1 -1
  77. package/esm/theme/theme-manager.d.ts +4 -3
  78. package/esm/theme/theme-manager.js +19 -9
  79. package/esm/theme/theme-manager.js.map +1 -1
  80. package/esm/theme/tokens.d.ts +0 -1
  81. package/esm/theme/tokens.js +92 -92
  82. package/esm/theme/tokens.js.map +1 -1
  83. package/esm/types/index.d.ts +104 -24
  84. package/esm/types/index.js +8 -3
  85. package/esm/types/index.js.map +1 -0
  86. package/lib/constants/dom.cjs +4 -4
  87. package/lib/constants/dom.cjs.map +1 -1
  88. package/lib/constants/dom.d.cts +25 -0
  89. package/lib/constants/dom.d.ts +3 -19
  90. package/lib/core/chat-manager.cjs +174 -50
  91. package/lib/core/chat-manager.cjs.map +1 -1
  92. package/lib/core/chat-manager.d.cts +45 -0
  93. package/lib/core/chat-manager.d.ts +33 -22
  94. package/lib/core/event-emitter.cjs +14 -7
  95. package/lib/core/event-emitter.cjs.map +1 -1
  96. package/lib/core/event-emitter.d.cts +47 -0
  97. package/lib/core/event-emitter.d.ts +0 -1
  98. package/lib/core/mention-engine.cjs +9 -9
  99. package/lib/core/mention-engine.cjs.map +1 -1
  100. package/lib/core/mention-engine.d.cts +40 -0
  101. package/lib/core/mention-engine.d.ts +1 -2
  102. package/lib/core/message-schema.cjs +202 -0
  103. package/lib/core/message-schema.cjs.map +1 -0
  104. package/lib/core/message-schema.d.cts +19 -0
  105. package/lib/core/message-schema.d.ts +19 -0
  106. package/lib/core/outbox.cjs +162 -0
  107. package/lib/core/outbox.cjs.map +1 -0
  108. package/lib/core/outbox.d.cts +63 -0
  109. package/lib/core/outbox.d.ts +63 -0
  110. package/lib/core/transport.cjs +103 -0
  111. package/lib/core/transport.cjs.map +1 -0
  112. package/lib/core/transport.d.cts +79 -0
  113. package/lib/core/transport.d.ts +79 -0
  114. package/lib/index.cjs +175 -35
  115. package/lib/index.cjs.map +1 -1
  116. package/lib/index.d.cts +33 -0
  117. package/lib/index.d.ts +12 -7
  118. package/lib/renderer/dom-renderer.cjs +316 -61
  119. package/lib/renderer/dom-renderer.cjs.map +1 -1
  120. package/lib/renderer/dom-renderer.d.cts +91 -0
  121. package/lib/renderer/dom-renderer.d.ts +16 -4
  122. package/lib/renderer/style-manager.cjs +92 -62
  123. package/lib/renderer/style-manager.cjs.map +1 -1
  124. package/lib/renderer/style-manager.d.cts +34 -0
  125. package/lib/renderer/style-manager.d.ts +14 -38
  126. package/lib/theme/theme-manager.cjs +19 -9
  127. package/lib/theme/theme-manager.cjs.map +1 -1
  128. package/lib/theme/theme-manager.d.cts +34 -0
  129. package/lib/theme/theme-manager.d.ts +4 -3
  130. package/lib/theme/tokens.cjs +92 -92
  131. package/lib/theme/tokens.cjs.map +1 -1
  132. package/lib/theme/tokens.d.cts +56 -0
  133. package/lib/theme/tokens.d.ts +0 -1
  134. package/lib/types/index.cjs +8 -3
  135. package/lib/types/index.cjs.map +1 -0
  136. package/lib/types/index.d.cts +284 -0
  137. package/lib/types/index.d.ts +104 -24
  138. package/package.json +16 -13
  139. package/es/constants/dom.d.ts.map +0 -1
  140. package/es/core/chat-manager.d.ts.map +0 -1
  141. package/es/core/event-emitter.d.ts.map +0 -1
  142. package/es/core/mention-engine.d.ts.map +0 -1
  143. package/es/index.d.ts.map +0 -1
  144. package/es/renderer/dom-renderer.d.ts.map +0 -1
  145. package/es/renderer/style-manager.d.ts.map +0 -1
  146. package/es/theme/theme-manager.d.ts.map +0 -1
  147. package/es/theme/tokens.d.ts.map +0 -1
  148. package/es/types/index.d.ts.map +0 -1
  149. package/esm/constants/dom.d.ts.map +0 -1
  150. package/esm/core/chat-manager.d.ts.map +0 -1
  151. package/esm/core/event-emitter.d.ts.map +0 -1
  152. package/esm/core/mention-engine.d.ts.map +0 -1
  153. package/esm/index.d.ts.map +0 -1
  154. package/esm/renderer/dom-renderer.d.ts.map +0 -1
  155. package/esm/renderer/style-manager.d.ts.map +0 -1
  156. package/esm/theme/theme-manager.d.ts.map +0 -1
  157. package/esm/theme/tokens.d.ts.map +0 -1
  158. package/esm/types/index.d.ts.map +0 -1
  159. package/lib/constants/dom.d.ts.map +0 -1
  160. package/lib/core/chat-manager.d.ts.map +0 -1
  161. package/lib/core/event-emitter.d.ts.map +0 -1
  162. package/lib/core/mention-engine.d.ts.map +0 -1
  163. package/lib/index.d.ts.map +0 -1
  164. package/lib/renderer/dom-renderer.d.ts.map +0 -1
  165. package/lib/renderer/style-manager.d.ts.map +0 -1
  166. package/lib/theme/theme-manager.d.ts.map +0 -1
  167. package/lib/theme/tokens.d.ts.map +0 -1
  168. package/lib/types/index.d.ts.map +0 -1
@@ -1,14 +1,15 @@
1
1
  /*!
2
2
  * ***********************************
3
- * @composy/chat-core v0.0.1 *
3
+ * @composy/chat-core v0.0.2 *
4
4
  * Built with rollup *
5
- * Build time: 2024-08-17 11:30:32 *
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.messageListEl.querySelector(`[data-msg-id="${this.escapeAttr(id)}"]`)?.querySelector(`.${this.prefix}-bubble`);
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.messageListEl.querySelector(`[data-msg-id="${this.escapeAttr(id)}"]`);
154
+ const rowEl = this.getMessageRow(id);
108
155
  if (!rowEl) return;
109
- rowEl.replaceWith(this.createMessageRow(message));
156
+ rowEl.replaceWith(this.createMessageRow({
157
+ ...message,
158
+ id
159
+ }));
110
160
  }
111
161
  removeMessage(id) {
112
- this.messageListEl.querySelector(`[data-msg-id="${this.escapeAttr(id)}"]`)?.remove();
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.select(index);
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.container.innerHTML = "";
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
- input.remove();
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
- document.body.appendChild(input);
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
- if (typeof window === "undefined") return;
266
- const SpeechRecognition = window.SpeechRecognition || window.webkitSpeechRecognition;
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
- this.recognition = new SpeechRecognition();
275
- this.recognition.continuous = false;
276
- this.recognition.interimResults = true;
277
- this.recognition.lang = this.config.voiceLang ?? "zh-CN";
278
- this.recognition.onresult = (event) => {
279
- let transcript = "";
280
- for (let i = 0; i < event.results.length; i++) transcript += event.results[i][0].transcript;
281
- this.lastVoiceTranscript = transcript;
282
- if (event.results[event.results.length - 1]?.isFinal && this.textareaEl) this.insertTextAtCursor(transcript);
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
- this.recognition.onend = () => {
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", this.lastVoiceTranscript);
287
- this.config.onVoiceEnd?.(this.lastVoiceTranscript);
370
+ this.emitter.emit("toolbar-voice-end", transcript);
371
+ this.config.onVoiceEnd?.(transcript);
288
372
  };
289
- this.recognition.onerror = (error) => {
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
- this.recognition.start();
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
- if (this.mentionEngine.getState().active) {
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 pos = this.textareaEl.selectionStart;
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(pos);
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
- const maxHeight = this.getCssSizeValue("--ldesign-chat-textarea-max-height", 120);
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
- Promise.resolve(this.config.onLoadMore?.()).finally(() => {
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.innerHTML = message.customHtml;
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
- bubble.innerHTML = this.renderTextContent(message.content ?? "", message.mentions);
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
  }