@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,13 +1,14 @@
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
  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.messageListEl.querySelector(`[data-msg-id="${this.escapeAttr(id)}"]`)?.querySelector(`.${this.prefix}-bubble`);
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.messageListEl.querySelector(`[data-msg-id="${this.escapeAttr(id)}"]`);
153
+ const rowEl = this.getMessageRow(id);
107
154
  if (!rowEl) return;
108
- rowEl.replaceWith(this.createMessageRow(message));
155
+ rowEl.replaceWith(this.createMessageRow({
156
+ ...message,
157
+ id
158
+ }));
109
159
  }
110
160
  removeMessage(id) {
111
- this.messageListEl.querySelector(`[data-msg-id="${this.escapeAttr(id)}"]`)?.remove();
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.select(index);
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.container.innerHTML = "";
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
- input.remove();
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
- document.body.appendChild(input);
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
- if (typeof window === "undefined") return;
265
- const SpeechRecognition = window.SpeechRecognition || window.webkitSpeechRecognition;
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
- this.recognition = new SpeechRecognition();
274
- this.recognition.continuous = false;
275
- this.recognition.interimResults = true;
276
- this.recognition.lang = this.config.voiceLang ?? "zh-CN";
277
- this.recognition.onresult = (event) => {
278
- let transcript = "";
279
- for (let i = 0; i < event.results.length; i++) transcript += event.results[i][0].transcript;
280
- this.lastVoiceTranscript = transcript;
281
- if (event.results[event.results.length - 1]?.isFinal && this.textareaEl) this.insertTextAtCursor(transcript);
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
- this.recognition.onend = () => {
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", this.lastVoiceTranscript);
286
- this.config.onVoiceEnd?.(this.lastVoiceTranscript);
369
+ this.emitter.emit("toolbar-voice-end", transcript);
370
+ this.config.onVoiceEnd?.(transcript);
287
371
  };
288
- this.recognition.onerror = (error) => {
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
- this.recognition.start();
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
- if (this.mentionEngine.getState().active) {
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 pos = this.textareaEl.selectionStart;
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(pos);
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
- const maxHeight = this.getCssSizeValue("--ldesign-chat-textarea-max-height", 120);
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
- Promise.resolve(this.config.onLoadMore?.()).finally(() => {
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.innerHTML = message.customHtml;
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
- bubble.innerHTML = this.renderTextContent(message.content ?? "", message.mentions);
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
  }