@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
package/dist/index.js CHANGED
@@ -1,8 +1,8 @@
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
  * ***********************************
@@ -11,6 +11,415 @@
11
11
  typeof exports === "object" && typeof module !== "undefined" ? factory(exports) : typeof define === "function" && define.amd ? define(["exports"], factory) : (global = typeof globalThis !== "undefined" ? globalThis : global || self, factory(global.LDesignChat = {}));
12
12
  })(this, function(exports) {
13
13
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
14
+ //#region src/constants/dom.ts
15
+ const DEFAULT_CLASS_PREFIX = "ldesign-chat";
16
+ const COMPONENT_CSS_VAR_PREFIX = "--ldesign-chat";
17
+ const STYLE_ELEMENT_ID = "ldesign-chat-styles";
18
+ function normalizeClassPrefix(prefix) {
19
+ const trimmed = prefix?.trim();
20
+ if (!trimmed) return DEFAULT_CLASS_PREFIX;
21
+ const safeSuffix = trimmed.replace(/^ldesign-/i, "").replace(/[^\w-]+/g, "-").replace(/-+/g, "-").replace(/^-|-$/g, "");
22
+ return safeSuffix ? `ldesign-${safeSuffix}` : DEFAULT_CLASS_PREFIX;
23
+ }
24
+ function getStyleElementId(prefix = DEFAULT_CLASS_PREFIX) {
25
+ const normalized = normalizeClassPrefix(prefix);
26
+ return normalized === "ldesign-chat" ? STYLE_ELEMENT_ID : `${normalized}-styles`;
27
+ }
28
+ function cx(...classes) {
29
+ return classes.filter(Boolean).join(" ");
30
+ }
31
+ //#endregion
32
+ //#region src/types/index.ts
33
+ const CHAT_MESSAGE_SCHEMA_VERSION = 1;
34
+ //#endregion
35
+ //#region src/core/message-schema.ts
36
+ function normalizeChatMessage(message) {
37
+ if (!message || typeof message !== "object") throw new TypeError("Chat message must be an object.");
38
+ if (typeof message.id !== "string" || !message.id.trim()) throw new TypeError("Chat message ID must be a non-empty string.");
39
+ if (!Number.isFinite(message.timestamp)) throw new TypeError("Chat message timestamp must be finite.");
40
+ const schemaVersion = message.schemaVersion ?? 1;
41
+ if (schemaVersion > 1) throw new Error(`Unsupported chat message schema version: ${schemaVersion}.`);
42
+ return cloneChatMessage({
43
+ ...message,
44
+ schemaVersion: 1,
45
+ ...message.contentBlocks && { contentBlocks: normalizeChatContentBlocks(message.contentBlocks) }
46
+ });
47
+ }
48
+ function normalizeChatContentBlocks(blocks) {
49
+ const normalized = [];
50
+ for (const value of blocks) {
51
+ const block = normalizeContentBlock(value);
52
+ if (block) normalized.push(block);
53
+ }
54
+ return normalized;
55
+ }
56
+ function normalizeChatMessages(messages) {
57
+ const result = [];
58
+ const ids = /* @__PURE__ */ new Set();
59
+ const clientIds = /* @__PURE__ */ new Set();
60
+ for (const message of messages) {
61
+ const normalized = normalizeChatMessage(message);
62
+ if (ids.has(normalized.id)) throw new Error(`Duplicate chat message ID: ${normalized.id}`);
63
+ if (normalized.clientMessageId && clientIds.has(normalized.clientMessageId)) throw new Error(`Duplicate chat client message ID: ${normalized.clientMessageId}`);
64
+ ids.add(normalized.id);
65
+ if (normalized.clientMessageId) clientIds.add(normalized.clientMessageId);
66
+ result.push(normalized);
67
+ }
68
+ return result;
69
+ }
70
+ function cloneChatMessage(message) {
71
+ return {
72
+ ...message,
73
+ sender: cloneUser(message.sender),
74
+ ...message.image && { image: cloneImage(message.image) },
75
+ ...message.file && { file: cloneFile(message.file) },
76
+ ...message.mentions && { mentions: message.mentions.map(cloneMention) },
77
+ ...message.contentBlocks && { contentBlocks: message.contentBlocks.map(cloneContentBlock) },
78
+ ...message.customData && { customData: cloneRecord(message.customData) }
79
+ };
80
+ }
81
+ function compareChatMessages(left, right) {
82
+ if (Number.isFinite(left.sequence) && Number.isFinite(right.sequence) && left.sequence !== right.sequence) return left.sequence - right.sequence;
83
+ if (left.timestamp !== right.timestamp) return left.timestamp - right.timestamp;
84
+ return left.id.localeCompare(right.id);
85
+ }
86
+ function cloneContentBlock(block) {
87
+ switch (block.type) {
88
+ case "text": return {
89
+ type: "text",
90
+ text: block.text
91
+ };
92
+ case "mention": return {
93
+ type: "mention",
94
+ id: block.id,
95
+ label: block.label
96
+ };
97
+ case "link": return {
98
+ type: "link",
99
+ url: block.url,
100
+ ...block.label && { label: block.label }
101
+ };
102
+ case "code": return {
103
+ type: "code",
104
+ code: block.code,
105
+ ...block.language && { language: block.language }
106
+ };
107
+ case "image": return {
108
+ type: "image",
109
+ src: block.src,
110
+ ...block.alt && { alt: block.alt },
111
+ ...block.width !== void 0 && { width: block.width },
112
+ ...block.height !== void 0 && { height: block.height }
113
+ };
114
+ case "file": return {
115
+ type: "file",
116
+ url: block.url,
117
+ name: block.name,
118
+ ...block.size !== void 0 && { size: block.size },
119
+ ...block.mimeType && { mimeType: block.mimeType }
120
+ };
121
+ case "break": return { type: "break" };
122
+ case "custom": return {
123
+ type: "custom",
124
+ kind: block.kind,
125
+ data: cloneRecord(block.data),
126
+ ...block.fallback && { fallback: block.fallback }
127
+ };
128
+ }
129
+ }
130
+ function normalizeContentBlock(value) {
131
+ if (!isRecord(value) || typeof value.type !== "string") return null;
132
+ switch (value.type) {
133
+ case "text": return typeof value.text === "string" ? {
134
+ type: "text",
135
+ text: value.text
136
+ } : null;
137
+ case "mention": return typeof value.id === "string" && typeof value.label === "string" ? {
138
+ type: "mention",
139
+ id: value.id,
140
+ label: value.label
141
+ } : null;
142
+ case "link": return typeof value.url === "string" ? {
143
+ type: "link",
144
+ url: value.url,
145
+ ...typeof value.label === "string" && { label: value.label }
146
+ } : null;
147
+ case "code": return typeof value.code === "string" ? {
148
+ type: "code",
149
+ code: value.code,
150
+ ...typeof value.language === "string" && { language: value.language }
151
+ } : null;
152
+ case "image": return typeof value.src === "string" ? {
153
+ type: "image",
154
+ src: value.src,
155
+ ...typeof value.alt === "string" && { alt: value.alt },
156
+ ...isPositiveNumber(value.width) && { width: value.width },
157
+ ...isPositiveNumber(value.height) && { height: value.height }
158
+ } : null;
159
+ case "file": return typeof value.url === "string" && typeof value.name === "string" ? {
160
+ type: "file",
161
+ url: value.url,
162
+ name: value.name,
163
+ ...isPositiveNumber(value.size) && { size: value.size },
164
+ ...typeof value.mimeType === "string" && { mimeType: value.mimeType }
165
+ } : null;
166
+ case "break": return { type: "break" };
167
+ case "custom": return typeof value.kind === "string" && isRecord(value.data) ? {
168
+ type: "custom",
169
+ kind: value.kind,
170
+ data: cloneRecord(value.data),
171
+ ...typeof value.fallback === "string" && { fallback: value.fallback }
172
+ } : fallbackBlock(value);
173
+ default: return fallbackBlock(value);
174
+ }
175
+ }
176
+ function fallbackBlock(value) {
177
+ return typeof value.fallback === "string" ? {
178
+ type: "text",
179
+ text: value.fallback
180
+ } : null;
181
+ }
182
+ function isRecord(value) {
183
+ return Boolean(value) && typeof value === "object" && !Array.isArray(value);
184
+ }
185
+ function isPositiveNumber(value) {
186
+ return typeof value === "number" && Number.isFinite(value) && value > 0;
187
+ }
188
+ function cloneUser(user) {
189
+ return { ...user };
190
+ }
191
+ function cloneMention(mention) {
192
+ return { ...mention };
193
+ }
194
+ function cloneImage(image) {
195
+ return { ...image };
196
+ }
197
+ function cloneFile(file) {
198
+ return { ...file };
199
+ }
200
+ function cloneRecord(value, seen = /* @__PURE__ */ new WeakMap()) {
201
+ if (!value || typeof value !== "object") return value;
202
+ if (seen.has(value)) return seen.get(value);
203
+ if (Array.isArray(value)) {
204
+ const clone2 = [];
205
+ seen.set(value, clone2);
206
+ value.forEach((item) => clone2.push(cloneRecord(item, seen)));
207
+ return clone2;
208
+ }
209
+ const prototype = Object.getPrototypeOf(value);
210
+ if (prototype !== Object.prototype && prototype !== null) return value;
211
+ const clone = {};
212
+ seen.set(value, clone);
213
+ Object.entries(value).forEach(([key, item]) => {
214
+ clone[key] = cloneRecord(item, seen);
215
+ });
216
+ return clone;
217
+ }
218
+ //#endregion
219
+ //#region src/core/chat-manager.ts
220
+ var ChatManager = class {
221
+ constructor(emitter, initialMessages) {
222
+ this.messages = [];
223
+ this.streams = /* @__PURE__ */ new Map();
224
+ this.emitter = emitter;
225
+ if (initialMessages?.length) this.messages = normalizeChatMessages(initialMessages);
226
+ }
227
+ getMessages() {
228
+ return this.messages.map(cloneChatMessage);
229
+ }
230
+ getMessage(id) {
231
+ const message = this.messages.find((item) => item.id === id);
232
+ return message ? cloneChatMessage(message) : void 0;
233
+ }
234
+ getMessageIdByClientMessageId(clientMessageId) {
235
+ return this.messages.find((message) => message.clientMessageId === clientMessageId)?.id;
236
+ }
237
+ addMessage(message) {
238
+ const storedMessage = normalizeChatMessage(message);
239
+ this.assertUniqueIds([storedMessage]);
240
+ this.messages.push(storedMessage);
241
+ this.emitAdded(storedMessage);
242
+ }
243
+ prependMessages(messages) {
244
+ const storedMessages = normalizeChatMessages(messages);
245
+ this.assertUniqueIds(storedMessages);
246
+ this.messages.unshift(...storedMessages);
247
+ this.emitter.emit("messages-prepended", storedMessages.map(cloneChatMessage));
248
+ this.emitChanged();
249
+ }
250
+ /** 合并本地 ack 或远端推送,按 id/clientMessageId 去重并拒绝旧 sequence。 */
251
+ upsertMessage(message, options = {}) {
252
+ const normalized = normalizeChatMessage({
253
+ ...message,
254
+ ...options.sequence !== void 0 && { sequence: options.sequence }
255
+ });
256
+ const index = this.findMessageIndex(normalized);
257
+ if (index < 0) {
258
+ this.messages.push(normalized);
259
+ this.emitAdded(normalized);
260
+ return "added";
261
+ }
262
+ const current = this.messages[index];
263
+ if (Number.isFinite(normalized.sequence) && Number.isFinite(current.sequence) && normalized.sequence <= current.sequence) return "ignored";
264
+ const merged = normalizeChatMessage({
265
+ ...current,
266
+ ...normalized,
267
+ id: current.id,
268
+ clientMessageId: normalized.clientMessageId ?? current.clientMessageId,
269
+ sequence: normalized.sequence ?? current.sequence
270
+ });
271
+ this.messages[index] = merged;
272
+ this.emitter.emit("message-updated", cloneChatMessage(merged));
273
+ this.emitChanged();
274
+ return "updated";
275
+ }
276
+ mergeMessages(messages, options = {}) {
277
+ const result = {
278
+ added: 0,
279
+ updated: 0,
280
+ ignored: 0
281
+ };
282
+ for (const message of messages) {
283
+ const outcome = this.upsertMessage(message, options);
284
+ result[outcome] += 1;
285
+ }
286
+ return result;
287
+ }
288
+ updateMessage(id, updates) {
289
+ const index = this.messages.findIndex((message) => message.id === id);
290
+ if (index === -1) return;
291
+ if (updates.id !== void 0 && updates.id !== id) throw new Error("Chat message IDs are immutable.");
292
+ const next = normalizeChatMessage({
293
+ ...this.messages[index],
294
+ ...updates,
295
+ id
296
+ });
297
+ this.assertUniqueClientMessageId(next, id);
298
+ this.messages[index] = next;
299
+ this.emitter.emit("message-updated", cloneChatMessage(next));
300
+ this.emitChanged();
301
+ }
302
+ markMessageStatus(id, status) {
303
+ this.updateMessage(id, { status });
304
+ }
305
+ removeMessage(id) {
306
+ const index = this.messages.findIndex((message) => message.id === id);
307
+ if (index === -1) return;
308
+ const [removed] = this.messages.splice(index, 1);
309
+ this.streams.delete(id);
310
+ this.emitter.emit("message-removed", cloneChatMessage(removed));
311
+ this.emitChanged();
312
+ }
313
+ setMessages(messages) {
314
+ this.messages = normalizeChatMessages(messages);
315
+ this.streams.clear();
316
+ this.emitChanged();
317
+ }
318
+ clearMessages() {
319
+ this.messages = [];
320
+ this.streams.clear();
321
+ this.emitChanged();
322
+ }
323
+ beginStreaming(id, generation = createGeneration()) {
324
+ const index = this.messages.findIndex((message) => message.id === id);
325
+ if (index === -1) return null;
326
+ this.streams.set(id, {
327
+ generation,
328
+ lastSequence: -1
329
+ });
330
+ this.messages[index] = normalizeChatMessage({
331
+ ...this.messages[index],
332
+ streaming: true
333
+ });
334
+ this.emitter.emit("message-updated", cloneChatMessage(this.messages[index]));
335
+ this.emitChanged();
336
+ return generation;
337
+ }
338
+ appendMessageContent(id, chunk, options = {}) {
339
+ const stream = this.streams.get(id);
340
+ if (options.generation) {
341
+ if (!stream || stream.generation !== options.generation) return false;
342
+ const sequence = options.sequence;
343
+ if (sequence !== void 0 && Number.isFinite(sequence) && sequence <= stream.lastSequence) return false;
344
+ if (sequence !== void 0 && Number.isFinite(sequence)) stream.lastSequence = sequence;
345
+ }
346
+ const index = this.messages.findIndex((message2) => message2.id === id);
347
+ if (index === -1) return false;
348
+ const message = this.messages[index];
349
+ this.messages[index] = normalizeChatMessage({
350
+ ...message,
351
+ content: (message.content ?? "") + chunk,
352
+ streaming: options.generation ? true : message.streaming
353
+ });
354
+ this.emitter.emit("message-content-appended", id, this.messages[index].content, options.generation, options.sequence);
355
+ this.emitChanged();
356
+ return true;
357
+ }
358
+ finishStreaming(id, generation) {
359
+ const stream = this.streams.get(id);
360
+ if (generation && (!stream || stream.generation !== generation)) return false;
361
+ const index = this.messages.findIndex((message) => message.id === id);
362
+ if (index === -1) return false;
363
+ this.messages[index] = normalizeChatMessage({
364
+ ...this.messages[index],
365
+ streaming: false
366
+ });
367
+ this.streams.delete(id);
368
+ this.emitter.emit("message-updated", cloneChatMessage(this.messages[index]));
369
+ this.emitChanged();
370
+ return true;
371
+ }
372
+ cancelStreaming(id, generation) {
373
+ const stream = this.streams.get(id);
374
+ if (generation && (!stream || stream.generation !== generation)) return false;
375
+ const index = this.messages.findIndex((message) => message.id === id);
376
+ if (index === -1) return false;
377
+ this.streams.delete(id);
378
+ this.messages[index] = normalizeChatMessage({
379
+ ...this.messages[index],
380
+ streaming: false
381
+ });
382
+ this.emitter.emit("stream-cancelled", cloneChatMessage(this.messages[index]));
383
+ this.emitter.emit("message-updated", cloneChatMessage(this.messages[index]));
384
+ this.emitChanged();
385
+ return true;
386
+ }
387
+ getStreamGeneration(id) {
388
+ return this.streams.get(id)?.generation;
389
+ }
390
+ findMessageIndex(message) {
391
+ const byId = this.messages.findIndex((item) => item.id === message.id);
392
+ if (byId >= 0) return byId;
393
+ if (!message.clientMessageId) return -1;
394
+ return this.messages.findIndex((item) => item.clientMessageId === message.clientMessageId);
395
+ }
396
+ assertUniqueIds(messages) {
397
+ const ids = new Set(this.messages.map((message) => message.id));
398
+ const clientIds = new Set(this.messages.flatMap((message) => message.clientMessageId ? [message.clientMessageId] : []));
399
+ for (const message of messages) {
400
+ if (!message.id || ids.has(message.id)) throw new Error(`Chat message ID must be non-empty and unique: ${message.id}`);
401
+ if (message.clientMessageId && clientIds.has(message.clientMessageId)) throw new Error(`Chat client message ID must be unique: ${message.clientMessageId}`);
402
+ ids.add(message.id);
403
+ if (message.clientMessageId) clientIds.add(message.clientMessageId);
404
+ }
405
+ }
406
+ assertUniqueClientMessageId(message, ownId) {
407
+ if (message.clientMessageId && this.messages.some((item) => item.id !== ownId && item.clientMessageId === message.clientMessageId)) throw new Error(`Chat client message ID must be unique: ${message.clientMessageId}`);
408
+ }
409
+ emitAdded(message) {
410
+ this.emitter.emit("message-added", cloneChatMessage(message));
411
+ this.emitChanged();
412
+ }
413
+ emitChanged() {
414
+ this.emitter.emit("messages-changed", this.getMessages());
415
+ }
416
+ };
417
+ function createGeneration() {
418
+ const cryptoObject = globalThis.crypto;
419
+ if (cryptoObject?.randomUUID) return cryptoObject.randomUUID();
420
+ return `stream-${Date.now()}-${Math.random().toString(36).slice(2)}`;
421
+ }
422
+ //#endregion
14
423
  //#region src/core/event-emitter.ts
15
424
  var EventEmitter = class {
16
425
  constructor() {
@@ -23,8 +432,12 @@
23
432
  * @returns 返回一个取消订阅的函数,调用即可移除该监听器
24
433
  */
25
434
  on(event, handler) {
26
- if (!this.listeners.has(event)) this.listeners.set(event, /* @__PURE__ */ new Set());
27
- this.listeners.get(event).add(handler);
435
+ let handlers = this.listeners.get(event);
436
+ if (!handlers) {
437
+ handlers = /* @__PURE__ */ new Set();
438
+ this.listeners.set(event, handlers);
439
+ }
440
+ handlers.add(handler);
28
441
  return () => this.off(event, handler);
29
442
  }
30
443
  /**
@@ -33,7 +446,10 @@
33
446
  * @param handler - 要移除的处理函数引用(必须与注册时为同一函数引用)
34
447
  */
35
448
  off(event, handler) {
36
- this.listeners.get(event)?.delete(handler);
449
+ const handlers = this.listeners.get(event);
450
+ if (!handlers) return;
451
+ handlers.delete(handler);
452
+ if (handlers.size === 0) this.listeners.delete(event);
37
453
  }
38
454
  /**
39
455
  * 触发(发射)指定事件,依次调用所有已注册的监听器
@@ -41,7 +457,7 @@
41
457
  * @param args - 传递给监听器函数的参数列表(剩余参数语法)
42
458
  */
43
459
  emit(event, ...args) {
44
- this.listeners.get(event)?.forEach((handler) => {
460
+ [...this.listeners.get(event) ?? []].forEach((handler) => {
45
461
  try {
46
462
  handler(...args);
47
463
  } catch (err) {
@@ -69,92 +485,11 @@
69
485
  * - 若不传参数,则清空所有事件的所有监听器
70
486
  */
71
487
  removeAll(event) {
72
- if (event) this.listeners.delete(event);
488
+ if (event !== void 0) this.listeners.delete(event);
73
489
  else this.listeners.clear();
74
490
  }
75
491
  };
76
492
  //#endregion
77
- //#region src/core/chat-manager.ts
78
- var ChatManager = class {
79
- constructor(emitter, initialMessages) {
80
- this.messages = [];
81
- this.emitter = emitter;
82
- if (initialMessages?.length) this.messages = [...initialMessages];
83
- }
84
- /** 获取所有消息 */
85
- getMessages() {
86
- return [...this.messages];
87
- }
88
- /** 根据 ID 获取消息 */
89
- getMessage(id) {
90
- return this.messages.find((m) => m.id === id);
91
- }
92
- /** 添加消息(末尾追加) */
93
- addMessage(message) {
94
- this.messages.push(message);
95
- this.emitter.emit("message-added", message);
96
- this.emitter.emit("messages-changed", this.getMessages());
97
- }
98
- /** 在顶部批量插入消息(用于加载更多) */
99
- prependMessages(messages) {
100
- this.messages.unshift(...messages);
101
- this.emitter.emit("messages-prepended", messages);
102
- this.emitter.emit("messages-changed", this.getMessages());
103
- }
104
- /** 更新消息 */
105
- updateMessage(id, updates) {
106
- const index = this.messages.findIndex((m) => m.id === id);
107
- if (index === -1) return;
108
- this.messages[index] = {
109
- ...this.messages[index],
110
- ...updates
111
- };
112
- this.emitter.emit("message-updated", this.messages[index]);
113
- this.emitter.emit("messages-changed", this.getMessages());
114
- }
115
- /** 删除消息 */
116
- removeMessage(id) {
117
- const index = this.messages.findIndex((m) => m.id === id);
118
- if (index === -1) return;
119
- const removed = this.messages.splice(index, 1)[0];
120
- this.emitter.emit("message-removed", removed);
121
- this.emitter.emit("messages-changed", this.getMessages());
122
- }
123
- /** 替换全部消息 */
124
- setMessages(messages) {
125
- this.messages = [...messages];
126
- this.emitter.emit("messages-changed", this.getMessages());
127
- }
128
- /** 清空所有消息 */
129
- clearMessages() {
130
- this.messages = [];
131
- this.emitter.emit("messages-changed", []);
132
- }
133
- /** 追加流式消息内容 */
134
- appendMessageContent(id, chunk) {
135
- const index = this.messages.findIndex((m) => m.id === id);
136
- if (index === -1) return;
137
- const msg = this.messages[index];
138
- this.messages[index] = {
139
- ...msg,
140
- content: (msg.content ?? "") + chunk
141
- };
142
- this.emitter.emit("message-content-appended", id, this.messages[index].content);
143
- this.emitter.emit("messages-changed", this.getMessages());
144
- }
145
- /** 完成流式消息 */
146
- finishStreaming(id) {
147
- const index = this.messages.findIndex((m) => m.id === id);
148
- if (index === -1) return;
149
- this.messages[index] = {
150
- ...this.messages[index],
151
- streaming: false
152
- };
153
- this.emitter.emit("message-updated", this.messages[index]);
154
- this.emitter.emit("messages-changed", this.getMessages());
155
- }
156
- };
157
- //#endregion
158
493
  //#region src/core/mention-engine.ts
159
494
  var MentionEngine = class {
160
495
  constructor(emitter, members) {
@@ -168,7 +503,7 @@
168
503
  };
169
504
  this.selectedMentions = [];
170
505
  this.emitter = emitter;
171
- if (members) this.members = [...members];
506
+ if (members) this.members = members.map((member) => ({ ...member }));
172
507
  }
173
508
  getState() {
174
509
  return {
@@ -177,10 +512,10 @@
177
512
  };
178
513
  }
179
514
  getSelectedMentions() {
180
- return [...this.selectedMentions];
515
+ return this.selectedMentions.map((member) => ({ ...member }));
181
516
  }
182
517
  setMembers(members) {
183
- this.members = [...members];
518
+ this.members = members.map((member) => ({ ...member }));
184
519
  if (this.state.active) this.filter(this.state.query);
185
520
  }
186
521
  handleInput(text, cursorPos) {
@@ -195,7 +530,7 @@
195
530
  return;
196
531
  }
197
532
  const query = beforeCursor.slice(atIndex + 1);
198
- if (/[^\S\r\n]/.test(query)) {
533
+ if (/\s/.test(query)) {
199
534
  this.deactivate();
200
535
  return;
201
536
  }
@@ -220,7 +555,7 @@
220
555
  select(index) {
221
556
  if (index < 0 || index >= this.state.filtered.length) return null;
222
557
  const member = this.state.filtered[index];
223
- if (!this.selectedMentions.find((m) => m.id === member.id)) this.selectedMentions.push(member);
558
+ if (!this.selectedMentions.some((m) => m.id === member.id)) this.selectedMentions.push(member);
224
559
  this.emitter.emit("mention-select", member);
225
560
  this.deactivate();
226
561
  return member;
@@ -239,7 +574,7 @@
239
574
  parseMentions(text) {
240
575
  const mentions = [];
241
576
  const sortedMembers = [...this.members].sort((a, b) => b.name.length - a.name.length);
242
- for (const member of sortedMembers) if (new RegExp(`(^|\\s)@${this.escapeRegex(member.name)}(?=\\s|$|[\uFF0C\u3002\uFF01\uFF1F,.!?])`, "u").test(text) && !mentions.find((m) => m.id === member.id)) mentions.push(member);
577
+ for (const member of sortedMembers) if (new RegExp(`(^|\\s)@${this.escapeRegex(member.name)}(?=\\s|$|[\uFF0C\u3002\uFF01\uFF1F,.!?])`, "u").test(text) && !mentions.some((m) => m.id === member.id)) mentions.push({ ...member });
243
578
  return mentions;
244
579
  }
245
580
  resetSelected() {
@@ -251,7 +586,7 @@
251
586
  filter(query) {
252
587
  const normalizedQuery = query.trim().toLowerCase();
253
588
  this.state.query = query;
254
- this.state.filtered = this.members.filter((member) => member.name.toLowerCase().includes(normalizedQuery) || member.id.toLowerCase().includes(normalizedQuery));
589
+ this.state.filtered = this.members.filter((member) => member.name.toLowerCase().includes(normalizedQuery) || member.id.toLowerCase().includes(normalizedQuery)).map((member) => ({ ...member }));
255
590
  this.state.activeIndex = 0;
256
591
  this.emitter.emit("mention-filter", this.getState());
257
592
  }
@@ -260,879 +595,162 @@
260
595
  }
261
596
  };
262
597
  //#endregion
263
- //#region src/theme/tokens.ts
264
- const lightTokens = {
265
- bgPrimary: "var(--color-gray-50)",
266
- bgSecondary: "var(--color-gray-100)",
267
- bgBubbleOutgoing: "var(--color-primary-default)",
268
- bgBubbleIncoming: "var(--color-gray-100)",
269
- bgInput: "var(--color-gray-50)",
270
- bgMention: "var(--color-gray-50)",
271
- bgMentionHover: "var(--color-primary-50)",
272
- bgMentionActive: "var(--color-primary-100)",
273
- bgLoadMore: "var(--color-gray-100)",
274
- bgSystemMessage: "var(--color-gray-100)",
275
- bgDropOverlay: "color-mix(in srgb, var(--color-primary-default) 10%, var(--color-gray-50))",
276
- bgDangerSoft: "color-mix(in srgb, var(--color-danger-default) 12%, var(--color-gray-50))",
277
- textPrimary: "var(--color-gray-900)",
278
- textSecondary: "var(--color-gray-600)",
279
- textBubbleOutgoing: "var(--color-gray-50)",
280
- textBubbleIncoming: "var(--color-gray-900)",
281
- textMention: "var(--color-gray-900)",
282
- textMentionTag: "var(--color-primary-default)",
283
- textTimestamp: "var(--color-gray-500)",
284
- textSystem: "var(--color-gray-600)",
285
- textPlaceholder: "var(--color-gray-400)",
286
- borderPrimary: "var(--color-gray-200)",
287
- borderInput: "var(--color-gray-300)",
288
- borderInputFocus: "var(--color-primary-default)",
289
- borderMention: "var(--color-gray-200)",
290
- colorSending: "var(--color-gray-400)",
291
- colorSent: "var(--color-gray-500)",
292
- colorDelivered: "var(--color-primary-default)",
293
- colorRead: "var(--color-success-default)",
294
- colorFailed: "var(--color-danger-default)",
295
- colorLink: "var(--color-primary-default)",
296
- shadowPanel: "var(--size-shadow-2)",
297
- shadowMention: "var(--size-shadow-4)",
298
- avatarBg: "var(--color-primary-default)",
299
- avatarText: "var(--color-gray-50)",
300
- btnPrimaryBg: "var(--color-primary-default)",
301
- btnPrimaryText: "var(--color-gray-50)",
302
- btnPrimaryHover: "var(--color-primary-hover)",
303
- scrollbarThumb: "var(--color-gray-300)",
304
- scrollbarTrack: "var(--color-gray-100)",
305
- bgToolbar: "var(--color-gray-100)",
306
- borderToolbar: "var(--color-gray-200)",
307
- toolbarIconColor: "var(--color-gray-600)",
308
- toolbarIconHover: "var(--color-primary-default)",
309
- toolbarVoiceActive: "var(--color-danger-default)"
310
- };
311
- const darkTokens = {
312
- bgPrimary: "var(--color-gray-900)",
313
- bgSecondary: "var(--color-gray-800)",
314
- bgBubbleOutgoing: "var(--color-primary-default)",
315
- bgBubbleIncoming: "var(--color-gray-800)",
316
- bgInput: "var(--color-gray-900)",
317
- bgMention: "var(--color-gray-900)",
318
- bgMentionHover: "var(--color-gray-800)",
319
- bgMentionActive: "var(--color-primary-900)",
320
- bgLoadMore: "var(--color-gray-800)",
321
- bgSystemMessage: "var(--color-gray-800)",
322
- bgDropOverlay: "color-mix(in srgb, var(--color-primary-default) 16%, var(--color-gray-900))",
323
- bgDangerSoft: "color-mix(in srgb, var(--color-danger-default) 18%, var(--color-gray-900))",
324
- textPrimary: "var(--color-gray-50)",
325
- textSecondary: "var(--color-gray-300)",
326
- textBubbleOutgoing: "var(--color-gray-50)",
327
- textBubbleIncoming: "var(--color-gray-50)",
328
- textMention: "var(--color-gray-50)",
329
- textMentionTag: "var(--color-primary-300)",
330
- textTimestamp: "var(--color-gray-400)",
331
- textSystem: "var(--color-gray-300)",
332
- textPlaceholder: "var(--color-gray-500)",
333
- borderPrimary: "var(--color-gray-700)",
334
- borderInput: "var(--color-gray-700)",
335
- borderInputFocus: "var(--color-primary-default)",
336
- borderMention: "var(--color-gray-700)",
337
- colorSending: "var(--color-gray-500)",
338
- colorSent: "var(--color-gray-400)",
339
- colorDelivered: "var(--color-primary-300)",
340
- colorRead: "var(--color-success-default)",
341
- colorFailed: "var(--color-danger-default)",
342
- colorLink: "var(--color-primary-300)",
343
- shadowPanel: "var(--size-shadow-3)",
344
- shadowMention: "var(--size-shadow-6)",
345
- avatarBg: "var(--color-primary-default)",
346
- avatarText: "var(--color-gray-50)",
347
- btnPrimaryBg: "var(--color-primary-default)",
348
- btnPrimaryText: "var(--color-gray-50)",
349
- btnPrimaryHover: "var(--color-primary-hover)",
350
- scrollbarThumb: "var(--color-gray-700)",
351
- scrollbarTrack: "var(--color-gray-800)",
352
- bgToolbar: "var(--color-gray-800)",
353
- borderToolbar: "var(--color-gray-700)",
354
- toolbarIconColor: "var(--color-gray-300)",
355
- toolbarIconHover: "var(--color-primary-300)",
356
- toolbarVoiceActive: "var(--color-danger-default)"
357
- };
358
- //#endregion
359
- //#region src/theme/theme-manager.ts
360
- var ThemeManager = class {
361
- constructor(emitter, mode) {
362
- this.mode = "light";
363
- this.mediaQuery = null;
364
- this.mediaHandler = null;
365
- this.emitter = emitter;
366
- if (mode) this.setTheme(mode);
367
- }
368
- /** 获取当前模式 */
369
- getMode() {
370
- return this.mode;
371
- }
372
- /** 获取当前实际主题(auto 时解析为 light/dark) */
373
- getResolvedTheme() {
374
- if (this.mode === "auto") return this.getSystemTheme();
375
- return this.mode;
376
- }
377
- /** 获取当前主题令牌 */
378
- getTokens() {
379
- return this.getResolvedTheme() === "dark" ? darkTokens : lightTokens;
380
- }
381
- /** 设置主题 */
382
- setTheme(mode) {
383
- this.cleanupMediaListener();
384
- this.mode = mode;
385
- if (mode === "auto") this.setupMediaListener();
386
- this.emitter.emit("theme-change", this.getResolvedTheme(), this.getTokens());
387
- }
388
- /** 获取系统主题偏好 */
389
- getSystemTheme() {
390
- if (typeof window === "undefined") return "light";
391
- return window.matchMedia("(prefers-color-scheme: dark)").matches ? "dark" : "light";
392
- }
393
- /** 监听系统主题变化 */
394
- setupMediaListener() {
395
- if (typeof window === "undefined") return;
396
- this.mediaQuery = window.matchMedia("(prefers-color-scheme: dark)");
397
- this.mediaHandler = () => {
398
- this.emitter.emit("theme-change", this.getResolvedTheme(), this.getTokens());
399
- };
400
- this.mediaQuery.addEventListener("change", this.mediaHandler);
598
+ //#region src/core/transport.ts
599
+ var ChatTransportController = class {
600
+ constructor(transport, options = {}) {
601
+ this.status = "idle";
602
+ this.unsubscribeEvents = null;
603
+ this.unsubscribeStatus = null;
604
+ this.destroyed = false;
605
+ this.connectPromise = null;
606
+ this.lifecycle = 0;
607
+ this.transport = transport;
608
+ this.options = options;
609
+ this.cursor = options.cursor;
610
+ }
611
+ getStatus() {
612
+ return this.status;
613
+ }
614
+ getCursor() {
615
+ return this.cursor;
616
+ }
617
+ async connect(signal) {
618
+ if (this.destroyed) throw new Error("Chat transport controller is disposed.");
619
+ if (this.status === "connected") return;
620
+ if (this.connectPromise) return this.connectPromise;
621
+ const lifecycle = ++this.lifecycle;
622
+ this.setStatus("connecting");
623
+ this.connectPromise = (async () => {
624
+ this.unsubscribeEvents?.();
625
+ this.unsubscribeStatus?.();
626
+ this.unsubscribeEvents = this.transport.subscribe((event) => this.handleEvent(event));
627
+ this.unsubscribeStatus = this.transport.onStatusChange?.((status) => this.setStatus(status)) ?? null;
628
+ try {
629
+ await this.transport.connect({
630
+ conversationId: this.options.conversationId,
631
+ cursor: this.cursor,
632
+ signal
633
+ });
634
+ if (this.destroyed || lifecycle !== this.lifecycle) return;
635
+ this.setStatus("connected");
636
+ } catch (error) {
637
+ if (!this.destroyed && lifecycle === this.lifecycle) this.setStatus("error");
638
+ throw error;
639
+ } finally {
640
+ this.connectPromise = null;
641
+ }
642
+ })();
643
+ return this.connectPromise;
401
644
  }
402
- /** 清理监听器 */
403
- cleanupMediaListener() {
404
- if (this.mediaQuery && this.mediaHandler) {
405
- this.mediaQuery.removeEventListener("change", this.mediaHandler);
406
- this.mediaQuery = null;
407
- this.mediaHandler = null;
645
+ async disconnect() {
646
+ if (this.destroyed) return;
647
+ this.lifecycle += 1;
648
+ this.connectPromise = null;
649
+ try {
650
+ await this.transport.disconnect();
651
+ } finally {
652
+ this.setStatus("disconnected");
408
653
  }
409
654
  }
410
- /** 销毁 */
411
- destroy() {
412
- this.cleanupMediaListener();
655
+ async send(message, options = {}) {
656
+ if (this.destroyed) throw new Error("Chat transport controller is disposed.");
657
+ if (this.status !== "connected") throw new Error(`Chat transport is not connected: ${this.status}`);
658
+ const result = await this.transport.send(message, options);
659
+ if (result && result.cursor) this.cursor = result.cursor;
660
+ return result;
661
+ }
662
+ dispose() {
663
+ if (this.destroyed) return;
664
+ this.destroyed = true;
665
+ this.lifecycle += 1;
666
+ this.unsubscribeEvents?.();
667
+ this.unsubscribeStatus?.();
668
+ this.unsubscribeEvents = null;
669
+ this.unsubscribeStatus = null;
670
+ this.connectPromise = null;
671
+ Promise.resolve(this.transport.disconnect()).catch(() => void 0);
672
+ this.setStatus("disconnected");
673
+ }
674
+ handleEvent(event) {
675
+ if (this.destroyed) return;
676
+ if ("cursor" in event && event.cursor) this.cursor = event.cursor;
677
+ if (event.type === "ack" && event.ack.cursor) this.cursor = event.ack.cursor;
678
+ if (event.type === "error" && !event.recoverable) this.setStatus("error");
679
+ this.options.onEvent?.(event);
680
+ }
681
+ setStatus(status) {
682
+ if (this.status === status) return;
683
+ this.status = status;
684
+ this.options.onStatusChange?.(status);
413
685
  }
414
686
  };
415
687
  //#endregion
416
- //#region src/constants/dom.ts
417
- const DEFAULT_CLASS_PREFIX = "ldesign-chat";
418
- const COMPONENT_CSS_VAR_PREFIX = "--ldesign-chat";
419
- const STYLE_ELEMENT_ID = "ldesign-chat-styles";
420
- function normalizeClassPrefix(prefix) {
421
- const trimmed = prefix?.trim();
422
- if (!trimmed) return DEFAULT_CLASS_PREFIX;
423
- if (trimmed.startsWith("ldesign-")) return trimmed;
424
- return `ldesign-${trimmed.replace(/^-+/, "")}`;
425
- }
426
- function getStyleElementId(prefix = DEFAULT_CLASS_PREFIX) {
427
- const normalized = normalizeClassPrefix(prefix);
428
- return normalized === "ldesign-chat" ? STYLE_ELEMENT_ID : `${normalized}-styles`;
688
+ //#region src/renderer/style-manager.ts
689
+ const styleRefs = /* @__PURE__ */ new WeakMap();
690
+ function injectStyles(tokens, prefix = DEFAULT_CLASS_PREFIX, targetRoot) {
691
+ const normalizedPrefix = normalizeClassPrefix(prefix);
692
+ const styleId = getStyleElementId(normalizedPrefix);
693
+ const ownerDocument = resolveDocument(targetRoot?.ownerDocument);
694
+ const refs = getDocumentRefs(ownerDocument);
695
+ refs.set(styleId, (refs.get(styleId) ?? 0) + 1);
696
+ if (!ownerDocument.getElementById(styleId)) upsertStyle(ownerDocument, styleId, generateCSS(tokens, normalizedPrefix));
697
+ if (targetRoot) applyThemeTokens(targetRoot, tokens);
429
698
  }
430
- function cx(...classes) {
431
- return classes.filter(Boolean).join(" ");
699
+ function updateStyles(tokens, prefix = DEFAULT_CLASS_PREFIX, targetRoot) {
700
+ const normalizedPrefix = normalizeClassPrefix(prefix);
701
+ const styleId = getStyleElementId(normalizedPrefix);
702
+ if (targetRoot) applyThemeTokens(targetRoot, tokens);
703
+ else upsertStyle(resolveDocument(), styleId, generateCSS(tokens, normalizedPrefix));
432
704
  }
433
- //#endregion
434
- //#region src/renderer/dom-renderer.ts
435
- 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>";
436
- 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>";
437
- const ICON_PAPERCLIP = "<svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"m21.44 11.05-9.19 9.19a6 6 0 0 1-8.49-8.49l8.57-8.57A4 4 0 1 1 18 8.84l-8.59 8.57a2 2 0 0 1-2.83-2.83l8.49-8.48\"/></svg>";
438
- 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>";
439
- 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>";
440
- 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>";
441
- const STATUS_MAP = {
442
- sending: "...",
443
- sent: "✓",
444
- delivered: "✓✓",
445
- read: "✓✓",
446
- failed: "!"
447
- };
448
- var DOMRenderer = class {
449
- constructor(container, config, emitter, mentionEngine) {
450
- this.inputArea = null;
451
- this.textareaEl = null;
452
- this.sendBtn = null;
453
- this.mentionPanel = null;
454
- this.loadMoreEl = null;
455
- this.toolbarEl = null;
456
- this.dropOverlay = null;
457
- this.voiceBtn = null;
458
- this.loadingMore = false;
459
- this.isRecording = false;
460
- this.recognition = null;
461
- this.lastVoiceTranscript = "";
462
- this.container = container;
463
- this.config = config;
464
- this.emitter = emitter;
465
- this.mentionEngine = mentionEngine;
466
- this.prefix = normalizeClassPrefix(config.classPrefix ?? "ldesign-chat");
705
+ function removeStyles(prefix = DEFAULT_CLASS_PREFIX, ownerDocument) {
706
+ const doc = resolveDocument(ownerDocument);
707
+ const refsByStyle = getDocumentRefs(doc);
708
+ const styleId = getStyleElementId(prefix);
709
+ const refs = refsByStyle.get(styleId) ?? 0;
710
+ if (refs > 1) {
711
+ refsByStyle.set(styleId, refs - 1);
712
+ return;
467
713
  }
468
- init() {
469
- this.container.innerHTML = "";
470
- this.root = this.el("div", `${this.prefix}-container`);
471
- this.messageListEl = this.el("div", `${this.prefix}-message-list`);
472
- this.root.appendChild(this.messageListEl);
473
- if (this.config.showInput !== false) this.renderInputArea();
474
- this.container.appendChild(this.root);
475
- this.setupEventListeners();
476
- }
477
- updateMessageContent(id, content) {
478
- const bubble = this.messageListEl.querySelector(`[data-msg-id="${this.escapeAttr(id)}"]`)?.querySelector(`.${this.prefix}-bubble`);
479
- if (!bubble) return;
480
- bubble.innerHTML = this.renderTextContent(content);
481
- }
482
- renderMessages(messages) {
483
- this.messageListEl.innerHTML = "";
484
- if (this.config.enableLoadMore) this.renderLoadMoreButton();
485
- if (messages.length === 0) {
486
- this.renderEmpty();
487
- return;
488
- }
489
- let lastTimestamp = 0;
490
- const interval = this.config.timeGroupInterval ?? 3e5;
491
- for (const message of messages) {
492
- if (this.config.showTimestamp !== false && message.timestamp - lastTimestamp > interval) {
493
- this.renderTimeDivider(message.timestamp);
494
- lastTimestamp = message.timestamp;
495
- }
496
- this.renderMessageRow(message);
497
- }
498
- }
499
- appendMessage(message) {
500
- this.removeEmpty();
501
- const messageEls = this.getMessageElements();
502
- const lastTime = messageEls.length > 0 ? Number.parseInt(messageEls[messageEls.length - 1].dataset.timestamp ?? "0", 10) : 0;
503
- const interval = this.config.timeGroupInterval ?? 3e5;
504
- if (this.config.showTimestamp !== false && message.timestamp - lastTime > interval) this.renderTimeDivider(message.timestamp);
505
- this.renderMessageRow(message);
506
- }
507
- prependMessages(messages) {
508
- if (!messages.length) return;
509
- this.removeEmpty();
510
- const scrollHeight = this.messageListEl.scrollHeight;
511
- const scrollTop = this.messageListEl.scrollTop;
512
- const fragment = document.createDocumentFragment();
513
- let lastTimestamp = 0;
514
- const interval = this.config.timeGroupInterval ?? 3e5;
515
- for (const message of messages) {
516
- if (this.config.showTimestamp !== false && message.timestamp - lastTimestamp > interval) {
517
- fragment.appendChild(this.createTimeDivider(message.timestamp));
518
- lastTimestamp = message.timestamp;
519
- }
520
- fragment.appendChild(this.createMessageRow(message));
521
- }
522
- const firstMessage = this.messageListEl.querySelector(`.${this.prefix}-message-row, .${this.prefix}-time-divider`);
523
- if (firstMessage) firstMessage.before(fragment);
524
- else if (this.loadMoreEl) this.loadMoreEl.after(fragment);
525
- else this.messageListEl.appendChild(fragment);
526
- this.messageListEl.scrollTop = scrollTop + (this.messageListEl.scrollHeight - scrollHeight);
527
- }
528
- updateMessage(id, message) {
529
- const rowEl = this.messageListEl.querySelector(`[data-msg-id="${this.escapeAttr(id)}"]`);
530
- if (!rowEl) return;
531
- rowEl.replaceWith(this.createMessageRow(message));
532
- }
533
- removeMessage(id) {
534
- this.messageListEl.querySelector(`[data-msg-id="${this.escapeAttr(id)}"]`)?.remove();
535
- if (this.getMessageElements().length === 0) this.renderEmpty();
536
- }
537
- scrollToBottom(smooth = true) {
538
- if (typeof this.messageListEl.scrollTo === "function") {
539
- this.messageListEl.scrollTo({
540
- top: this.messageListEl.scrollHeight,
541
- behavior: smooth ? "smooth" : "auto"
542
- });
543
- return;
544
- }
545
- this.messageListEl.scrollTop = this.messageListEl.scrollHeight;
546
- }
547
- isAtBottom(threshold = 4) {
548
- return this.messageListEl.scrollHeight - this.messageListEl.scrollTop - this.messageListEl.clientHeight <= threshold;
549
- }
550
- setLoadingMore(loading) {
551
- this.loadingMore = loading;
552
- if (!this.loadMoreEl) return;
553
- this.loadMoreEl.textContent = loading ? "加载中..." : this.config.loadMoreText ?? "加载更多";
554
- this.loadMoreEl.classList.toggle(`${this.prefix}-load-more--loading`, loading);
555
- }
556
- focusInput() {
557
- this.textareaEl?.focus();
558
- }
559
- setInputContent(content) {
560
- if (!this.textareaEl) return;
561
- this.textareaEl.value = this.limitInput(content);
562
- this.autoResize();
563
- this.updateSendButtonState();
564
- this.config.onInputChange?.(this.textareaEl.value);
565
- }
566
- getInputContent() {
567
- return this.textareaEl?.value ?? "";
568
- }
569
- clearInput() {
570
- this.setInputContent("");
571
- }
572
- renderMentionPanel(members, activeIndex) {
573
- if (!this.mentionPanel) return;
574
- if (members.length === 0) {
575
- this.mentionPanel.style.display = "none";
576
- return;
577
- }
578
- this.mentionPanel.style.display = "block";
579
- this.mentionPanel.innerHTML = "";
580
- members.forEach((member, index) => {
581
- const item = this.el("div", cx(`${this.prefix}-mention-item`, index === activeIndex && `${this.prefix}-mention-item--active`));
582
- item.setAttribute("role", "option");
583
- item.setAttribute("aria-selected", String(index === activeIndex));
584
- const avatar = this.el("div", `${this.prefix}-mention-item-avatar`);
585
- if (member.avatar) {
586
- const img = document.createElement("img");
587
- img.src = member.avatar;
588
- img.alt = "";
589
- avatar.appendChild(img);
590
- } else avatar.textContent = member.name[0]?.toUpperCase() ?? "";
591
- item.appendChild(avatar);
592
- const name = this.el("span", `${this.prefix}-mention-item-name`);
593
- name.textContent = member.name;
594
- item.appendChild(name);
595
- item.addEventListener("click", () => {
596
- this.mentionEngine.select(index);
597
- });
598
- this.mentionPanel?.appendChild(item);
599
- });
600
- }
601
- hideMentionPanel() {
602
- if (this.mentionPanel) this.mentionPanel.style.display = "none";
603
- }
604
- destroy() {
605
- this.stopVoice();
606
- this.container.innerHTML = "";
607
- }
608
- renderInputArea() {
609
- this.inputArea = this.el("div", `${this.prefix}-input-area`);
610
- const wrapper = this.el("div", `${this.prefix}-input-wrapper`);
611
- this.mentionPanel = this.el("div", `${this.prefix}-mention-panel`);
612
- this.mentionPanel.style.display = "none";
613
- this.mentionPanel.setAttribute("role", "listbox");
614
- wrapper.appendChild(this.mentionPanel);
615
- if (this.config.enableToolbar !== false) this.renderToolbar(wrapper);
616
- this.textareaEl = document.createElement("textarea");
617
- this.textareaEl.className = `${this.prefix}-textarea`;
618
- this.textareaEl.placeholder = this.config.inputPlaceholder ?? "输入消息...";
619
- this.textareaEl.rows = 1;
620
- if (this.config.maxInputLength && this.config.maxInputLength > 0) this.textareaEl.maxLength = this.config.maxInputLength;
621
- wrapper.appendChild(this.textareaEl);
622
- this.dropOverlay = this.el("div", `${this.prefix}-drop-overlay`);
623
- this.dropOverlay.innerHTML = "<span>拖放文件到这里</span>";
624
- this.dropOverlay.style.display = "none";
625
- wrapper.appendChild(this.dropOverlay);
626
- this.inputArea.appendChild(wrapper);
627
- this.sendBtn = document.createElement("button");
628
- this.sendBtn.className = `${this.prefix}-send-btn`;
629
- this.sendBtn.innerHTML = SEND_ICON_SVG;
630
- this.sendBtn.title = "发送";
631
- this.sendBtn.type = "button";
632
- this.sendBtn.disabled = true;
633
- this.inputArea.appendChild(this.sendBtn);
634
- this.root.appendChild(this.inputArea);
635
- }
636
- renderToolbar(wrapper) {
637
- const toolbar = this.config.toolbarConfig ?? {};
638
- this.toolbarEl = this.el("div", `${this.prefix}-toolbar`);
639
- if (toolbar.image !== false) {
640
- const imageBtn = this.createToolbarBtn(ICON_IMAGE, "插入图片");
641
- imageBtn.addEventListener("click", () => this.openFilePicker("image/*"));
642
- this.toolbarEl.appendChild(imageBtn);
643
- }
644
- if (toolbar.file !== false) {
645
- const fileBtn = this.createToolbarBtn(ICON_PAPERCLIP, "插入文件");
646
- fileBtn.addEventListener("click", () => this.openFilePicker("*/*"));
647
- this.toolbarEl.appendChild(fileBtn);
648
- }
649
- if (toolbar.voice !== false) {
650
- this.voiceBtn = this.createToolbarBtn(ICON_MIC, "语音输入");
651
- this.voiceBtn.addEventListener("click", () => this.toggleVoice());
652
- this.toolbarEl.appendChild(this.voiceBtn);
653
- }
654
- wrapper.insertBefore(this.toolbarEl, wrapper.firstChild);
655
- }
656
- createToolbarBtn(iconSvg, title) {
657
- const btn = document.createElement("button");
658
- btn.className = `${this.prefix}-toolbar-btn`;
659
- btn.innerHTML = iconSvg;
660
- btn.title = title;
661
- btn.type = "button";
662
- return btn;
663
- }
664
- openFilePicker(accept) {
665
- const input = document.createElement("input");
666
- input.type = "file";
667
- input.accept = accept;
668
- input.multiple = true;
669
- input.style.display = "none";
670
- input.addEventListener("change", () => {
671
- const files = Array.from(input.files ?? []);
672
- input.remove();
673
- if (!files.length) return;
674
- const isImage = accept.startsWith("image");
675
- this.emitter.emit(isImage ? "toolbar-image" : "toolbar-file", files);
676
- if (isImage) this.config.onImageUpload?.(files);
677
- else this.config.onFileUpload?.(files);
678
- });
679
- document.body.appendChild(input);
680
- input.click();
681
- }
682
- toggleVoice() {
683
- if (this.isRecording) this.stopVoice();
684
- else this.startVoice();
685
- }
686
- startVoice() {
687
- if (typeof window === "undefined") return;
688
- const SpeechRecognition = window.SpeechRecognition || window.webkitSpeechRecognition;
689
- if (!SpeechRecognition) {
690
- this.emitter.emit("toolbar-voice-unsupported");
691
- this.config.onVoiceUnsupported?.();
692
- return;
693
- }
694
- try {
695
- this.lastVoiceTranscript = "";
696
- this.recognition = new SpeechRecognition();
697
- this.recognition.continuous = false;
698
- this.recognition.interimResults = true;
699
- this.recognition.lang = this.config.voiceLang ?? "zh-CN";
700
- this.recognition.onresult = (event) => {
701
- let transcript = "";
702
- for (let i = 0; i < event.results.length; i++) transcript += event.results[i][0].transcript;
703
- this.lastVoiceTranscript = transcript;
704
- if (event.results[event.results.length - 1]?.isFinal && this.textareaEl) this.insertTextAtCursor(transcript);
705
- };
706
- this.recognition.onend = () => {
707
- this.finishVoice();
708
- this.emitter.emit("toolbar-voice-end", this.lastVoiceTranscript);
709
- this.config.onVoiceEnd?.(this.lastVoiceTranscript);
710
- };
711
- this.recognition.onerror = (error) => {
712
- this.finishVoice();
713
- this.emitter.emit("toolbar-voice-error", error);
714
- this.config.onVoiceError?.(error);
715
- };
716
- this.isRecording = true;
717
- this.voiceBtn?.classList.add(`${this.prefix}-toolbar-btn--active`);
718
- if (this.voiceBtn) this.voiceBtn.innerHTML = ICON_MIC_OFF;
719
- this.recognition.start();
720
- this.emitter.emit("toolbar-voice-start");
721
- this.config.onVoiceStart?.();
722
- } catch (error) {
723
- this.finishVoice();
724
- this.emitter.emit("toolbar-voice-error", error);
725
- this.config.onVoiceError?.(error);
726
- }
714
+ refsByStyle.delete(styleId);
715
+ doc.getElementById(styleId)?.remove();
716
+ }
717
+ function cls(prefix, ...parts) {
718
+ const normalizedPrefix = normalizeClassPrefix(prefix);
719
+ return parts.map((part) => `${normalizedPrefix}-${part}`).join(" ");
720
+ }
721
+ const clsx = cx;
722
+ function upsertStyle(ownerDocument, styleId, cssText) {
723
+ let style = ownerDocument.getElementById(styleId);
724
+ if (!style) {
725
+ style = ownerDocument.createElement("style");
726
+ style.id = styleId;
727
+ style.dataset.ldesignChat = "true";
728
+ (ownerDocument.head ?? ownerDocument.documentElement).appendChild(style);
727
729
  }
728
- stopVoice() {
729
- if (!this.recognition) return;
730
- const recognition = this.recognition;
731
- this.recognition = null;
732
- try {
733
- recognition.stop();
734
- } catch {
735
- this.finishVoice();
736
- }
730
+ style.textContent = cssText;
731
+ }
732
+ function applyThemeTokens(root, tokens) {
733
+ Object.entries(themeVariableMap(tokens)).forEach(([name, value]) => {
734
+ root.style.setProperty(name, value);
735
+ });
736
+ }
737
+ function resolveDocument(ownerDocument) {
738
+ if (ownerDocument) return ownerDocument;
739
+ if (typeof document !== "undefined") return document;
740
+ throw new Error("[Chat] Styles require a browser Document.");
741
+ }
742
+ function getDocumentRefs(ownerDocument) {
743
+ let refs = styleRefs.get(ownerDocument);
744
+ if (!refs) {
745
+ refs = /* @__PURE__ */ new Map();
746
+ styleRefs.set(ownerDocument, refs);
737
747
  }
738
- finishVoice() {
739
- this.isRecording = false;
740
- this.recognition = null;
741
- this.voiceBtn?.classList.remove(`${this.prefix}-toolbar-btn--active`);
742
- if (this.voiceBtn) this.voiceBtn.innerHTML = ICON_MIC;
743
- }
744
- setupEventListeners() {
745
- if (this.textareaEl) {
746
- this.textareaEl.addEventListener("input", () => {
747
- const limited = this.limitInput(this.textareaEl?.value ?? "");
748
- if (this.textareaEl && limited !== this.textareaEl.value) this.textareaEl.value = limited;
749
- this.autoResize();
750
- this.updateSendButtonState();
751
- this.config.onInputChange?.(this.textareaEl?.value ?? "");
752
- if (this.config.enableMention && this.textareaEl) this.mentionEngine.handleInput(this.textareaEl.value, this.textareaEl.selectionStart);
753
- });
754
- this.textareaEl.addEventListener("keydown", (event) => {
755
- if (this.mentionEngine.getState().active) {
756
- if (event.key === "ArrowUp") {
757
- event.preventDefault();
758
- this.mentionEngine.moveUp();
759
- } else if (event.key === "ArrowDown") {
760
- event.preventDefault();
761
- this.mentionEngine.moveDown();
762
- } else if (event.key === "Enter" && !event.shiftKey) {
763
- event.preventDefault();
764
- const member = this.mentionEngine.selectCurrent();
765
- if (member) this.insertMention(member);
766
- } else if (event.key === "Escape") {
767
- event.preventDefault();
768
- this.mentionEngine.deactivate();
769
- }
770
- return;
771
- }
772
- if (this.config.enterToSend !== false && event.key === "Enter" && !event.shiftKey) {
773
- event.preventDefault();
774
- this.handleSend();
775
- }
776
- });
777
- this.textareaEl.addEventListener("paste", (event) => {
778
- const items = event.clipboardData?.items;
779
- if (!items) return;
780
- const files = [];
781
- for (let i = 0; i < items.length; i++) if (items[i].kind === "file") {
782
- const file = items[i].getAsFile();
783
- if (file) files.push(file);
784
- }
785
- if (files.length > 0) {
786
- event.preventDefault();
787
- this.emitter.emit("toolbar-paste", files);
788
- this.config.onPaste?.(files);
789
- }
790
- });
791
- }
792
- if (this.inputArea) {
793
- const wrapper = this.inputArea.querySelector(`.${this.prefix}-input-wrapper`);
794
- if (wrapper) this.setupDropEvents(wrapper);
795
- }
796
- this.sendBtn?.addEventListener("click", () => {
797
- this.handleSend();
798
- });
799
- this.emitter.on("mention-filter", (state) => {
800
- this.renderMentionPanel(state.filtered, state.activeIndex);
801
- });
802
- this.emitter.on("mention-active-change", (index) => {
803
- (this.mentionPanel?.querySelectorAll(`.${this.prefix}-mention-item`))?.forEach((el, i) => {
804
- el.classList.toggle(`${this.prefix}-mention-item--active`, i === index);
805
- el.setAttribute("aria-selected", String(i === index));
806
- });
807
- });
808
- this.emitter.on("mention-deactivate", () => {
809
- this.hideMentionPanel();
810
- });
811
- }
812
- setupDropEvents(wrapper) {
813
- let dragCounter = 0;
814
- wrapper.addEventListener("dragenter", (event) => {
815
- event.preventDefault();
816
- dragCounter++;
817
- if (this.dropOverlay) this.dropOverlay.style.display = "flex";
818
- });
819
- wrapper.addEventListener("dragleave", (event) => {
820
- event.preventDefault();
821
- dragCounter--;
822
- if (dragCounter <= 0) {
823
- dragCounter = 0;
824
- if (this.dropOverlay) this.dropOverlay.style.display = "none";
825
- }
826
- });
827
- wrapper.addEventListener("dragover", (event) => {
828
- event.preventDefault();
829
- });
830
- wrapper.addEventListener("drop", (event) => {
831
- event.preventDefault();
832
- dragCounter = 0;
833
- if (this.dropOverlay) this.dropOverlay.style.display = "none";
834
- const files = Array.from(event.dataTransfer?.files ?? []);
835
- if (files.length > 0) {
836
- this.emitter.emit("toolbar-drop", files);
837
- this.config.onDrop?.(files);
838
- }
839
- });
840
- }
841
- handleSend() {
842
- const content = (this.textareaEl?.value ?? "").trim();
843
- if (!content) return;
844
- const mentions = this.mentionEngine.parseMentions(content);
845
- this.emitter.emit("send", content, mentions);
846
- this.config.onSend?.(content, mentions);
847
- if (this.config.clearInputOnSend !== false) this.clearInput();
848
- this.mentionEngine.resetSelected();
849
- }
850
- insertMention(member) {
851
- if (!this.textareaEl) return;
852
- const state = this.mentionEngine.getState();
853
- const text = this.textareaEl.value;
854
- const pos = this.textareaEl.selectionStart;
855
- const insertText = this.mentionEngine.getInsertText(member);
856
- const before = text.slice(0, state.triggerPosition);
857
- const after = text.slice(pos);
858
- this.textareaEl.value = this.limitInput(before + insertText + after);
859
- const newPos = Math.min(before.length + insertText.length, this.textareaEl.value.length);
860
- this.textareaEl.setSelectionRange(newPos, newPos);
861
- this.textareaEl.focus();
862
- this.autoResize();
863
- this.updateSendButtonState();
864
- this.config.onInputChange?.(this.textareaEl.value);
865
- }
866
- insertTextAtCursor(text) {
867
- if (!this.textareaEl) return;
868
- const current = this.textareaEl.value;
869
- const start = this.textareaEl.selectionStart;
870
- const end = this.textareaEl.selectionEnd;
871
- this.textareaEl.value = this.limitInput(current.slice(0, start) + text + current.slice(end));
872
- const nextCursor = Math.min(start + text.length, this.textareaEl.value.length);
873
- this.textareaEl.setSelectionRange(nextCursor, nextCursor);
874
- this.autoResize();
875
- this.updateSendButtonState();
876
- this.config.onInputChange?.(this.textareaEl.value);
877
- }
878
- autoResize() {
879
- if (!this.textareaEl) return;
880
- this.textareaEl.style.height = "auto";
881
- const maxHeight = this.getCssSizeValue("--ldesign-chat-textarea-max-height", 120);
882
- this.textareaEl.style.height = `${Math.min(this.textareaEl.scrollHeight, maxHeight)}px`;
883
- }
884
- updateSendButtonState() {
885
- if (!this.sendBtn) return;
886
- this.sendBtn.disabled = !this.getInputContent().trim();
887
- }
888
- renderLoadMoreButton() {
889
- this.loadMoreEl = this.el("div", `${this.prefix}-load-more`);
890
- this.loadMoreEl.textContent = this.config.loadMoreText ?? "加载更多";
891
- this.loadMoreEl.addEventListener("click", () => {
892
- if (this.loadingMore) return;
893
- this.setLoadingMore(true);
894
- this.emitter.emit("load-more");
895
- Promise.resolve(this.config.onLoadMore?.()).finally(() => {
896
- this.setLoadingMore(false);
897
- });
898
- });
899
- this.messageListEl.appendChild(this.loadMoreEl);
900
- }
901
- renderTimeDivider(timestamp) {
902
- this.messageListEl.appendChild(this.createTimeDivider(timestamp));
903
- }
904
- createTimeDivider(timestamp) {
905
- const divider = this.el("div", `${this.prefix}-time-divider`);
906
- const span = document.createElement("span");
907
- span.textContent = this.formatTime(timestamp);
908
- divider.appendChild(span);
909
- return divider;
910
- }
911
- renderMessageRow(message) {
912
- this.messageListEl.appendChild(this.createMessageRow(message));
913
- }
914
- createMessageRow(message) {
915
- const p = this.prefix;
916
- if (message.type === "system") {
917
- const row2 = this.el("div", cx(`${p}-message-row`, `${p}-message-row--system`));
918
- row2.dataset.msgId = message.id;
919
- row2.dataset.timestamp = String(message.timestamp);
920
- const system = this.el("div", `${p}-system-message`);
921
- system.textContent = message.content ?? "";
922
- row2.appendChild(system);
923
- return row2;
924
- }
925
- const isOutgoing = message.direction === "outgoing";
926
- const row = this.el("div", cx(`${p}-message-row`, isOutgoing && `${p}-message-row--outgoing`));
927
- row.dataset.msgId = message.id;
928
- row.dataset.timestamp = String(message.timestamp);
929
- if (this.config.showAvatar !== false) row.appendChild(this.createAvatar(message.sender.nickname, message.sender.avatar));
930
- const body = this.el("div", `${p}-message-body`);
931
- if (this.config.showNickname !== false && !isOutgoing) {
932
- const nickname = this.el("div", `${p}-nickname`);
933
- nickname.textContent = message.sender.nickname;
934
- body.appendChild(nickname);
935
- }
936
- body.appendChild(this.createBubble(message, isOutgoing));
937
- body.appendChild(this.createMessageFooter(message, isOutgoing));
938
- row.appendChild(body);
939
- row.addEventListener("click", () => {
940
- this.emitter.emit("message-click", message);
941
- this.config.onMessageClick?.(message);
942
- });
943
- return row;
944
- }
945
- createAvatar(name, avatarUrl) {
946
- const avatar = this.el("div", `${this.prefix}-avatar`);
947
- if (avatarUrl) {
948
- const img = document.createElement("img");
949
- img.src = avatarUrl;
950
- img.alt = name;
951
- avatar.appendChild(img);
952
- } else avatar.textContent = name[0]?.toUpperCase() ?? "";
953
- return avatar;
954
- }
955
- createMessageFooter(message, isOutgoing) {
956
- const footer = this.el("div", `${this.prefix}-message-footer`);
957
- if (this.config.showTimestamp !== false) {
958
- const time = this.el("span", `${this.prefix}-timestamp`);
959
- time.textContent = this.formatShortTime(message.timestamp);
960
- footer.appendChild(time);
961
- }
962
- if (this.config.showStatus !== false && isOutgoing && message.status) {
963
- const status = this.el("span", cx(`${this.prefix}-status`, `${this.prefix}-status--${message.status}`));
964
- status.textContent = STATUS_MAP[message.status] ?? "";
965
- if (message.status === "failed") status.addEventListener("click", (event) => {
966
- event.stopPropagation();
967
- this.emitter.emit("message-retry", message);
968
- });
969
- footer.appendChild(status);
970
- }
971
- return footer;
972
- }
973
- createBubble(message, isOutgoing) {
974
- const p = this.prefix;
975
- const dirClass = isOutgoing ? `${p}-bubble--outgoing` : `${p}-bubble--incoming`;
976
- if (message.revoked) {
977
- const bubble = this.el("div", cx(`${p}-bubble`, dirClass, `${p}-bubble--revoked`));
978
- bubble.textContent = "消息已撤回";
979
- return bubble;
980
- }
981
- switch (message.type) {
982
- case "image": {
983
- const bubble = this.el("div", cx(`${p}-bubble`, dirClass, `${p}-bubble--image`));
984
- if (message.image) {
985
- const img = document.createElement("img");
986
- img.src = message.image.thumbUrl ?? message.image.url;
987
- img.alt = message.content ?? "";
988
- if (message.image.width) img.width = Math.min(message.image.width, 240);
989
- bubble.appendChild(img);
990
- }
991
- return bubble;
992
- }
993
- case "file": {
994
- const bubble = this.el("div", cx(`${p}-bubble`, dirClass, `${p}-bubble--file`));
995
- const icon = this.el("div", `${p}-file-icon`);
996
- icon.innerHTML = FILE_ICON;
997
- bubble.appendChild(icon);
998
- const info = this.el("div", `${p}-file-info`);
999
- const name = this.el("div", `${p}-file-name`);
1000
- name.textContent = message.file?.name ?? "file";
1001
- info.appendChild(name);
1002
- if (message.file?.size) {
1003
- const size = this.el("div", `${p}-file-size`);
1004
- size.textContent = this.formatFileSize(message.file.size);
1005
- info.appendChild(size);
1006
- }
1007
- bubble.appendChild(info);
1008
- return bubble;
1009
- }
1010
- case "custom": {
1011
- const bubble = this.el("div", cx(`${p}-bubble`, dirClass));
1012
- if (message.customHtml) bubble.innerHTML = message.customHtml;
1013
- else bubble.textContent = message.content ?? "";
1014
- return bubble;
1015
- }
1016
- default: {
1017
- const bubble = this.el("div", cx(`${p}-bubble`, dirClass, message.streaming && `${p}-bubble--streaming`));
1018
- bubble.innerHTML = this.renderTextContent(message.content ?? "", message.mentions);
1019
- return bubble;
1020
- }
1021
- }
1022
- }
1023
- renderTextContent(content, mentions) {
1024
- let html = this.escapeHtml(content);
1025
- if (mentions?.length) for (const mention of mentions) {
1026
- const tag = `<span class="${this.prefix}-mention-tag" data-mention-id="${this.escapeAttr(mention.id)}">@${this.escapeHtml(mention.name)}</span>`;
1027
- html = html.replace(new RegExp(`@${this.escapeRegex(mention.name)}`, "g"), tag);
1028
- }
1029
- return html.replace(/(https?:\/\/[^\s<]+)/g, `<a class="${this.prefix}-link" href="$1" target="_blank" rel="noopener noreferrer">$1</a>`);
1030
- }
1031
- renderEmpty() {
1032
- if (this.messageListEl.querySelector(`.${this.prefix}-empty`)) return;
1033
- const empty = this.el("div", `${this.prefix}-empty`);
1034
- empty.textContent = "暂无消息";
1035
- this.messageListEl.appendChild(empty);
1036
- }
1037
- removeEmpty() {
1038
- this.messageListEl.querySelector(`.${this.prefix}-empty`)?.remove();
1039
- }
1040
- el(tag, className) {
1041
- const el = document.createElement(tag);
1042
- el.className = className;
1043
- return el;
1044
- }
1045
- getMessageElements() {
1046
- return Array.from(this.messageListEl.querySelectorAll(`.${this.prefix}-message-row`));
1047
- }
1048
- formatTime(timestamp) {
1049
- if (this.config.formatTime) return this.config.formatTime(timestamp);
1050
- const date = new Date(timestamp);
1051
- const now = /* @__PURE__ */ new Date();
1052
- if (date.toDateString() === now.toDateString()) return date.toLocaleTimeString("zh-CN", {
1053
- hour: "2-digit",
1054
- minute: "2-digit"
1055
- });
1056
- return date.toLocaleDateString("zh-CN", {
1057
- month: "2-digit",
1058
- day: "2-digit",
1059
- hour: "2-digit",
1060
- minute: "2-digit"
1061
- });
1062
- }
1063
- formatShortTime(timestamp) {
1064
- return new Date(timestamp).toLocaleTimeString("zh-CN", {
1065
- hour: "2-digit",
1066
- minute: "2-digit"
1067
- });
1068
- }
1069
- formatFileSize(bytes) {
1070
- if (bytes < 1024) return `${bytes} B`;
1071
- if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
1072
- return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
1073
- }
1074
- limitInput(content) {
1075
- const max = this.config.maxInputLength;
1076
- if (!max || max <= 0) return content;
1077
- return content.slice(0, max);
1078
- }
1079
- getCssSizeValue(name, fallback) {
1080
- if (typeof window === "undefined" || !this.root) return fallback;
1081
- const value = window.getComputedStyle(this.root).getPropertyValue(name).trim();
1082
- const parsed = Number.parseFloat(value);
1083
- return Number.isFinite(parsed) ? parsed : fallback;
1084
- }
1085
- escapeHtml(str) {
1086
- const div = document.createElement("div");
1087
- div.textContent = str;
1088
- return div.innerHTML;
1089
- }
1090
- escapeAttr(str) {
1091
- return str.replace(/"/g, "&quot;").replace(/'/g, "&#39;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
1092
- }
1093
- escapeRegex(str) {
1094
- return str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
1095
- }
1096
- };
1097
- //#endregion
1098
- //#region src/renderer/style-manager.ts
1099
- const styleRefs = /* @__PURE__ */ new Map();
1100
- function injectStyles(tokens, prefix = DEFAULT_CLASS_PREFIX) {
1101
- const normalizedPrefix = normalizeClassPrefix(prefix);
1102
- const styleId = getStyleElementId(normalizedPrefix);
1103
- styleRefs.set(styleId, (styleRefs.get(styleId) ?? 0) + 1);
1104
- upsertStyle(styleId, generateCSS(tokens, normalizedPrefix));
1105
- }
1106
- function updateStyles(tokens, prefix = DEFAULT_CLASS_PREFIX) {
1107
- const normalizedPrefix = normalizeClassPrefix(prefix);
1108
- upsertStyle(getStyleElementId(normalizedPrefix), generateCSS(tokens, normalizedPrefix));
1109
- }
1110
- function removeStyles(prefix = DEFAULT_CLASS_PREFIX) {
1111
- const styleId = getStyleElementId(prefix);
1112
- const refs = styleRefs.get(styleId) ?? 0;
1113
- if (refs > 1) {
1114
- styleRefs.set(styleId, refs - 1);
1115
- return;
1116
- }
1117
- styleRefs.delete(styleId);
1118
- document.getElementById(styleId)?.remove();
1119
- }
1120
- function cls(prefix, ...parts) {
1121
- const normalizedPrefix = normalizeClassPrefix(prefix);
1122
- return parts.map((part) => `${normalizedPrefix}-${part}`).join(" ");
1123
- }
1124
- const clsx = cx;
1125
- function upsertStyle(styleId, cssText) {
1126
- let style = document.getElementById(styleId);
1127
- if (!style) {
1128
- style = document.createElement("style");
1129
- style.id = styleId;
1130
- style.dataset.ldesignChat = "true";
1131
- document.head.appendChild(style);
1132
- }
1133
- style.textContent = cssText;
748
+ return refs;
1134
749
  }
1135
750
  function tokenVars(t) {
751
+ return Object.entries(themeVariableMap(t)).map(([key, value]) => ` ${key}: ${value};`).join("\n");
752
+ }
753
+ function themeVariableMap(t) {
1136
754
  const vars = {
1137
755
  bgPrimary: t.bgPrimary,
1138
756
  bgSecondary: t.bgSecondary,
@@ -1180,51 +798,52 @@
1180
798
  toolbarIconHover: t.toolbarIconHover,
1181
799
  toolbarVoiceActive: t.toolbarVoiceActive
1182
800
  };
1183
- return Object.entries(vars).map(([key, value]) => ` ${COMPONENT_CSS_VAR_PREFIX}-${toKebab(key)}: ${value};`).join("\n");
801
+ return Object.fromEntries(Object.entries(vars).map(([key, value]) => [`${COMPONENT_CSS_VAR_PREFIX}-${toKebab(key)}`, value]));
1184
802
  }
1185
803
  function sizeVars() {
1186
804
  return `
1187
- ${COMPONENT_CSS_VAR_PREFIX}-font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'PingFang SC', 'Hiragino Sans GB', 'Microsoft YaHei', sans-serif;
1188
- ${COMPONENT_CSS_VAR_PREFIX}-font-size: var(--size-font-base);
1189
- ${COMPONENT_CSS_VAR_PREFIX}-font-size-sm: var(--size-font-sm);
1190
- ${COMPONENT_CSS_VAR_PREFIX}-font-size-xs: var(--size-font-xs);
1191
- ${COMPONENT_CSS_VAR_PREFIX}-font-size-lg: var(--size-font-lg);
1192
- ${COMPONENT_CSS_VAR_PREFIX}-font-weight-regular: var(--size-font-weight-regular);
1193
- ${COMPONENT_CSS_VAR_PREFIX}-font-weight-medium: var(--size-font-weight-medium);
1194
- ${COMPONENT_CSS_VAR_PREFIX}-font-weight-semibold: var(--size-font-weight-semibold);
1195
- ${COMPONENT_CSS_VAR_PREFIX}-line-height: var(--size-line-normal);
1196
- ${COMPONENT_CSS_VAR_PREFIX}-line-height-bubble: var(--size-line-relaxed);
1197
- ${COMPONENT_CSS_VAR_PREFIX}-space-1: var(--size-1);
1198
- ${COMPONENT_CSS_VAR_PREFIX}-space-2: var(--size-2);
1199
- ${COMPONENT_CSS_VAR_PREFIX}-space-3: var(--size-3);
1200
- ${COMPONENT_CSS_VAR_PREFIX}-space-4: var(--size-4);
1201
- ${COMPONENT_CSS_VAR_PREFIX}-space-5: var(--size-5);
1202
- ${COMPONENT_CSS_VAR_PREFIX}-space-6: var(--size-6);
1203
- ${COMPONENT_CSS_VAR_PREFIX}-space-8: var(--size-8);
1204
- ${COMPONENT_CSS_VAR_PREFIX}-avatar-size: var(--size-11);
1205
- ${COMPONENT_CSS_VAR_PREFIX}-mention-avatar-size: var(--size-9);
1206
- ${COMPONENT_CSS_VAR_PREFIX}-toolbar-button-size: var(--size-9);
1207
- ${COMPONENT_CSS_VAR_PREFIX}-input-button-size: var(--size-11);
1208
- ${COMPONENT_CSS_VAR_PREFIX}-file-icon-size: var(--size-12);
1209
- ${COMPONENT_CSS_VAR_PREFIX}-image-max-size: calc(var(--size-13) * 5);
1210
- ${COMPONENT_CSS_VAR_PREFIX}-panel-max-height: calc(var(--size-24) + var(--size-4));
1211
- ${COMPONENT_CSS_VAR_PREFIX}-textarea-min-height: var(--size-11);
1212
- ${COMPONENT_CSS_VAR_PREFIX}-textarea-max-height: calc(var(--size-12) * 3);
805
+ ${COMPONENT_CSS_VAR_PREFIX}-font-family: var(--ld-font-family);
806
+ ${COMPONENT_CSS_VAR_PREFIX}-font-size: var(--ld-font-base);
807
+ ${COMPONENT_CSS_VAR_PREFIX}-font-size-sm: var(--ld-font-sm);
808
+ ${COMPONENT_CSS_VAR_PREFIX}-font-size-xs: var(--ld-font-xs);
809
+ ${COMPONENT_CSS_VAR_PREFIX}-font-size-lg: var(--ld-font-lg);
810
+ ${COMPONENT_CSS_VAR_PREFIX}-font-weight-regular: 400;
811
+ ${COMPONENT_CSS_VAR_PREFIX}-font-weight-medium: 500;
812
+ ${COMPONENT_CSS_VAR_PREFIX}-font-weight-semibold: 600;
813
+ ${COMPONENT_CSS_VAR_PREFIX}-line-height: var(--ld-line-normal);
814
+ ${COMPONENT_CSS_VAR_PREFIX}-line-height-bubble: 1.6;
815
+ ${COMPONENT_CSS_VAR_PREFIX}-space-1: var(--ld-spacing-3xs);
816
+ ${COMPONENT_CSS_VAR_PREFIX}-space-2: var(--ld-spacing-2xs);
817
+ ${COMPONENT_CSS_VAR_PREFIX}-space-3: var(--ld-spacing-xs);
818
+ ${COMPONENT_CSS_VAR_PREFIX}-space-4: var(--ld-spacing-sm);
819
+ ${COMPONENT_CSS_VAR_PREFIX}-space-5: var(--ld-spacing-md);
820
+ ${COMPONENT_CSS_VAR_PREFIX}-space-6: var(--ld-spacing-lg);
821
+ ${COMPONENT_CSS_VAR_PREFIX}-space-8: var(--ld-spacing-xl);
822
+ ${COMPONENT_CSS_VAR_PREFIX}-avatar-size: calc(var(--ld-px-40) + var(--ld-px-4));
823
+ ${COMPONENT_CSS_VAR_PREFIX}-mention-avatar-size: var(--ld-px-36);
824
+ ${COMPONENT_CSS_VAR_PREFIX}-toolbar-button-size: var(--ld-px-36);
825
+ ${COMPONENT_CSS_VAR_PREFIX}-input-button-size: calc(var(--ld-px-40) + var(--ld-px-4));
826
+ ${COMPONENT_CSS_VAR_PREFIX}-file-icon-size: var(--ld-px-48);
827
+ ${COMPONENT_CSS_VAR_PREFIX}-image-max-size: var(--ld-px-260);
828
+ ${COMPONENT_CSS_VAR_PREFIX}-panel-max-height: var(--ld-px-220);
829
+ ${COMPONENT_CSS_VAR_PREFIX}-textarea-min-height: calc(var(--ld-px-40) + var(--ld-px-4));
830
+ ${COMPONENT_CSS_VAR_PREFIX}-textarea-max-height: calc(var(--ld-px-64) + var(--ld-px-56));
1213
831
  ${COMPONENT_CSS_VAR_PREFIX}-message-max-width: 65%;
1214
832
  ${COMPONENT_CSS_VAR_PREFIX}-system-message-max-width: 80%;
1215
- ${COMPONENT_CSS_VAR_PREFIX}-file-bubble-min-width: calc(var(--size-24) + var(--size-4));
1216
- ${COMPONENT_CSS_VAR_PREFIX}-border-width: var(--size-border-width-thin);
1217
- ${COMPONENT_CSS_VAR_PREFIX}-border-width-drop: var(--size-border-width-medium);
1218
- ${COMPONENT_CSS_VAR_PREFIX}-radius-xs: var(--size-radius-sm);
1219
- ${COMPONENT_CSS_VAR_PREFIX}-radius-sm: var(--size-radius-md);
1220
- ${COMPONENT_CSS_VAR_PREFIX}-radius-md: var(--size-radius-lg);
1221
- ${COMPONENT_CSS_VAR_PREFIX}-radius-lg: var(--size-radius-xl);
1222
- ${COMPONENT_CSS_VAR_PREFIX}-radius-pill: var(--size-radius-full);
1223
- ${COMPONENT_CSS_VAR_PREFIX}-radius-circle: var(--size-radius-circle);
1224
- ${COMPONENT_CSS_VAR_PREFIX}-transition-fast: var(--size-duration-fast) var(--size-ease-out);
1225
- ${COMPONENT_CSS_VAR_PREFIX}-transition-normal: var(--size-duration-normal) var(--size-ease-out);
1226
- ${COMPONENT_CSS_VAR_PREFIX}-z-overlay: var(--size-z-dropdown);
1227
- ${COMPONENT_CSS_VAR_PREFIX}-z-mention: var(--size-z-popover);
833
+ ${COMPONENT_CSS_VAR_PREFIX}-file-bubble-min-width: var(--ld-px-220);
834
+ ${COMPONENT_CSS_VAR_PREFIX}-border-width: var(--ld-border-width-thin);
835
+ ${COMPONENT_CSS_VAR_PREFIX}-border-width-drop: var(--ld-border-width-medium);
836
+ ${COMPONENT_CSS_VAR_PREFIX}-radius-xs: var(--ld-radius-sm);
837
+ ${COMPONENT_CSS_VAR_PREFIX}-radius-sm: var(--ld-radius-md);
838
+ ${COMPONENT_CSS_VAR_PREFIX}-radius-md: var(--ld-radius-lg);
839
+ ${COMPONENT_CSS_VAR_PREFIX}-radius-lg: var(--ld-radius-xl);
840
+ ${COMPONENT_CSS_VAR_PREFIX}-radius-pill: var(--ld-radius-full);
841
+ ${COMPONENT_CSS_VAR_PREFIX}-radius-circle: var(--ld-radius-full);
842
+ ${COMPONENT_CSS_VAR_PREFIX}-transition-fast: 150ms cubic-bezier(0.4, 0, 0.2, 1);
843
+ ${COMPONENT_CSS_VAR_PREFIX}-transition-normal: 200ms cubic-bezier(0.4, 0, 0.2, 1);
844
+ ${COMPONENT_CSS_VAR_PREFIX}-z-overlay: 1000;
845
+ ${COMPONENT_CSS_VAR_PREFIX}-z-mention: 1100;
846
+ ${COMPONENT_CSS_VAR_PREFIX}-icon-size: var(--ld-px-24);
1228
847
  `;
1229
848
  }
1230
849
  function generateCSS(tokens, p) {
@@ -1264,7 +883,7 @@ ${sizeVars()}
1264
883
  }
1265
884
 
1266
885
  .${p}-message-list::-webkit-scrollbar {
1267
- width: var(--size-3);
886
+ width: var(--ld-px-4);
1268
887
  }
1269
888
 
1270
889
  .${p}-message-list::-webkit-scrollbar-thumb {
@@ -1563,8 +1182,8 @@ ${sizeVars()}
1563
1182
  }
1564
1183
 
1565
1184
  .${p}-send-btn svg {
1566
- width: var(--size-7);
1567
- height: var(--size-7);
1185
+ width: var(--ldesign-chat-icon-size);
1186
+ height: var(--ldesign-chat-icon-size);
1568
1187
  }
1569
1188
 
1570
1189
  .${p}-mention-panel {
@@ -1661,8 +1280,8 @@ ${sizeVars()}
1661
1280
  }
1662
1281
 
1663
1282
  .${p}-toolbar-btn svg {
1664
- width: var(--size-6);
1665
- height: var(--size-6);
1283
+ width: var(--ldesign-chat-space-6);
1284
+ height: var(--ldesign-chat-space-6);
1666
1285
  }
1667
1286
 
1668
1287
  .${p}-toolbar-btn:hover {
@@ -1716,18 +1335,1251 @@ ${sizeVars()}
1716
1335
  }
1717
1336
  `;
1718
1337
  }
1719
- function v(name) {
1720
- return `var(${COMPONENT_CSS_VAR_PREFIX}-${name})`;
1338
+ function v(name) {
1339
+ return `var(${COMPONENT_CSS_VAR_PREFIX}-${name})`;
1340
+ }
1341
+ function toKebab(key) {
1342
+ return key.replace(/[A-Z]/g, (match) => `-${match.toLowerCase()}`);
1343
+ }
1344
+ //#endregion
1345
+ //#region src/renderer/dom-renderer.ts
1346
+ 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>";
1347
+ 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>";
1348
+ const ICON_PAPERCLIP = "<svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"m21.44 11.05-9.19 9.19a6 6 0 0 1-8.49-8.49l8.57-8.57A4 4 0 1 1 18 8.84l-8.59 8.57a2 2 0 0 1-2.83-2.83l8.49-8.48\"/></svg>";
1349
+ 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>";
1350
+ 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>";
1351
+ 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>";
1352
+ const ALLOWED_CUSTOM_TAGS = /* @__PURE__ */ new Set([
1353
+ "a",
1354
+ "b",
1355
+ "blockquote",
1356
+ "br",
1357
+ "code",
1358
+ "div",
1359
+ "em",
1360
+ "h1",
1361
+ "h2",
1362
+ "h3",
1363
+ "h4",
1364
+ "h5",
1365
+ "h6",
1366
+ "i",
1367
+ "img",
1368
+ "li",
1369
+ "ol",
1370
+ "p",
1371
+ "pre",
1372
+ "s",
1373
+ "span",
1374
+ "strong",
1375
+ "u",
1376
+ "ul"
1377
+ ]);
1378
+ const DANGEROUS_CUSTOM_TAGS = /* @__PURE__ */ new Set([
1379
+ "iframe",
1380
+ "object",
1381
+ "embed",
1382
+ "script",
1383
+ "style",
1384
+ "svg",
1385
+ "math",
1386
+ "template",
1387
+ "form",
1388
+ "input",
1389
+ "button",
1390
+ "link",
1391
+ "meta",
1392
+ "base"
1393
+ ]);
1394
+ const STATUS_MAP = {
1395
+ sending: "...",
1396
+ sent: "✓",
1397
+ delivered: "✓✓",
1398
+ read: "✓✓",
1399
+ failed: "!"
1400
+ };
1401
+ var DOMRenderer = class {
1402
+ constructor(container, config, emitter, mentionEngine) {
1403
+ this.inputArea = null;
1404
+ this.textareaEl = null;
1405
+ this.sendBtn = null;
1406
+ this.mentionPanel = null;
1407
+ this.loadMoreEl = null;
1408
+ this.toolbarEl = null;
1409
+ this.dropOverlay = null;
1410
+ this.voiceBtn = null;
1411
+ this.loadingMore = false;
1412
+ this.isRecording = false;
1413
+ this.recognition = null;
1414
+ this.lastVoiceTranscript = "";
1415
+ this.destroyed = false;
1416
+ this.pendingFileInputs = /* @__PURE__ */ new Set();
1417
+ this.container = container;
1418
+ this.document = container.ownerDocument;
1419
+ this.config = config;
1420
+ this.emitter = emitter;
1421
+ this.mentionEngine = mentionEngine;
1422
+ this.prefix = normalizeClassPrefix(config.classPrefix ?? "ldesign-chat");
1423
+ }
1424
+ init() {
1425
+ if (this.destroyed) return;
1426
+ this.container.innerHTML = "";
1427
+ this.root = this.el("div", `${this.prefix}-container`);
1428
+ this.messageListEl = this.el("div", `${this.prefix}-message-list`);
1429
+ this.root.appendChild(this.messageListEl);
1430
+ if (this.config.showInput !== false) this.renderInputArea();
1431
+ this.container.appendChild(this.root);
1432
+ this.setupEventListeners();
1433
+ }
1434
+ updateMessageContent(id, content) {
1435
+ const bubble = this.getMessageRow(id)?.querySelector(`.${this.prefix}-bubble`);
1436
+ if (!bubble) return;
1437
+ bubble.innerHTML = this.renderTextContent(content);
1438
+ }
1439
+ renderMessages(messages) {
1440
+ this.messageListEl.innerHTML = "";
1441
+ if (this.config.enableLoadMore) this.renderLoadMoreButton();
1442
+ if (messages.length === 0) {
1443
+ this.renderEmpty();
1444
+ return;
1445
+ }
1446
+ let lastTimestamp = 0;
1447
+ const interval = this.config.timeGroupInterval ?? 3e5;
1448
+ for (const message of messages) {
1449
+ if (this.config.showTimestamp !== false && message.timestamp - lastTimestamp > interval) {
1450
+ this.renderTimeDivider(message.timestamp);
1451
+ lastTimestamp = message.timestamp;
1452
+ }
1453
+ this.renderMessageRow(message);
1454
+ }
1455
+ }
1456
+ appendMessage(message) {
1457
+ this.removeEmpty();
1458
+ const messageEls = this.getMessageElements();
1459
+ const lastTime = messageEls.length > 0 ? Number.parseInt(messageEls[messageEls.length - 1].dataset.timestamp ?? "0", 10) : 0;
1460
+ const interval = this.config.timeGroupInterval ?? 3e5;
1461
+ if (this.config.showTimestamp !== false && message.timestamp - lastTime > interval) this.renderTimeDivider(message.timestamp);
1462
+ this.renderMessageRow(message);
1463
+ }
1464
+ prependMessages(messages) {
1465
+ if (!messages.length) return;
1466
+ this.removeEmpty();
1467
+ const scrollHeight = this.messageListEl.scrollHeight;
1468
+ const scrollTop = this.messageListEl.scrollTop;
1469
+ const fragment = this.document.createDocumentFragment();
1470
+ let lastTimestamp = 0;
1471
+ const interval = this.config.timeGroupInterval ?? 3e5;
1472
+ for (const message of messages) {
1473
+ if (this.config.showTimestamp !== false && message.timestamp - lastTimestamp > interval) {
1474
+ fragment.appendChild(this.createTimeDivider(message.timestamp));
1475
+ lastTimestamp = message.timestamp;
1476
+ }
1477
+ fragment.appendChild(this.createMessageRow(message));
1478
+ }
1479
+ const firstMessage = this.messageListEl.querySelector(`.${this.prefix}-message-row, .${this.prefix}-time-divider`);
1480
+ if (firstMessage) firstMessage.before(fragment);
1481
+ else if (this.loadMoreEl) this.loadMoreEl.after(fragment);
1482
+ else this.messageListEl.appendChild(fragment);
1483
+ this.messageListEl.scrollTop = scrollTop + (this.messageListEl.scrollHeight - scrollHeight);
1484
+ }
1485
+ updateMessage(id, message) {
1486
+ const rowEl = this.getMessageRow(id);
1487
+ if (!rowEl) return;
1488
+ rowEl.replaceWith(this.createMessageRow({
1489
+ ...message,
1490
+ id
1491
+ }));
1492
+ }
1493
+ removeMessage(id) {
1494
+ this.getMessageRow(id)?.remove();
1495
+ this.refreshTimeDividers();
1496
+ if (this.getMessageElements().length === 0) this.renderEmpty();
1497
+ else this.removeEmpty();
1498
+ }
1499
+ scrollToBottom(smooth = true) {
1500
+ if (typeof this.messageListEl.scrollTo === "function") {
1501
+ this.messageListEl.scrollTo({
1502
+ top: this.messageListEl.scrollHeight,
1503
+ behavior: smooth ? "smooth" : "auto"
1504
+ });
1505
+ return;
1506
+ }
1507
+ this.messageListEl.scrollTop = this.messageListEl.scrollHeight;
1508
+ }
1509
+ isAtBottom(threshold = 4) {
1510
+ return this.messageListEl.scrollHeight - this.messageListEl.scrollTop - this.messageListEl.clientHeight <= threshold;
1511
+ }
1512
+ setLoadingMore(loading) {
1513
+ this.loadingMore = loading;
1514
+ if (!this.loadMoreEl) return;
1515
+ this.loadMoreEl.textContent = loading ? "加载中..." : this.config.loadMoreText ?? "加载更多";
1516
+ this.loadMoreEl.classList.toggle(`${this.prefix}-load-more--loading`, loading);
1517
+ }
1518
+ setThemeTokens(tokens) {
1519
+ if (this.root) applyThemeTokens(this.root, tokens);
1520
+ }
1521
+ getRootElement() {
1522
+ return this.root;
1523
+ }
1524
+ focusInput() {
1525
+ this.textareaEl?.focus();
1526
+ }
1527
+ setInputContent(content) {
1528
+ if (!this.textareaEl) return;
1529
+ this.textareaEl.value = this.limitInput(content);
1530
+ this.autoResize();
1531
+ this.updateSendButtonState();
1532
+ this.config.onInputChange?.(this.textareaEl.value);
1533
+ }
1534
+ getInputContent() {
1535
+ return this.textareaEl?.value ?? "";
1536
+ }
1537
+ clearInput() {
1538
+ this.setInputContent("");
1539
+ }
1540
+ renderMentionPanel(members, activeIndex) {
1541
+ if (!this.mentionPanel) return;
1542
+ if (members.length === 0) {
1543
+ this.mentionPanel.style.display = "none";
1544
+ return;
1545
+ }
1546
+ this.mentionPanel.style.display = "block";
1547
+ this.mentionPanel.innerHTML = "";
1548
+ members.forEach((member, index) => {
1549
+ const item = this.el("div", cx(`${this.prefix}-mention-item`, index === activeIndex && `${this.prefix}-mention-item--active`));
1550
+ item.setAttribute("role", "option");
1551
+ item.setAttribute("aria-selected", String(index === activeIndex));
1552
+ const avatar = this.el("div", `${this.prefix}-mention-item-avatar`);
1553
+ if (member.avatar) {
1554
+ const img = this.document.createElement("img");
1555
+ img.src = this.safeResourceUrl(member.avatar);
1556
+ img.alt = "";
1557
+ avatar.appendChild(img);
1558
+ } else avatar.textContent = member.name[0]?.toUpperCase() ?? "";
1559
+ item.appendChild(avatar);
1560
+ const name = this.el("span", `${this.prefix}-mention-item-name`);
1561
+ name.textContent = member.name;
1562
+ item.appendChild(name);
1563
+ item.addEventListener("mousedown", (event) => event.preventDefault());
1564
+ item.addEventListener("click", () => {
1565
+ const triggerPosition = this.mentionEngine.getState().triggerPosition;
1566
+ const selectionEnd = this.textareaEl?.selectionEnd;
1567
+ const selected = this.mentionEngine.select(index);
1568
+ if (selected) this.insertMention(selected, triggerPosition, selectionEnd);
1569
+ });
1570
+ this.mentionPanel?.appendChild(item);
1571
+ });
1572
+ }
1573
+ hideMentionPanel() {
1574
+ if (this.mentionPanel) this.mentionPanel.style.display = "none";
1575
+ }
1576
+ destroy() {
1577
+ if (this.destroyed) return;
1578
+ this.destroyed = true;
1579
+ this.stopVoice();
1580
+ this.finishVoice();
1581
+ this.pendingFileInputs.forEach((input) => input.remove());
1582
+ this.pendingFileInputs.clear();
1583
+ this.root?.remove();
1584
+ }
1585
+ renderInputArea() {
1586
+ this.inputArea = this.el("div", `${this.prefix}-input-area`);
1587
+ const wrapper = this.el("div", `${this.prefix}-input-wrapper`);
1588
+ this.mentionPanel = this.el("div", `${this.prefix}-mention-panel`);
1589
+ this.mentionPanel.style.display = "none";
1590
+ this.mentionPanel.setAttribute("role", "listbox");
1591
+ wrapper.appendChild(this.mentionPanel);
1592
+ if (this.config.enableToolbar !== false) this.renderToolbar(wrapper);
1593
+ this.textareaEl = this.document.createElement("textarea");
1594
+ this.textareaEl.className = `${this.prefix}-textarea`;
1595
+ this.textareaEl.placeholder = this.config.inputPlaceholder ?? "输入消息...";
1596
+ this.textareaEl.rows = 1;
1597
+ if (this.config.maxInputLength && this.config.maxInputLength > 0) this.textareaEl.maxLength = this.config.maxInputLength;
1598
+ wrapper.appendChild(this.textareaEl);
1599
+ this.dropOverlay = this.el("div", `${this.prefix}-drop-overlay`);
1600
+ this.dropOverlay.innerHTML = "<span>拖放文件到这里</span>";
1601
+ this.dropOverlay.style.display = "none";
1602
+ wrapper.appendChild(this.dropOverlay);
1603
+ this.inputArea.appendChild(wrapper);
1604
+ this.sendBtn = this.document.createElement("button");
1605
+ this.sendBtn.className = `${this.prefix}-send-btn`;
1606
+ this.sendBtn.innerHTML = SEND_ICON_SVG;
1607
+ this.sendBtn.title = "发送";
1608
+ this.sendBtn.type = "button";
1609
+ this.sendBtn.disabled = true;
1610
+ this.inputArea.appendChild(this.sendBtn);
1611
+ this.root.appendChild(this.inputArea);
1612
+ }
1613
+ renderToolbar(wrapper) {
1614
+ const toolbar = this.config.toolbarConfig ?? {};
1615
+ this.toolbarEl = this.el("div", `${this.prefix}-toolbar`);
1616
+ if (toolbar.image !== false) {
1617
+ const imageBtn = this.createToolbarBtn(ICON_IMAGE, "插入图片");
1618
+ imageBtn.addEventListener("click", () => this.openFilePicker("image/*"));
1619
+ this.toolbarEl.appendChild(imageBtn);
1620
+ }
1621
+ if (toolbar.file !== false) {
1622
+ const fileBtn = this.createToolbarBtn(ICON_PAPERCLIP, "插入文件");
1623
+ fileBtn.addEventListener("click", () => this.openFilePicker("*/*"));
1624
+ this.toolbarEl.appendChild(fileBtn);
1625
+ }
1626
+ if (toolbar.voice !== false) {
1627
+ this.voiceBtn = this.createToolbarBtn(ICON_MIC, "语音输入");
1628
+ this.voiceBtn.addEventListener("click", () => this.toggleVoice());
1629
+ this.toolbarEl.appendChild(this.voiceBtn);
1630
+ }
1631
+ wrapper.insertBefore(this.toolbarEl, wrapper.firstChild);
1632
+ }
1633
+ createToolbarBtn(iconSvg, title) {
1634
+ const btn = this.document.createElement("button");
1635
+ btn.className = `${this.prefix}-toolbar-btn`;
1636
+ btn.innerHTML = iconSvg;
1637
+ btn.title = title;
1638
+ btn.type = "button";
1639
+ return btn;
1640
+ }
1641
+ openFilePicker(accept) {
1642
+ const input = this.document.createElement("input");
1643
+ input.type = "file";
1644
+ input.accept = accept;
1645
+ input.multiple = true;
1646
+ input.style.display = "none";
1647
+ this.pendingFileInputs.add(input);
1648
+ const cleanup = () => {
1649
+ input.remove();
1650
+ this.pendingFileInputs.delete(input);
1651
+ };
1652
+ input.addEventListener("change", () => {
1653
+ const files = Array.from(input.files ?? []);
1654
+ cleanup();
1655
+ if (!files.length) return;
1656
+ const isImage = accept.startsWith("image");
1657
+ this.emitter.emit(isImage ? "toolbar-image" : "toolbar-file", files);
1658
+ if (isImage) this.config.onImageUpload?.(files);
1659
+ else this.config.onFileUpload?.(files);
1660
+ });
1661
+ input.addEventListener("cancel", cleanup);
1662
+ (this.document.body ?? this.document.documentElement).appendChild(input);
1663
+ input.click();
1664
+ }
1665
+ toggleVoice() {
1666
+ if (this.isRecording) this.stopVoice();
1667
+ else this.startVoice();
1668
+ }
1669
+ startVoice() {
1670
+ const view = this.document.defaultView;
1671
+ const SpeechRecognition = view?.SpeechRecognition || view?.webkitSpeechRecognition;
1672
+ if (!SpeechRecognition) {
1673
+ this.emitter.emit("toolbar-voice-unsupported");
1674
+ this.config.onVoiceUnsupported?.();
1675
+ return;
1676
+ }
1677
+ try {
1678
+ this.lastVoiceTranscript = "";
1679
+ const recognition = new SpeechRecognition();
1680
+ const finalSegments = /* @__PURE__ */ new Map();
1681
+ recognition.continuous = false;
1682
+ recognition.interimResults = true;
1683
+ recognition.lang = this.config.voiceLang ?? "zh-CN";
1684
+ this.recognition = recognition;
1685
+ recognition.onresult = (event) => {
1686
+ if (this.destroyed || this.recognition !== recognition) return;
1687
+ const firstChangedResult = Math.max(0, event.resultIndex ?? 0);
1688
+ const newFinalSegments = [];
1689
+ for (let index = firstChangedResult; index < event.results.length; index++) {
1690
+ const result = event.results[index];
1691
+ if (!result?.isFinal || finalSegments.has(index)) continue;
1692
+ finalSegments.set(index, result[0].transcript);
1693
+ newFinalSegments.push(result[0].transcript);
1694
+ }
1695
+ this.lastVoiceTranscript = [...finalSegments.entries()].sort(([left], [right]) => left - right).map(([, transcript]) => transcript).join("");
1696
+ if (newFinalSegments.length && this.textareaEl) this.insertTextAtCursor(newFinalSegments.join(""));
1697
+ };
1698
+ recognition.onend = () => {
1699
+ if (this.destroyed || this.recognition !== recognition) return;
1700
+ const transcript = this.lastVoiceTranscript;
1701
+ this.finishVoice();
1702
+ this.emitter.emit("toolbar-voice-end", transcript);
1703
+ this.config.onVoiceEnd?.(transcript);
1704
+ };
1705
+ recognition.onerror = (error) => {
1706
+ if (this.destroyed || this.recognition !== recognition) return;
1707
+ this.finishVoice();
1708
+ this.emitter.emit("toolbar-voice-error", error);
1709
+ this.config.onVoiceError?.(error);
1710
+ };
1711
+ this.isRecording = true;
1712
+ this.voiceBtn?.classList.add(`${this.prefix}-toolbar-btn--active`);
1713
+ if (this.voiceBtn) this.voiceBtn.innerHTML = ICON_MIC_OFF;
1714
+ recognition.start();
1715
+ this.emitter.emit("toolbar-voice-start");
1716
+ this.config.onVoiceStart?.();
1717
+ } catch (error) {
1718
+ this.finishVoice();
1719
+ this.emitter.emit("toolbar-voice-error", error);
1720
+ this.config.onVoiceError?.(error);
1721
+ }
1722
+ }
1723
+ stopVoice() {
1724
+ if (!this.recognition) return;
1725
+ const recognition = this.recognition;
1726
+ try {
1727
+ recognition.stop();
1728
+ } catch {
1729
+ this.finishVoice();
1730
+ }
1731
+ }
1732
+ finishVoice() {
1733
+ this.isRecording = false;
1734
+ this.recognition = null;
1735
+ this.voiceBtn?.classList.remove(`${this.prefix}-toolbar-btn--active`);
1736
+ if (this.voiceBtn) this.voiceBtn.innerHTML = ICON_MIC;
1737
+ }
1738
+ setupEventListeners() {
1739
+ if (this.textareaEl) {
1740
+ this.textareaEl.addEventListener("input", () => {
1741
+ const limited = this.limitInput(this.textareaEl?.value ?? "");
1742
+ if (this.textareaEl && limited !== this.textareaEl.value) this.textareaEl.value = limited;
1743
+ this.autoResize();
1744
+ this.updateSendButtonState();
1745
+ this.config.onInputChange?.(this.textareaEl?.value ?? "");
1746
+ if (this.config.enableMention && this.textareaEl) this.mentionEngine.handleInput(this.textareaEl.value, this.textareaEl.selectionStart);
1747
+ });
1748
+ this.textareaEl.addEventListener("keydown", (event) => {
1749
+ const mentionState = this.mentionEngine.getState();
1750
+ if (mentionState.active) {
1751
+ if (event.key === "ArrowUp") {
1752
+ event.preventDefault();
1753
+ this.mentionEngine.moveUp();
1754
+ } else if (event.key === "ArrowDown") {
1755
+ event.preventDefault();
1756
+ this.mentionEngine.moveDown();
1757
+ } else if (event.key === "Enter" && !event.shiftKey) {
1758
+ event.preventDefault();
1759
+ const triggerPosition = mentionState.triggerPosition;
1760
+ const selectionEnd = this.textareaEl.selectionEnd;
1761
+ const member = this.mentionEngine.selectCurrent();
1762
+ if (member) this.insertMention(member, triggerPosition, selectionEnd);
1763
+ } else if (event.key === "Escape") {
1764
+ event.preventDefault();
1765
+ this.mentionEngine.deactivate();
1766
+ }
1767
+ return;
1768
+ }
1769
+ if (this.config.enterToSend !== false && event.key === "Enter" && !event.shiftKey) {
1770
+ event.preventDefault();
1771
+ this.handleSend();
1772
+ }
1773
+ });
1774
+ this.textareaEl.addEventListener("paste", (event) => {
1775
+ const items = event.clipboardData?.items;
1776
+ if (!items) return;
1777
+ const files = [];
1778
+ for (let i = 0; i < items.length; i++) if (items[i].kind === "file") {
1779
+ const file = items[i].getAsFile();
1780
+ if (file) files.push(file);
1781
+ }
1782
+ if (files.length > 0) {
1783
+ event.preventDefault();
1784
+ this.emitter.emit("toolbar-paste", files);
1785
+ this.config.onPaste?.(files);
1786
+ }
1787
+ });
1788
+ }
1789
+ if (this.inputArea) {
1790
+ const wrapper = this.inputArea.querySelector(`.${this.prefix}-input-wrapper`);
1791
+ if (wrapper) this.setupDropEvents(wrapper);
1792
+ }
1793
+ this.sendBtn?.addEventListener("click", () => {
1794
+ this.handleSend();
1795
+ });
1796
+ this.emitter.on("mention-filter", (state) => {
1797
+ this.renderMentionPanel(state.filtered, state.activeIndex);
1798
+ });
1799
+ this.emitter.on("mention-active-change", (index) => {
1800
+ (this.mentionPanel?.querySelectorAll(`.${this.prefix}-mention-item`))?.forEach((el, i) => {
1801
+ el.classList.toggle(`${this.prefix}-mention-item--active`, i === index);
1802
+ el.setAttribute("aria-selected", String(i === index));
1803
+ });
1804
+ });
1805
+ this.emitter.on("mention-deactivate", () => {
1806
+ this.hideMentionPanel();
1807
+ });
1808
+ }
1809
+ setupDropEvents(wrapper) {
1810
+ let dragCounter = 0;
1811
+ wrapper.addEventListener("dragenter", (event) => {
1812
+ event.preventDefault();
1813
+ dragCounter++;
1814
+ if (this.dropOverlay) this.dropOverlay.style.display = "flex";
1815
+ });
1816
+ wrapper.addEventListener("dragleave", (event) => {
1817
+ event.preventDefault();
1818
+ dragCounter--;
1819
+ if (dragCounter <= 0) {
1820
+ dragCounter = 0;
1821
+ if (this.dropOverlay) this.dropOverlay.style.display = "none";
1822
+ }
1823
+ });
1824
+ wrapper.addEventListener("dragover", (event) => {
1825
+ event.preventDefault();
1826
+ });
1827
+ wrapper.addEventListener("drop", (event) => {
1828
+ event.preventDefault();
1829
+ dragCounter = 0;
1830
+ if (this.dropOverlay) this.dropOverlay.style.display = "none";
1831
+ const files = Array.from(event.dataTransfer?.files ?? []);
1832
+ if (files.length > 0) {
1833
+ this.emitter.emit("toolbar-drop", files);
1834
+ this.config.onDrop?.(files);
1835
+ }
1836
+ });
1837
+ }
1838
+ handleSend() {
1839
+ const content = (this.textareaEl?.value ?? "").trim();
1840
+ if (!content) return;
1841
+ const mentions = this.mentionEngine.parseMentions(content);
1842
+ this.emitter.emit("send", content, mentions);
1843
+ this.config.onSend?.(content, mentions);
1844
+ if (this.config.clearInputOnSend !== false) this.clearInput();
1845
+ this.mentionEngine.resetSelected();
1846
+ }
1847
+ insertMention(member, triggerPosition, selectionEnd) {
1848
+ if (!this.textareaEl) return;
1849
+ const state = this.mentionEngine.getState();
1850
+ const text = this.textareaEl.value;
1851
+ const end = selectionEnd ?? this.textareaEl.selectionEnd;
1852
+ const insertText = this.mentionEngine.getInsertText(member);
1853
+ const before = text.slice(0, triggerPosition ?? state.triggerPosition);
1854
+ const after = text.slice(end);
1855
+ this.textareaEl.value = this.limitInput(before + insertText + after);
1856
+ const newPos = Math.min(before.length + insertText.length, this.textareaEl.value.length);
1857
+ this.textareaEl.setSelectionRange(newPos, newPos);
1858
+ this.textareaEl.focus();
1859
+ this.autoResize();
1860
+ this.updateSendButtonState();
1861
+ this.config.onInputChange?.(this.textareaEl.value);
1862
+ }
1863
+ insertTextAtCursor(text) {
1864
+ if (!this.textareaEl) return;
1865
+ const current = this.textareaEl.value;
1866
+ const start = this.textareaEl.selectionStart;
1867
+ const end = this.textareaEl.selectionEnd;
1868
+ this.textareaEl.value = this.limitInput(current.slice(0, start) + text + current.slice(end));
1869
+ const nextCursor = Math.min(start + text.length, this.textareaEl.value.length);
1870
+ this.textareaEl.setSelectionRange(nextCursor, nextCursor);
1871
+ this.autoResize();
1872
+ this.updateSendButtonState();
1873
+ this.config.onInputChange?.(this.textareaEl.value);
1874
+ }
1875
+ autoResize() {
1876
+ if (!this.textareaEl) return;
1877
+ this.textareaEl.style.height = "auto";
1878
+ this.textareaEl.style.height = `${this.textareaEl.scrollHeight}px`;
1879
+ }
1880
+ updateSendButtonState() {
1881
+ if (!this.sendBtn) return;
1882
+ this.sendBtn.disabled = !this.getInputContent().trim();
1883
+ }
1884
+ renderLoadMoreButton() {
1885
+ this.loadMoreEl = this.el("div", `${this.prefix}-load-more`);
1886
+ this.loadMoreEl.textContent = this.config.loadMoreText ?? "加载更多";
1887
+ this.loadMoreEl.addEventListener("click", () => {
1888
+ if (this.destroyed || this.loadingMore) return;
1889
+ this.setLoadingMore(true);
1890
+ this.emitter.emit("load-more");
1891
+ this.loadMore();
1892
+ });
1893
+ this.messageListEl.appendChild(this.loadMoreEl);
1894
+ }
1895
+ async loadMore() {
1896
+ try {
1897
+ await this.config.onLoadMore?.();
1898
+ } catch (error) {
1899
+ this.emitter.emit("load-more-error", error);
1900
+ } finally {
1901
+ if (!this.destroyed) this.setLoadingMore(false);
1902
+ }
1903
+ }
1904
+ renderTimeDivider(timestamp) {
1905
+ this.messageListEl.appendChild(this.createTimeDivider(timestamp));
1906
+ }
1907
+ createTimeDivider(timestamp) {
1908
+ const divider = this.el("div", `${this.prefix}-time-divider`);
1909
+ const span = this.document.createElement("span");
1910
+ span.textContent = this.formatTime(timestamp);
1911
+ divider.appendChild(span);
1912
+ return divider;
1913
+ }
1914
+ renderMessageRow(message) {
1915
+ this.messageListEl.appendChild(this.createMessageRow(message));
1916
+ }
1917
+ createMessageRow(message) {
1918
+ const p = this.prefix;
1919
+ if (message.type === "system") {
1920
+ const row2 = this.el("div", cx(`${p}-message-row`, `${p}-message-row--system`));
1921
+ row2.dataset.msgId = message.id;
1922
+ row2.dataset.timestamp = String(message.timestamp);
1923
+ const system = this.el("div", `${p}-system-message`);
1924
+ system.textContent = message.content ?? "";
1925
+ row2.appendChild(system);
1926
+ return row2;
1927
+ }
1928
+ const isOutgoing = message.direction === "outgoing";
1929
+ const row = this.el("div", cx(`${p}-message-row`, isOutgoing && `${p}-message-row--outgoing`));
1930
+ row.dataset.msgId = message.id;
1931
+ row.dataset.timestamp = String(message.timestamp);
1932
+ if (this.config.showAvatar !== false) row.appendChild(this.createAvatar(message.sender.nickname, message.sender.avatar));
1933
+ const body = this.el("div", `${p}-message-body`);
1934
+ if (this.config.showNickname !== false && !isOutgoing) {
1935
+ const nickname = this.el("div", `${p}-nickname`);
1936
+ nickname.textContent = message.sender.nickname;
1937
+ body.appendChild(nickname);
1938
+ }
1939
+ body.appendChild(this.createBubble(message, isOutgoing));
1940
+ body.appendChild(this.createMessageFooter(message, isOutgoing));
1941
+ row.appendChild(body);
1942
+ row.addEventListener("click", () => {
1943
+ this.emitter.emit("message-click", message);
1944
+ this.config.onMessageClick?.(message);
1945
+ });
1946
+ return row;
1947
+ }
1948
+ createAvatar(name, avatarUrl) {
1949
+ const avatar = this.el("div", `${this.prefix}-avatar`);
1950
+ if (avatarUrl) {
1951
+ const img = this.document.createElement("img");
1952
+ img.src = this.safeResourceUrl(avatarUrl);
1953
+ img.alt = name;
1954
+ avatar.appendChild(img);
1955
+ } else avatar.textContent = name[0]?.toUpperCase() ?? "";
1956
+ return avatar;
1957
+ }
1958
+ createMessageFooter(message, isOutgoing) {
1959
+ const footer = this.el("div", `${this.prefix}-message-footer`);
1960
+ if (this.config.showTimestamp !== false) {
1961
+ const time = this.el("span", `${this.prefix}-timestamp`);
1962
+ time.textContent = this.formatShortTime(message.timestamp);
1963
+ footer.appendChild(time);
1964
+ }
1965
+ if (this.config.showStatus !== false && isOutgoing && message.status) {
1966
+ const status = this.el("span", cx(`${this.prefix}-status`, `${this.prefix}-status--${message.status}`));
1967
+ status.textContent = STATUS_MAP[message.status] ?? "";
1968
+ if (message.status === "failed") status.addEventListener("click", (event) => {
1969
+ event.stopPropagation();
1970
+ this.emitter.emit("message-retry", message);
1971
+ });
1972
+ footer.appendChild(status);
1973
+ }
1974
+ return footer;
1975
+ }
1976
+ createBubble(message, isOutgoing) {
1977
+ const p = this.prefix;
1978
+ const dirClass = isOutgoing ? `${p}-bubble--outgoing` : `${p}-bubble--incoming`;
1979
+ if (message.revoked) {
1980
+ const bubble = this.el("div", cx(`${p}-bubble`, dirClass, `${p}-bubble--revoked`));
1981
+ bubble.textContent = "消息已撤回";
1982
+ return bubble;
1983
+ }
1984
+ switch (message.type) {
1985
+ case "image": {
1986
+ const bubble = this.el("div", cx(`${p}-bubble`, dirClass, `${p}-bubble--image`));
1987
+ if (message.image) {
1988
+ const img = this.document.createElement("img");
1989
+ img.src = this.safeResourceUrl(message.image.thumbUrl ?? message.image.url);
1990
+ img.alt = message.content ?? "";
1991
+ if (message.image.width) img.width = Math.min(message.image.width, 240);
1992
+ bubble.appendChild(img);
1993
+ }
1994
+ return bubble;
1995
+ }
1996
+ case "file": {
1997
+ const bubble = this.el("div", cx(`${p}-bubble`, dirClass, `${p}-bubble--file`));
1998
+ const icon = this.el("div", `${p}-file-icon`);
1999
+ icon.innerHTML = FILE_ICON;
2000
+ bubble.appendChild(icon);
2001
+ const info = this.el("div", `${p}-file-info`);
2002
+ const name = this.el("div", `${p}-file-name`);
2003
+ name.textContent = message.file?.name ?? "file";
2004
+ info.appendChild(name);
2005
+ if (message.file?.size) {
2006
+ const size = this.el("div", `${p}-file-size`);
2007
+ size.textContent = this.formatFileSize(message.file.size);
2008
+ info.appendChild(size);
2009
+ }
2010
+ bubble.appendChild(info);
2011
+ return bubble;
2012
+ }
2013
+ case "custom": {
2014
+ const bubble = this.el("div", cx(`${p}-bubble`, dirClass));
2015
+ if (message.customHtml) this.setSanitizedCustomHtml(bubble, message.customHtml);
2016
+ else bubble.textContent = message.content ?? "";
2017
+ return bubble;
2018
+ }
2019
+ default: {
2020
+ const bubble = this.el("div", cx(`${p}-bubble`, dirClass, message.streaming && `${p}-bubble--streaming`));
2021
+ if (message.contentBlocks?.length) this.renderContentBlocks(bubble, message.contentBlocks);
2022
+ else bubble.innerHTML = this.renderTextContent(message.content ?? "", message.mentions);
2023
+ return bubble;
2024
+ }
2025
+ }
2026
+ }
2027
+ renderContentBlocks(target, blocks) {
2028
+ target.replaceChildren();
2029
+ for (const block of blocks) switch (block.type) {
2030
+ case "text":
2031
+ target.appendChild(this.document.createTextNode(block.text));
2032
+ break;
2033
+ case "mention": {
2034
+ const mention = this.el("span", `${this.prefix}-mention-tag`);
2035
+ mention.dataset.mentionId = block.id;
2036
+ mention.textContent = `@${block.label}`;
2037
+ target.appendChild(mention);
2038
+ break;
2039
+ }
2040
+ case "link": {
2041
+ const link = this.document.createElement("a");
2042
+ const safeUrl = this.safeUrl(block.url, [
2043
+ "http:",
2044
+ "https:",
2045
+ "mailto:"
2046
+ ]);
2047
+ link.className = `${this.prefix}-link`;
2048
+ link.textContent = block.label ?? block.url;
2049
+ if (safeUrl) {
2050
+ link.href = safeUrl;
2051
+ link.target = "_blank";
2052
+ link.rel = "noopener noreferrer";
2053
+ }
2054
+ target.appendChild(link);
2055
+ break;
2056
+ }
2057
+ case "code": {
2058
+ const code = this.document.createElement("code");
2059
+ code.textContent = block.code;
2060
+ if (block.language) code.dataset.language = block.language;
2061
+ target.appendChild(code);
2062
+ break;
2063
+ }
2064
+ case "image": {
2065
+ const image = this.document.createElement("img");
2066
+ const safeUrl = this.safeResourceUrl(block.src);
2067
+ const width = block.width;
2068
+ const height = block.height;
2069
+ if (safeUrl) image.src = safeUrl;
2070
+ image.alt = block.alt ?? "";
2071
+ image.loading = "lazy";
2072
+ if (Number.isFinite(width) && width > 0) image.width = width;
2073
+ if (Number.isFinite(height) && height > 0) image.height = height;
2074
+ target.appendChild(image);
2075
+ break;
2076
+ }
2077
+ case "file": {
2078
+ const file = this.document.createElement("a");
2079
+ const safeUrl = this.safeResourceUrl(block.url);
2080
+ file.className = `${this.prefix}-file-link`;
2081
+ file.textContent = block.name;
2082
+ if (safeUrl) {
2083
+ file.href = safeUrl;
2084
+ file.target = "_blank";
2085
+ file.rel = "noopener noreferrer";
2086
+ }
2087
+ target.appendChild(file);
2088
+ break;
2089
+ }
2090
+ case "break":
2091
+ target.appendChild(this.document.createElement("br"));
2092
+ break;
2093
+ case "custom":
2094
+ target.appendChild(this.document.createTextNode(block.fallback ?? `[${block.kind}]`));
2095
+ break;
2096
+ }
2097
+ }
2098
+ renderTextContent(content, mentions) {
2099
+ let html = this.escapeHtml(content);
2100
+ if (mentions?.length) for (const mention of mentions) {
2101
+ const tag = `<span class="${this.prefix}-mention-tag" data-mention-id="${this.escapeAttr(mention.id)}">@${this.escapeHtml(mention.name)}</span>`;
2102
+ html = html.replace(new RegExp(`@${this.escapeRegex(mention.name)}`, "g"), tag);
2103
+ }
2104
+ return html.replace(/(https?:\/\/[^\s<]+)/g, `<a class="${this.prefix}-link" href="$1" target="_blank" rel="noopener noreferrer">$1</a>`);
2105
+ }
2106
+ setSanitizedCustomHtml(target, html) {
2107
+ const template = this.document.createElement("template");
2108
+ template.innerHTML = html;
2109
+ const fragment = this.document.createDocumentFragment();
2110
+ for (const node of Array.from(template.content.childNodes)) {
2111
+ const sanitized = this.sanitizeCustomNode(node);
2112
+ if (sanitized) fragment.appendChild(sanitized);
2113
+ }
2114
+ target.replaceChildren(fragment);
2115
+ }
2116
+ sanitizeCustomNode(node) {
2117
+ if (node.nodeType === 3) return this.document.createTextNode(node.textContent ?? "");
2118
+ if (node.nodeType !== 1) return null;
2119
+ const source = node;
2120
+ const tagName = source.tagName.toLowerCase();
2121
+ if (DANGEROUS_CUSTOM_TAGS.has(tagName)) return null;
2122
+ if (!ALLOWED_CUSTOM_TAGS.has(tagName)) {
2123
+ const fragment = this.document.createDocumentFragment();
2124
+ for (const child of Array.from(source.childNodes)) {
2125
+ const sanitized = this.sanitizeCustomNode(child);
2126
+ if (sanitized) fragment.appendChild(sanitized);
2127
+ }
2128
+ return fragment;
2129
+ }
2130
+ const element = this.document.createElement(tagName);
2131
+ const title = source.getAttribute("title");
2132
+ if (title) element.setAttribute("title", title);
2133
+ const ariaLabel = source.getAttribute("aria-label");
2134
+ if (ariaLabel) element.setAttribute("aria-label", ariaLabel);
2135
+ if (tagName === "a") {
2136
+ const href = source.getAttribute("href");
2137
+ const safeHref = href ? this.safeUrl(href, [
2138
+ "http:",
2139
+ "https:",
2140
+ "mailto:"
2141
+ ]) : null;
2142
+ if (safeHref) element.setAttribute("href", safeHref);
2143
+ const target = source.getAttribute("target");
2144
+ if (target === "_blank" || target === "_self") element.setAttribute("target", target);
2145
+ element.setAttribute("rel", "noopener noreferrer");
2146
+ }
2147
+ if (tagName === "img") {
2148
+ const src = source.getAttribute("src");
2149
+ const safeSrc = src ? this.safeUrl(src, [
2150
+ "http:",
2151
+ "https:",
2152
+ "blob:"
2153
+ ]) : null;
2154
+ if (safeSrc) element.setAttribute("src", safeSrc);
2155
+ element.setAttribute("alt", source.getAttribute("alt") ?? "");
2156
+ for (const dimension of ["width", "height"]) {
2157
+ const value = Number(source.getAttribute(dimension));
2158
+ if (Number.isFinite(value) && value > 0) element.setAttribute(dimension, String(value));
2159
+ }
2160
+ element.setAttribute("loading", "lazy");
2161
+ }
2162
+ for (const child of Array.from(source.childNodes)) {
2163
+ const sanitized = this.sanitizeCustomNode(child);
2164
+ if (sanitized) element.appendChild(sanitized);
2165
+ }
2166
+ return element;
2167
+ }
2168
+ safeResourceUrl(value) {
2169
+ if (!value) return "";
2170
+ return this.safeUrl(value, [
2171
+ "http:",
2172
+ "https:",
2173
+ "blob:"
2174
+ ]) ?? "";
2175
+ }
2176
+ safeUrl(value, protocols) {
2177
+ const trimmed = value.trim();
2178
+ if (!trimmed || [...trimmed].some((character) => {
2179
+ const code = character.charCodeAt(0);
2180
+ return code < 32 || code === 127;
2181
+ })) return null;
2182
+ try {
2183
+ const resolved = new URL(trimmed, this.document.baseURI);
2184
+ return protocols.includes(resolved.protocol) ? trimmed : null;
2185
+ } catch {
2186
+ return null;
2187
+ }
2188
+ }
2189
+ renderEmpty() {
2190
+ if (this.messageListEl.querySelector(`.${this.prefix}-empty`)) return;
2191
+ const empty = this.el("div", `${this.prefix}-empty`);
2192
+ empty.textContent = "暂无消息";
2193
+ this.messageListEl.appendChild(empty);
2194
+ }
2195
+ removeEmpty() {
2196
+ this.messageListEl.querySelector(`.${this.prefix}-empty`)?.remove();
2197
+ }
2198
+ el(tag, className) {
2199
+ const el = this.document.createElement(tag);
2200
+ el.className = className;
2201
+ return el;
2202
+ }
2203
+ getMessageElements() {
2204
+ return Array.from(this.messageListEl.querySelectorAll(`.${this.prefix}-message-row`));
2205
+ }
2206
+ getMessageRow(id) {
2207
+ return this.getMessageElements().find((element) => element.dataset.msgId === id) ?? null;
2208
+ }
2209
+ refreshTimeDividers() {
2210
+ this.messageListEl.querySelectorAll(`.${this.prefix}-time-divider`).forEach((divider) => divider.remove());
2211
+ const interval = this.config.timeGroupInterval ?? 3e5;
2212
+ let previousTimestamp = 0;
2213
+ for (const row of this.getMessageElements()) {
2214
+ const timestamp = Number(row.dataset.timestamp ?? 0);
2215
+ if (this.config.showTimestamp !== false && timestamp - previousTimestamp > interval) row.before(this.createTimeDivider(timestamp));
2216
+ previousTimestamp = timestamp;
2217
+ }
2218
+ }
2219
+ formatTime(timestamp) {
2220
+ if (this.config.formatTime) return this.config.formatTime(timestamp);
2221
+ const date = new Date(timestamp);
2222
+ const now = /* @__PURE__ */ new Date();
2223
+ if (date.toDateString() === now.toDateString()) return date.toLocaleTimeString("zh-CN", {
2224
+ hour: "2-digit",
2225
+ minute: "2-digit"
2226
+ });
2227
+ return date.toLocaleDateString("zh-CN", {
2228
+ month: "2-digit",
2229
+ day: "2-digit",
2230
+ hour: "2-digit",
2231
+ minute: "2-digit"
2232
+ });
2233
+ }
2234
+ formatShortTime(timestamp) {
2235
+ return new Date(timestamp).toLocaleTimeString("zh-CN", {
2236
+ hour: "2-digit",
2237
+ minute: "2-digit"
2238
+ });
2239
+ }
2240
+ formatFileSize(bytes) {
2241
+ if (bytes < 1024) return `${bytes} B`;
2242
+ if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
2243
+ return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
2244
+ }
2245
+ limitInput(content) {
2246
+ const max = this.config.maxInputLength;
2247
+ if (!max || max <= 0) return content;
2248
+ return content.slice(0, max);
2249
+ }
2250
+ escapeHtml(str) {
2251
+ const div = this.document.createElement("div");
2252
+ div.textContent = str;
2253
+ return div.innerHTML;
2254
+ }
2255
+ escapeAttr(str) {
2256
+ return str.replace(/"/g, "&quot;").replace(/'/g, "&#39;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
2257
+ }
2258
+ escapeRegex(str) {
2259
+ return str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
2260
+ }
2261
+ };
2262
+ //#endregion
2263
+ //#region src/theme/tokens.ts
2264
+ const lightTokens = {
2265
+ bgPrimary: "var(--ld-bg-surface)",
2266
+ bgSecondary: "var(--ld-bg-surface-soft)",
2267
+ bgBubbleOutgoing: "var(--ld-primary-default)",
2268
+ bgBubbleIncoming: "var(--ld-bg-surface-soft)",
2269
+ bgInput: "var(--ld-bg-surface)",
2270
+ bgMention: "var(--ld-bg-surface)",
2271
+ bgMentionHover: "var(--ld-primary-50)",
2272
+ bgMentionActive: "var(--ld-primary-100)",
2273
+ bgLoadMore: "var(--ld-bg-surface-soft)",
2274
+ bgSystemMessage: "var(--ld-bg-surface-soft)",
2275
+ bgDropOverlay: "color-mix(in srgb, var(--ld-primary-default) 10%, var(--ld-bg-surface))",
2276
+ bgDangerSoft: "color-mix(in srgb, var(--ld-danger-default) 12%, var(--ld-bg-surface))",
2277
+ textPrimary: "var(--ld-text-primary)",
2278
+ textSecondary: "var(--ld-text-secondary)",
2279
+ textBubbleOutgoing: "var(--ld-gray-50)",
2280
+ textBubbleIncoming: "var(--ld-text-primary)",
2281
+ textMention: "var(--ld-text-primary)",
2282
+ textMentionTag: "var(--ld-primary-default)",
2283
+ textTimestamp: "var(--ld-text-muted)",
2284
+ textSystem: "var(--ld-text-secondary)",
2285
+ textPlaceholder: "var(--ld-text-muted)",
2286
+ borderPrimary: "var(--ld-border)",
2287
+ borderInput: "var(--ld-gray-300)",
2288
+ borderInputFocus: "var(--ld-primary-default)",
2289
+ borderMention: "var(--ld-border)",
2290
+ colorSending: "var(--ld-gray-400)",
2291
+ colorSent: "var(--ld-gray-500)",
2292
+ colorDelivered: "var(--ld-primary-default)",
2293
+ colorRead: "var(--ld-success-default)",
2294
+ colorFailed: "var(--ld-danger-default)",
2295
+ colorLink: "var(--ld-primary-default)",
2296
+ shadowPanel: "none",
2297
+ shadowMention: "none",
2298
+ avatarBg: "var(--ld-primary-default)",
2299
+ avatarText: "var(--ld-gray-50)",
2300
+ btnPrimaryBg: "var(--ld-primary-default)",
2301
+ btnPrimaryText: "var(--ld-gray-50)",
2302
+ btnPrimaryHover: "var(--ld-primary-hover)",
2303
+ scrollbarThumb: "var(--ld-gray-300)",
2304
+ scrollbarTrack: "var(--ld-gray-100)",
2305
+ bgToolbar: "var(--ld-bg-surface-soft)",
2306
+ borderToolbar: "var(--ld-border)",
2307
+ toolbarIconColor: "var(--ld-text-secondary)",
2308
+ toolbarIconHover: "var(--ld-primary-default)",
2309
+ toolbarVoiceActive: "var(--ld-danger-default)"
2310
+ };
2311
+ const darkTokens = {
2312
+ bgPrimary: "var(--ld-gray-900)",
2313
+ bgSecondary: "var(--ld-gray-800)",
2314
+ bgBubbleOutgoing: "var(--ld-primary-default)",
2315
+ bgBubbleIncoming: "var(--ld-gray-800)",
2316
+ bgInput: "var(--ld-gray-900)",
2317
+ bgMention: "var(--ld-gray-900)",
2318
+ bgMentionHover: "var(--ld-gray-800)",
2319
+ bgMentionActive: "var(--ld-primary-900)",
2320
+ bgLoadMore: "var(--ld-gray-800)",
2321
+ bgSystemMessage: "var(--ld-gray-800)",
2322
+ bgDropOverlay: "color-mix(in srgb, var(--ld-primary-default) 16%, var(--ld-gray-900))",
2323
+ bgDangerSoft: "color-mix(in srgb, var(--ld-danger-default) 18%, var(--ld-gray-900))",
2324
+ textPrimary: "var(--ld-gray-50)",
2325
+ textSecondary: "var(--ld-gray-300)",
2326
+ textBubbleOutgoing: "var(--ld-gray-50)",
2327
+ textBubbleIncoming: "var(--ld-gray-50)",
2328
+ textMention: "var(--ld-gray-50)",
2329
+ textMentionTag: "var(--ld-primary-300)",
2330
+ textTimestamp: "var(--ld-gray-400)",
2331
+ textSystem: "var(--ld-gray-300)",
2332
+ textPlaceholder: "var(--ld-gray-500)",
2333
+ borderPrimary: "var(--ld-gray-700)",
2334
+ borderInput: "var(--ld-gray-700)",
2335
+ borderInputFocus: "var(--ld-primary-default)",
2336
+ borderMention: "var(--ld-gray-700)",
2337
+ colorSending: "var(--ld-gray-500)",
2338
+ colorSent: "var(--ld-gray-400)",
2339
+ colorDelivered: "var(--ld-primary-300)",
2340
+ colorRead: "var(--ld-success-default)",
2341
+ colorFailed: "var(--ld-danger-default)",
2342
+ colorLink: "var(--ld-primary-300)",
2343
+ shadowPanel: "none",
2344
+ shadowMention: "none",
2345
+ avatarBg: "var(--ld-primary-default)",
2346
+ avatarText: "var(--ld-gray-50)",
2347
+ btnPrimaryBg: "var(--ld-primary-default)",
2348
+ btnPrimaryText: "var(--ld-gray-50)",
2349
+ btnPrimaryHover: "var(--ld-primary-hover)",
2350
+ scrollbarThumb: "var(--ld-gray-700)",
2351
+ scrollbarTrack: "var(--ld-gray-800)",
2352
+ bgToolbar: "var(--ld-gray-800)",
2353
+ borderToolbar: "var(--ld-gray-700)",
2354
+ toolbarIconColor: "var(--ld-gray-300)",
2355
+ toolbarIconHover: "var(--ld-primary-300)",
2356
+ toolbarVoiceActive: "var(--ld-danger-default)"
2357
+ };
2358
+ //#endregion
2359
+ //#region src/theme/theme-manager.ts
2360
+ var ThemeManager = class {
2361
+ constructor(emitter, mode, view) {
2362
+ this.mode = "light";
2363
+ this.mediaQuery = null;
2364
+ this.mediaHandler = null;
2365
+ this.destroyed = false;
2366
+ this.emitter = emitter;
2367
+ this.view = view === void 0 ? typeof window === "undefined" ? null : window : view;
2368
+ if (mode) this.setTheme(mode);
2369
+ }
2370
+ /** 获取当前模式 */
2371
+ getMode() {
2372
+ return this.mode;
2373
+ }
2374
+ /** 获取当前实际主题(auto 时解析为 light/dark) */
2375
+ getResolvedTheme() {
2376
+ if (this.mode === "auto") return this.getSystemTheme();
2377
+ return this.mode;
2378
+ }
2379
+ /** 获取当前主题令牌 */
2380
+ getTokens() {
2381
+ return this.getResolvedTheme() === "dark" ? darkTokens : lightTokens;
2382
+ }
2383
+ /** 设置主题 */
2384
+ setTheme(mode) {
2385
+ if (this.destroyed) return;
2386
+ this.cleanupMediaListener();
2387
+ this.mode = mode;
2388
+ if (mode === "auto") this.setupMediaListener();
2389
+ this.emitter.emit("theme-change", this.getResolvedTheme(), this.getTokens());
2390
+ }
2391
+ /** 获取系统主题偏好 */
2392
+ getSystemTheme() {
2393
+ const matchMedia = this.view?.matchMedia;
2394
+ if (!matchMedia) return "light";
2395
+ return matchMedia.call(this.view, "(prefers-color-scheme: dark)").matches ? "dark" : "light";
2396
+ }
2397
+ /** 监听系统主题变化 */
2398
+ setupMediaListener() {
2399
+ if (!this.view?.matchMedia) return;
2400
+ this.mediaQuery = this.view.matchMedia("(prefers-color-scheme: dark)");
2401
+ this.mediaHandler = () => {
2402
+ this.emitter.emit("theme-change", this.getResolvedTheme(), this.getTokens());
2403
+ };
2404
+ const query = this.mediaQuery;
2405
+ if (typeof query.addEventListener === "function") query.addEventListener("change", this.mediaHandler);
2406
+ else query.addListener?.(this.mediaHandler);
2407
+ }
2408
+ /** 清理监听器 */
2409
+ cleanupMediaListener() {
2410
+ if (this.mediaQuery && this.mediaHandler) {
2411
+ const query = this.mediaQuery;
2412
+ if (typeof query.removeEventListener === "function") query.removeEventListener("change", this.mediaHandler);
2413
+ else query.removeListener?.(this.mediaHandler);
2414
+ this.mediaQuery = null;
2415
+ this.mediaHandler = null;
2416
+ }
2417
+ }
2418
+ /** 销毁 */
2419
+ destroy() {
2420
+ if (this.destroyed) return;
2421
+ this.destroyed = true;
2422
+ this.cleanupMediaListener();
2423
+ }
2424
+ };
2425
+ //#endregion
2426
+ //#region src/core/outbox.ts
2427
+ var MemoryChatOutboxStorage = class {
2428
+ constructor() {
2429
+ this.entries = [];
2430
+ }
2431
+ load() {
2432
+ return cloneEntries(this.entries);
2433
+ }
2434
+ save(entries) {
2435
+ this.entries = cloneEntries(entries);
2436
+ }
2437
+ };
2438
+ var ChatOutbox = class {
2439
+ constructor(options = {}) {
2440
+ this.entries = [];
2441
+ this.hydrated = false;
2442
+ this.writePromise = Promise.resolve();
2443
+ this.storage = options.storage ?? new MemoryChatOutboxStorage();
2444
+ this.now = options.now ?? Date.now;
2445
+ }
2446
+ async hydrate() {
2447
+ if (!this.hydrated) {
2448
+ this.entries = (await this.storage.load()).map(normalizeEntry);
2449
+ this.hydrated = true;
2450
+ }
2451
+ return this.list();
2452
+ }
2453
+ list() {
2454
+ return cloneEntries(this.entries);
2455
+ }
2456
+ async enqueue(message) {
2457
+ await this.hydrate();
2458
+ const normalized = normalizeChatMessage({
2459
+ ...message,
2460
+ clientMessageId: message.clientMessageId ?? message.id
2461
+ });
2462
+ const key = getOutboxKey(normalized);
2463
+ const existing = this.entries.find((entry2) => entry2.id === key);
2464
+ if (existing) return cloneEntry(existing);
2465
+ const entry = {
2466
+ id: key,
2467
+ message: normalized,
2468
+ enqueuedAt: this.now(),
2469
+ attempts: 0,
2470
+ state: "pending"
2471
+ };
2472
+ this.entries.push(entry);
2473
+ await this.persist();
2474
+ return cloneEntry(entry);
2475
+ }
2476
+ async remove(id) {
2477
+ await this.hydrate();
2478
+ const index = this.entries.findIndex((entry) => entry.id === id);
2479
+ if (index < 0) return false;
2480
+ this.entries.splice(index, 1);
2481
+ await this.persist();
2482
+ return true;
2483
+ }
2484
+ async retry(id) {
2485
+ await this.hydrate();
2486
+ for (const entry of this.entries) if (!id || entry.id === id) {
2487
+ entry.state = "pending";
2488
+ entry.lastError = void 0;
2489
+ }
2490
+ await this.persist();
2491
+ }
2492
+ async clear() {
2493
+ await this.hydrate();
2494
+ this.entries = [];
2495
+ await this.persist();
2496
+ }
2497
+ async flush(send, options = {}) {
2498
+ await this.hydrate();
2499
+ const maxAttempts = Math.max(1, Math.floor(options.maxAttempts ?? 3));
2500
+ const concurrency = Math.min(8, Math.max(1, Math.floor(options.concurrency ?? 1)));
2501
+ const candidates = this.entries.filter((entry) => entry.state === "pending");
2502
+ const sent = [];
2503
+ const failed = [];
2504
+ let cursor = 0;
2505
+ const worker = async () => {
2506
+ while (cursor < candidates.length) {
2507
+ if (options.signal?.aborted) throw createAbortError();
2508
+ const entry = candidates[cursor++];
2509
+ entry.attempts += 1;
2510
+ try {
2511
+ await send(entry.message, {
2512
+ signal: options.signal,
2513
+ clientMessageId: entry.message.clientMessageId ?? entry.id
2514
+ });
2515
+ const index = this.entries.findIndex((item) => item.id === entry.id);
2516
+ if (index >= 0) this.entries.splice(index, 1);
2517
+ sent.push(entry.id);
2518
+ } catch (error) {
2519
+ entry.lastError = toErrorMessage(error);
2520
+ if (entry.attempts >= maxAttempts) entry.state = "failed";
2521
+ failed.push({
2522
+ id: entry.id,
2523
+ error
2524
+ });
2525
+ }
2526
+ await this.persist();
2527
+ }
2528
+ };
2529
+ await Promise.all(Array.from({ length: concurrency }, () => worker()));
2530
+ return {
2531
+ sent,
2532
+ failed,
2533
+ remaining: this.list()
2534
+ };
2535
+ }
2536
+ async persist() {
2537
+ const snapshot = cloneEntries(this.entries);
2538
+ const previous = this.writePromise;
2539
+ this.writePromise = previous.then(() => this.storage.save(snapshot));
2540
+ await this.writePromise;
2541
+ }
2542
+ };
2543
+ function getOutboxKey(message) {
2544
+ return message.clientMessageId ?? message.id;
2545
+ }
2546
+ function cloneEntry(entry) {
2547
+ return {
2548
+ ...entry,
2549
+ message: cloneChatMessage(entry.message)
2550
+ };
1721
2551
  }
1722
- function toKebab(key) {
1723
- return key.replace(/[A-Z]/g, (match) => `-${match.toLowerCase()}`);
2552
+ function cloneEntries(entries) {
2553
+ return entries.map(cloneEntry);
2554
+ }
2555
+ function normalizeEntry(entry) {
2556
+ const message = normalizeChatMessage(entry.message);
2557
+ return {
2558
+ id: typeof entry.id === "string" && entry.id ? entry.id : getOutboxKey(message),
2559
+ message,
2560
+ enqueuedAt: Number.isFinite(entry.enqueuedAt) ? entry.enqueuedAt : Date.now(),
2561
+ attempts: Number.isFinite(entry.attempts) ? Math.max(0, Math.floor(entry.attempts)) : 0,
2562
+ state: entry.state === "failed" ? "failed" : "pending",
2563
+ ...typeof entry.lastError === "string" && { lastError: entry.lastError }
2564
+ };
2565
+ }
2566
+ function toErrorMessage(error) {
2567
+ return error instanceof Error ? error.message : String(error);
2568
+ }
2569
+ function createAbortError() {
2570
+ return new DOMException("The operation was aborted.", "AbortError");
1724
2571
  }
1725
2572
  //#endregion
1726
2573
  //#region src/index.ts
1727
2574
  const version = "0.1.0";
1728
2575
  function createChat(config) {
1729
- const container = typeof config.container === "string" ? document.querySelector(config.container) : config.container;
2576
+ let container;
2577
+ if (typeof config.container === "string") {
2578
+ if (typeof document === "undefined") throw new Error("[Chat] A browser Document is required to resolve a container selector.");
2579
+ container = document.querySelector(config.container);
2580
+ } else container = config.container;
1730
2581
  if (!container) throw new Error(`[Chat] Container not found: ${config.container}`);
2582
+ const ownerDocument = container.ownerDocument;
1731
2583
  const prefix = normalizeClassPrefix(config.classPrefix ?? "ldesign-chat");
1732
2584
  const normalizedConfig = {
1733
2585
  ...config,
@@ -1736,23 +2588,70 @@ ${sizeVars()}
1736
2588
  const emitter = new EventEmitter();
1737
2589
  const chatManager = new ChatManager(emitter, normalizedConfig.messages);
1738
2590
  const mentionEngine = new MentionEngine(emitter, normalizedConfig.mentionMembers);
1739
- const themeManager = new ThemeManager(emitter, normalizedConfig.theme ?? "light");
2591
+ const themeManager = new ThemeManager(emitter, normalizedConfig.theme ?? "light", ownerDocument.defaultView);
1740
2592
  const renderer = new DOMRenderer(container, normalizedConfig, emitter, mentionEngine);
1741
- injectStyles(themeManager.getTokens(), prefix);
2593
+ let destroyed = false;
2594
+ const schedule = (callback) => {
2595
+ const run = () => {
2596
+ if (!destroyed) callback();
2597
+ };
2598
+ if (ownerDocument.defaultView?.requestAnimationFrame) ownerDocument.defaultView.requestAnimationFrame(run);
2599
+ else setTimeout(run, 0);
2600
+ };
1742
2601
  renderer.init();
1743
2602
  renderer.renderMessages(chatManager.getMessages());
1744
- requestAnimationFrame(() => {
2603
+ renderer.setThemeTokens(themeManager.getTokens());
2604
+ injectStyles(themeManager.getTokens(), prefix, renderer.getRootElement());
2605
+ const applyTransportAck = async (ack) => {
2606
+ const localId = chatManager.getMessageIdByClientMessageId(ack.clientMessageId);
2607
+ if (localId) chatManager.updateMessage(localId, {
2608
+ ...ack.message ?? {},
2609
+ id: localId,
2610
+ clientMessageId: ack.clientMessageId,
2611
+ status: "sent",
2612
+ ...ack.sequence !== void 0 && { sequence: ack.sequence },
2613
+ ...ack.cursor !== void 0 && { cursor: ack.cursor }
2614
+ });
2615
+ if (normalizedConfig.outbox) await normalizedConfig.outbox.remove(ack.clientMessageId);
2616
+ emitter.emit("transport-ack", ack);
2617
+ };
2618
+ const handleTransportEvent = (event) => {
2619
+ if (event.type === "message") chatManager.upsertMessage({
2620
+ ...event.message,
2621
+ ...event.cursor !== void 0 && { cursor: event.cursor }
2622
+ }, {
2623
+ sequence: event.sequence,
2624
+ source: "remote"
2625
+ });
2626
+ else if (event.type === "ack") applyTransportAck(event.ack).catch((error) => {
2627
+ emitter.emit("transport-error", error);
2628
+ });
2629
+ else if (event.type === "cursor") emitter.emit("transport-cursor", event.cursor);
2630
+ else emitter.emit("transport-error", event.error, event.recoverable);
2631
+ };
2632
+ const transportController = normalizedConfig.transport ? new ChatTransportController(normalizedConfig.transport, {
2633
+ conversationId: normalizedConfig.conversationId,
2634
+ cursor: normalizedConfig.transportCursor,
2635
+ onEvent: handleTransportEvent,
2636
+ onStatusChange: (status) => {
2637
+ emitter.emit("transport-status", status);
2638
+ }
2639
+ }) : null;
2640
+ schedule(() => {
1745
2641
  renderer.scrollToBottom(false);
1746
2642
  });
1747
2643
  emitter.on("theme-change", (_resolved, tokens) => {
1748
- updateStyles(tokens, prefix);
2644
+ renderer.setThemeTokens(tokens);
1749
2645
  });
1750
2646
  emitter.on("message-added", (message) => {
1751
2647
  renderer.appendMessage(message);
1752
- requestAnimationFrame(() => {
2648
+ schedule(() => {
1753
2649
  renderer.scrollToBottom(true);
1754
2650
  });
1755
2651
  });
2652
+ emitter.on("mention-select", (member) => {
2653
+ normalizedConfig.onMentionSelect?.(member);
2654
+ });
1756
2655
  emitter.on("messages-prepended", (messages) => {
1757
2656
  renderer.prependMessages(messages);
1758
2657
  });
@@ -1764,101 +2663,185 @@ ${sizeVars()}
1764
2663
  });
1765
2664
  emitter.on("message-content-appended", (id, content) => {
1766
2665
  renderer.updateMessageContent(id, content);
1767
- requestAnimationFrame(() => {
2666
+ schedule(() => {
1768
2667
  renderer.scrollToBottom(false);
1769
2668
  });
1770
2669
  });
1771
- return {
2670
+ const instance = {
1772
2671
  addMessage(message) {
1773
- chatManager.addMessage(message);
2672
+ if (!destroyed) chatManager.addMessage(message);
1774
2673
  },
1775
2674
  prependMessages(messages) {
1776
- chatManager.prependMessages(messages);
2675
+ if (!destroyed) chatManager.prependMessages(messages);
1777
2676
  },
1778
2677
  updateMessage(id, updates) {
1779
- chatManager.updateMessage(id, updates);
2678
+ if (!destroyed) chatManager.updateMessage(id, updates);
1780
2679
  },
1781
2680
  removeMessage(id) {
1782
- chatManager.removeMessage(id);
2681
+ if (!destroyed) chatManager.removeMessage(id);
1783
2682
  },
1784
2683
  setMessages(messages) {
2684
+ if (destroyed) return;
1785
2685
  chatManager.setMessages(messages);
1786
2686
  renderer.renderMessages(chatManager.getMessages());
1787
- requestAnimationFrame(() => {
2687
+ schedule(() => {
1788
2688
  renderer.scrollToBottom(false);
1789
2689
  });
1790
2690
  },
1791
2691
  clearMessages() {
1792
- chatManager.clearMessages();
1793
- renderer.renderMessages([]);
2692
+ if (!destroyed) {
2693
+ chatManager.clearMessages();
2694
+ renderer.renderMessages([]);
2695
+ }
1794
2696
  },
1795
2697
  scrollToBottom(smooth = true) {
1796
- renderer.scrollToBottom(smooth);
2698
+ if (!destroyed) renderer.scrollToBottom(smooth);
1797
2699
  },
1798
2700
  setTheme(theme) {
1799
- themeManager.setTheme(theme);
2701
+ if (!destroyed) themeManager.setTheme(theme);
1800
2702
  },
1801
2703
  setMentionMembers(members) {
1802
- mentionEngine.setMembers(members);
2704
+ if (!destroyed) mentionEngine.setMembers(members);
1803
2705
  },
1804
2706
  setLoadingMore(loading) {
1805
- renderer.setLoadingMore(loading);
2707
+ if (!destroyed) renderer.setLoadingMore(loading);
1806
2708
  },
1807
2709
  focusInput() {
1808
- renderer.focusInput();
2710
+ if (!destroyed) renderer.focusInput();
1809
2711
  },
1810
2712
  setInputContent(content) {
1811
- renderer.setInputContent(content);
2713
+ if (!destroyed) renderer.setInputContent(content);
1812
2714
  },
1813
2715
  getInputContent() {
1814
- return renderer.getInputContent();
2716
+ return destroyed ? "" : renderer.getInputContent();
1815
2717
  },
1816
2718
  clearInput() {
1817
- renderer.clearInput();
2719
+ if (!destroyed) renderer.clearInput();
1818
2720
  },
1819
2721
  isAtBottom(threshold) {
1820
- return renderer.isAtBottom(threshold);
2722
+ return destroyed || renderer.isAtBottom(threshold);
1821
2723
  },
1822
2724
  on(event, handler) {
1823
- return emitter.on(event, handler);
2725
+ return destroyed ? () => {} : emitter.on(event, handler);
1824
2726
  },
1825
2727
  getMessages() {
1826
- return chatManager.getMessages();
2728
+ return destroyed ? [] : chatManager.getMessages();
1827
2729
  },
1828
2730
  getMessage(id) {
1829
- return chatManager.getMessage(id);
2731
+ return destroyed ? void 0 : chatManager.getMessage(id);
1830
2732
  },
1831
2733
  appendMessageContent(id, chunk) {
1832
- chatManager.appendMessageContent(id, chunk);
2734
+ return destroyed ? false : chatManager.appendMessageContent(id, chunk);
2735
+ },
2736
+ finishStreaming(id, generation) {
2737
+ return destroyed ? false : chatManager.finishStreaming(id, generation);
2738
+ },
2739
+ beginStreaming(id, generation) {
2740
+ return destroyed ? null : chatManager.beginStreaming(id, generation);
2741
+ },
2742
+ cancelStreaming(id, generation) {
2743
+ return destroyed ? false : chatManager.cancelStreaming(id, generation);
2744
+ },
2745
+ mergeMessage(message, sequence) {
2746
+ return destroyed ? "ignored" : chatManager.upsertMessage(message, {
2747
+ sequence,
2748
+ source: "remote"
2749
+ });
2750
+ },
2751
+ async connectTransport(signal) {
2752
+ if (!transportController) throw new Error("No chat transport is configured.");
2753
+ await transportController.connect(signal);
2754
+ },
2755
+ async disconnectTransport() {
2756
+ await transportController?.disconnect();
1833
2757
  },
1834
- finishStreaming(id) {
1835
- chatManager.finishStreaming(id);
2758
+ getTransportStatus() {
2759
+ return transportController?.getStatus() ?? "idle";
2760
+ },
2761
+ async sendMessage(message, options = {}) {
2762
+ if (!transportController) throw new Error("No chat transport is configured.");
2763
+ if (destroyed) throw new Error("Chat instance is destroyed.");
2764
+ const prepared = normalizeChatMessage({
2765
+ ...message,
2766
+ clientMessageId: message.clientMessageId ?? createClientMessageId(message.id),
2767
+ status: message.status ?? "sending"
2768
+ });
2769
+ chatManager.addMessage(prepared);
2770
+ try {
2771
+ if (normalizedConfig.outbox) await normalizedConfig.outbox.enqueue(prepared);
2772
+ const ack = await transportController.send(prepared, {
2773
+ signal: options.signal,
2774
+ clientMessageId: prepared.clientMessageId
2775
+ });
2776
+ if (ack) await applyTransportAck(ack);
2777
+ else chatManager.markMessageStatus(prepared.id, "sent");
2778
+ emitter.emit("message-sent", prepared, ack);
2779
+ return ack;
2780
+ } catch (error) {
2781
+ chatManager.markMessageStatus(prepared.id, "failed");
2782
+ emitter.emit("message-send-failed", prepared, error);
2783
+ throw error;
2784
+ }
2785
+ },
2786
+ async flushOutbox(options = {}) {
2787
+ if (!normalizedConfig.outbox) return {
2788
+ sent: [],
2789
+ failed: [],
2790
+ remaining: []
2791
+ };
2792
+ if (!transportController) throw new Error("No chat transport is configured.");
2793
+ return normalizedConfig.outbox.flush(async (message, sendOptions) => {
2794
+ const ack = await transportController.send(message, sendOptions);
2795
+ if (ack) await applyTransportAck(ack);
2796
+ else if (message.id) chatManager.markMessageStatus(message.id, "sent");
2797
+ }, options);
1836
2798
  },
1837
2799
  destroy() {
2800
+ if (destroyed) return;
2801
+ destroyed = true;
2802
+ transportController?.dispose();
1838
2803
  themeManager.destroy();
1839
2804
  renderer.destroy();
1840
2805
  emitter.removeAll();
1841
- removeStyles(prefix);
2806
+ removeStyles(prefix, ownerDocument);
1842
2807
  }
1843
2808
  };
2809
+ if (normalizedConfig.autoConnect && transportController) instance.connectTransport().catch((error) => {
2810
+ emitter.emit("transport-error", error);
2811
+ });
2812
+ return instance;
2813
+ }
2814
+ function createClientMessageId(messageId) {
2815
+ const cryptoObject = globalThis.crypto;
2816
+ if (cryptoObject?.randomUUID) return cryptoObject.randomUUID();
2817
+ return `client-${messageId}-${Date.now()}-${Math.random().toString(36).slice(2)}`;
1844
2818
  }
1845
2819
  //#endregion
2820
+ exports.CHAT_MESSAGE_SCHEMA_VERSION = CHAT_MESSAGE_SCHEMA_VERSION;
1846
2821
  exports.COMPONENT_CSS_VAR_PREFIX = COMPONENT_CSS_VAR_PREFIX;
1847
2822
  exports.ChatManager = ChatManager;
2823
+ exports.ChatOutbox = ChatOutbox;
2824
+ exports.ChatTransportController = ChatTransportController;
1848
2825
  exports.DEFAULT_CLASS_PREFIX = DEFAULT_CLASS_PREFIX;
1849
2826
  exports.DOMRenderer = DOMRenderer;
1850
2827
  exports.EventEmitter = EventEmitter;
2828
+ exports.MemoryChatOutboxStorage = MemoryChatOutboxStorage;
1851
2829
  exports.MentionEngine = MentionEngine;
1852
2830
  exports.STYLE_ELEMENT_ID = STYLE_ELEMENT_ID;
1853
2831
  exports.ThemeManager = ThemeManager;
2832
+ exports.cloneChatMessage = cloneChatMessage;
1854
2833
  exports.cls = cls;
1855
2834
  exports.clsx = clsx;
2835
+ exports.compareChatMessages = compareChatMessages;
1856
2836
  exports.createChat = createChat;
1857
2837
  exports.cx = cx;
1858
2838
  exports.darkTokens = darkTokens;
1859
2839
  exports.getStyleElementId = getStyleElementId;
1860
2840
  exports.injectStyles = injectStyles;
1861
2841
  exports.lightTokens = lightTokens;
2842
+ exports.normalizeChatContentBlocks = normalizeChatContentBlocks;
2843
+ exports.normalizeChatMessage = normalizeChatMessage;
2844
+ exports.normalizeChatMessages = normalizeChatMessages;
1862
2845
  exports.normalizeClassPrefix = normalizeClassPrefix;
1863
2846
  exports.removeStyles = removeStyles;
1864
2847
  exports.updateStyles = updateStyles;