@composy/chat-core 0.0.1

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 (123) hide show
  1. package/README.md +123 -0
  2. package/dist/index.js +1868 -0
  3. package/dist/index.js.map +1 -0
  4. package/dist/index.min.js +1868 -0
  5. package/es/constants/dom.d.ts +41 -0
  6. package/es/constants/dom.d.ts.map +1 -0
  7. package/es/constants/dom.js +30 -0
  8. package/es/constants/dom.js.map +1 -0
  9. package/es/core/chat-manager.d.ts +34 -0
  10. package/es/core/chat-manager.d.ts.map +1 -0
  11. package/es/core/chat-manager.js +93 -0
  12. package/es/core/chat-manager.js.map +1 -0
  13. package/es/core/event-emitter.d.ts +48 -0
  14. package/es/core/event-emitter.d.ts.map +1 -0
  15. package/es/core/event-emitter.js +75 -0
  16. package/es/core/event-emitter.js.map +1 -0
  17. package/es/core/mention-engine.d.ts +41 -0
  18. package/es/core/mention-engine.d.ts.map +1 -0
  19. package/es/core/mention-engine.js +117 -0
  20. package/es/core/mention-engine.js.map +1 -0
  21. package/es/index.d.ts +28 -0
  22. package/es/index.d.ts.map +1 -0
  23. package/es/index.js +140 -0
  24. package/es/index.js.map +1 -0
  25. package/es/renderer/dom-renderer.d.ts +79 -0
  26. package/es/renderer/dom-renderer.d.ts.map +1 -0
  27. package/es/renderer/dom-renderer.js +677 -0
  28. package/es/renderer/dom-renderer.js.map +1 -0
  29. package/es/renderer/style-manager.d.ts +58 -0
  30. package/es/renderer/style-manager.d.ts.map +1 -0
  31. package/es/renderer/style-manager.js +641 -0
  32. package/es/renderer/style-manager.js.map +1 -0
  33. package/es/theme/theme-manager.d.ts +33 -0
  34. package/es/theme/theme-manager.d.ts.map +1 -0
  35. package/es/theme/theme-manager.js +70 -0
  36. package/es/theme/theme-manager.js.map +1 -0
  37. package/es/theme/tokens.d.ts +57 -0
  38. package/es/theme/tokens.d.ts.map +1 -0
  39. package/es/theme/tokens.js +108 -0
  40. package/es/theme/tokens.js.map +1 -0
  41. package/es/types/index.d.ts +204 -0
  42. package/es/types/index.d.ts.map +1 -0
  43. package/es/types/index.js +10 -0
  44. package/esm/constants/dom.d.ts +41 -0
  45. package/esm/constants/dom.d.ts.map +1 -0
  46. package/esm/constants/dom.js +30 -0
  47. package/esm/constants/dom.js.map +1 -0
  48. package/esm/core/chat-manager.d.ts +34 -0
  49. package/esm/core/chat-manager.d.ts.map +1 -0
  50. package/esm/core/chat-manager.js +93 -0
  51. package/esm/core/chat-manager.js.map +1 -0
  52. package/esm/core/event-emitter.d.ts +48 -0
  53. package/esm/core/event-emitter.d.ts.map +1 -0
  54. package/esm/core/event-emitter.js +75 -0
  55. package/esm/core/event-emitter.js.map +1 -0
  56. package/esm/core/mention-engine.d.ts +41 -0
  57. package/esm/core/mention-engine.d.ts.map +1 -0
  58. package/esm/core/mention-engine.js +117 -0
  59. package/esm/core/mention-engine.js.map +1 -0
  60. package/esm/index.d.ts +28 -0
  61. package/esm/index.d.ts.map +1 -0
  62. package/esm/index.js +140 -0
  63. package/esm/index.js.map +1 -0
  64. package/esm/renderer/dom-renderer.d.ts +79 -0
  65. package/esm/renderer/dom-renderer.d.ts.map +1 -0
  66. package/esm/renderer/dom-renderer.js +677 -0
  67. package/esm/renderer/dom-renderer.js.map +1 -0
  68. package/esm/renderer/style-manager.d.ts +58 -0
  69. package/esm/renderer/style-manager.d.ts.map +1 -0
  70. package/esm/renderer/style-manager.js +641 -0
  71. package/esm/renderer/style-manager.js.map +1 -0
  72. package/esm/theme/theme-manager.d.ts +33 -0
  73. package/esm/theme/theme-manager.d.ts.map +1 -0
  74. package/esm/theme/theme-manager.js +70 -0
  75. package/esm/theme/theme-manager.js.map +1 -0
  76. package/esm/theme/tokens.d.ts +57 -0
  77. package/esm/theme/tokens.d.ts.map +1 -0
  78. package/esm/theme/tokens.js +108 -0
  79. package/esm/theme/tokens.js.map +1 -0
  80. package/esm/types/index.d.ts +204 -0
  81. package/esm/types/index.d.ts.map +1 -0
  82. package/esm/types/index.js +10 -0
  83. package/lib/constants/dom.cjs +36 -0
  84. package/lib/constants/dom.cjs.map +1 -0
  85. package/lib/constants/dom.d.ts +41 -0
  86. package/lib/constants/dom.d.ts.map +1 -0
  87. package/lib/core/chat-manager.cjs +94 -0
  88. package/lib/core/chat-manager.cjs.map +1 -0
  89. package/lib/core/chat-manager.d.ts +34 -0
  90. package/lib/core/chat-manager.d.ts.map +1 -0
  91. package/lib/core/event-emitter.cjs +76 -0
  92. package/lib/core/event-emitter.cjs.map +1 -0
  93. package/lib/core/event-emitter.d.ts +48 -0
  94. package/lib/core/event-emitter.d.ts.map +1 -0
  95. package/lib/core/mention-engine.cjs +118 -0
  96. package/lib/core/mention-engine.cjs.map +1 -0
  97. package/lib/core/mention-engine.d.ts +41 -0
  98. package/lib/core/mention-engine.d.ts.map +1 -0
  99. package/lib/index.cjs +160 -0
  100. package/lib/index.cjs.map +1 -0
  101. package/lib/index.d.ts +28 -0
  102. package/lib/index.d.ts.map +1 -0
  103. package/lib/package.json +3 -0
  104. package/lib/renderer/dom-renderer.cjs +678 -0
  105. package/lib/renderer/dom-renderer.cjs.map +1 -0
  106. package/lib/renderer/dom-renderer.d.ts +79 -0
  107. package/lib/renderer/dom-renderer.d.ts.map +1 -0
  108. package/lib/renderer/style-manager.cjs +646 -0
  109. package/lib/renderer/style-manager.cjs.map +1 -0
  110. package/lib/renderer/style-manager.d.ts +58 -0
  111. package/lib/renderer/style-manager.d.ts.map +1 -0
  112. package/lib/theme/theme-manager.cjs +71 -0
  113. package/lib/theme/theme-manager.cjs.map +1 -0
  114. package/lib/theme/theme-manager.d.ts +33 -0
  115. package/lib/theme/theme-manager.d.ts.map +1 -0
  116. package/lib/theme/tokens.cjs +110 -0
  117. package/lib/theme/tokens.cjs.map +1 -0
  118. package/lib/theme/tokens.d.ts +57 -0
  119. package/lib/theme/tokens.d.ts.map +1 -0
  120. package/lib/types/index.cjs +11 -0
  121. package/lib/types/index.d.ts +204 -0
  122. package/lib/types/index.d.ts.map +1 -0
  123. package/package.json +107 -0
package/dist/index.js ADDED
@@ -0,0 +1,1868 @@
1
+ /*!
2
+ * ***********************************
3
+ * @composy/chat-core v0.0.1 *
4
+ * Built with rollup *
5
+ * Build time: 2024-08-17 11:30:32 *
6
+ * Build mode: production *
7
+ * Minified: No *
8
+ * ***********************************
9
+ */
10
+ (function(global, factory) {
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
+ })(this, function(exports) {
13
+ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
14
+ //#region src/core/event-emitter.ts
15
+ var EventEmitter = class {
16
+ constructor() {
17
+ this.listeners = /* @__PURE__ */ new Map();
18
+ }
19
+ /**
20
+ * 注册事件监听器
21
+ * @param event - 要监听的事件名称
22
+ * @param handler - 事件触发时执行的回调函数
23
+ * @returns 返回一个取消订阅的函数,调用即可移除该监听器
24
+ */
25
+ on(event, handler) {
26
+ if (!this.listeners.has(event)) this.listeners.set(event, /* @__PURE__ */ new Set());
27
+ this.listeners.get(event).add(handler);
28
+ return () => this.off(event, handler);
29
+ }
30
+ /**
31
+ * 移除指定事件的某个监听器
32
+ * @param event - 事件名称
33
+ * @param handler - 要移除的处理函数引用(必须与注册时为同一函数引用)
34
+ */
35
+ off(event, handler) {
36
+ this.listeners.get(event)?.delete(handler);
37
+ }
38
+ /**
39
+ * 触发(发射)指定事件,依次调用所有已注册的监听器
40
+ * @param event - 要触发的事件名称
41
+ * @param args - 传递给监听器函数的参数列表(剩余参数语法)
42
+ */
43
+ emit(event, ...args) {
44
+ this.listeners.get(event)?.forEach((handler) => {
45
+ try {
46
+ handler(...args);
47
+ } catch (err) {
48
+ console.error(`[Chat] Event handler error for "${event}":`, err);
49
+ }
50
+ });
51
+ }
52
+ /**
53
+ * 注册一次性事件监听器(触发一次后自动移除)
54
+ * @param event - 要监听的事件名称
55
+ * @param handler - 事件触发时执行的回调函数
56
+ * @returns 返回取消订阅的函数,可在触发前手动取消
57
+ */
58
+ once(event, handler) {
59
+ const wrapper = (...args) => {
60
+ this.off(event, wrapper);
61
+ handler(...args);
62
+ };
63
+ return this.on(event, wrapper);
64
+ }
65
+ /**
66
+ * 批量移除事件监听器
67
+ * @param event - 可选的事件名称
68
+ * - 若传入事件名,则只移除该事件的所有监听器
69
+ * - 若不传参数,则清空所有事件的所有监听器
70
+ */
71
+ removeAll(event) {
72
+ if (event) this.listeners.delete(event);
73
+ else this.listeners.clear();
74
+ }
75
+ };
76
+ //#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
+ //#region src/core/mention-engine.ts
159
+ var MentionEngine = class {
160
+ constructor(emitter, members) {
161
+ this.members = [];
162
+ this.state = {
163
+ active: false,
164
+ query: "",
165
+ filtered: [],
166
+ activeIndex: 0,
167
+ triggerPosition: -1
168
+ };
169
+ this.selectedMentions = [];
170
+ this.emitter = emitter;
171
+ if (members) this.members = [...members];
172
+ }
173
+ getState() {
174
+ return {
175
+ ...this.state,
176
+ filtered: [...this.state.filtered]
177
+ };
178
+ }
179
+ getSelectedMentions() {
180
+ return [...this.selectedMentions];
181
+ }
182
+ setMembers(members) {
183
+ this.members = [...members];
184
+ if (this.state.active) this.filter(this.state.query);
185
+ }
186
+ handleInput(text, cursorPos) {
187
+ const beforeCursor = text.slice(0, cursorPos);
188
+ const atIndex = beforeCursor.lastIndexOf("@");
189
+ if (atIndex === -1) {
190
+ this.deactivate();
191
+ return;
192
+ }
193
+ if (atIndex > 0 && !/\s/.test(beforeCursor[atIndex - 1])) {
194
+ this.deactivate();
195
+ return;
196
+ }
197
+ const query = beforeCursor.slice(atIndex + 1);
198
+ if (/[^\S\r\n]/.test(query)) {
199
+ this.deactivate();
200
+ return;
201
+ }
202
+ this.state.active = true;
203
+ this.state.triggerPosition = atIndex;
204
+ this.filter(query);
205
+ }
206
+ moveUp() {
207
+ if (!this.state.active || this.state.filtered.length === 0) return;
208
+ this.state.activeIndex = (this.state.activeIndex - 1 + this.state.filtered.length) % this.state.filtered.length;
209
+ this.emitter.emit("mention-active-change", this.state.activeIndex);
210
+ }
211
+ moveDown() {
212
+ if (!this.state.active || this.state.filtered.length === 0) return;
213
+ this.state.activeIndex = (this.state.activeIndex + 1) % this.state.filtered.length;
214
+ this.emitter.emit("mention-active-change", this.state.activeIndex);
215
+ }
216
+ selectCurrent() {
217
+ if (!this.state.active || this.state.filtered.length === 0) return null;
218
+ return this.select(this.state.activeIndex);
219
+ }
220
+ select(index) {
221
+ if (index < 0 || index >= this.state.filtered.length) return null;
222
+ const member = this.state.filtered[index];
223
+ if (!this.selectedMentions.find((m) => m.id === member.id)) this.selectedMentions.push(member);
224
+ this.emitter.emit("mention-select", member);
225
+ this.deactivate();
226
+ return member;
227
+ }
228
+ deactivate() {
229
+ if (!this.state.active) return;
230
+ this.state = {
231
+ active: false,
232
+ query: "",
233
+ filtered: [],
234
+ activeIndex: 0,
235
+ triggerPosition: -1
236
+ };
237
+ this.emitter.emit("mention-deactivate");
238
+ }
239
+ parseMentions(text) {
240
+ const mentions = [];
241
+ 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);
243
+ return mentions;
244
+ }
245
+ resetSelected() {
246
+ this.selectedMentions = [];
247
+ }
248
+ getInsertText(member) {
249
+ return `@${member.name} `;
250
+ }
251
+ filter(query) {
252
+ const normalizedQuery = query.trim().toLowerCase();
253
+ this.state.query = query;
254
+ this.state.filtered = this.members.filter((member) => member.name.toLowerCase().includes(normalizedQuery) || member.id.toLowerCase().includes(normalizedQuery));
255
+ this.state.activeIndex = 0;
256
+ this.emitter.emit("mention-filter", this.getState());
257
+ }
258
+ escapeRegex(str) {
259
+ return str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
260
+ }
261
+ };
262
+ //#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);
401
+ }
402
+ /** 清理监听器 */
403
+ cleanupMediaListener() {
404
+ if (this.mediaQuery && this.mediaHandler) {
405
+ this.mediaQuery.removeEventListener("change", this.mediaHandler);
406
+ this.mediaQuery = null;
407
+ this.mediaHandler = null;
408
+ }
409
+ }
410
+ /** 销毁 */
411
+ destroy() {
412
+ this.cleanupMediaListener();
413
+ }
414
+ };
415
+ //#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`;
429
+ }
430
+ function cx(...classes) {
431
+ return classes.filter(Boolean).join(" ");
432
+ }
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");
467
+ }
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
+ }
727
+ }
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
+ }
737
+ }
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;
1134
+ }
1135
+ function tokenVars(t) {
1136
+ const vars = {
1137
+ bgPrimary: t.bgPrimary,
1138
+ bgSecondary: t.bgSecondary,
1139
+ bgBubbleOutgoing: t.bgBubbleOutgoing,
1140
+ bgBubbleIncoming: t.bgBubbleIncoming,
1141
+ bgInput: t.bgInput,
1142
+ bgMention: t.bgMention,
1143
+ bgMentionHover: t.bgMentionHover,
1144
+ bgMentionActive: t.bgMentionActive,
1145
+ bgLoadMore: t.bgLoadMore,
1146
+ bgSystemMessage: t.bgSystemMessage,
1147
+ bgDropOverlay: t.bgDropOverlay,
1148
+ bgDangerSoft: t.bgDangerSoft,
1149
+ textPrimary: t.textPrimary,
1150
+ textSecondary: t.textSecondary,
1151
+ textBubbleOutgoing: t.textBubbleOutgoing,
1152
+ textBubbleIncoming: t.textBubbleIncoming,
1153
+ textMention: t.textMention,
1154
+ textMentionTag: t.textMentionTag,
1155
+ textTimestamp: t.textTimestamp,
1156
+ textSystem: t.textSystem,
1157
+ textPlaceholder: t.textPlaceholder,
1158
+ borderPrimary: t.borderPrimary,
1159
+ borderInput: t.borderInput,
1160
+ borderInputFocus: t.borderInputFocus,
1161
+ borderMention: t.borderMention,
1162
+ colorSending: t.colorSending,
1163
+ colorSent: t.colorSent,
1164
+ colorDelivered: t.colorDelivered,
1165
+ colorRead: t.colorRead,
1166
+ colorFailed: t.colorFailed,
1167
+ colorLink: t.colorLink,
1168
+ shadowPanel: t.shadowPanel,
1169
+ shadowMention: t.shadowMention,
1170
+ avatarBg: t.avatarBg,
1171
+ avatarText: t.avatarText,
1172
+ btnPrimaryBg: t.btnPrimaryBg,
1173
+ btnPrimaryText: t.btnPrimaryText,
1174
+ btnPrimaryHover: t.btnPrimaryHover,
1175
+ scrollbarThumb: t.scrollbarThumb,
1176
+ scrollbarTrack: t.scrollbarTrack,
1177
+ bgToolbar: t.bgToolbar,
1178
+ borderToolbar: t.borderToolbar,
1179
+ toolbarIconColor: t.toolbarIconColor,
1180
+ toolbarIconHover: t.toolbarIconHover,
1181
+ toolbarVoiceActive: t.toolbarVoiceActive
1182
+ };
1183
+ return Object.entries(vars).map(([key, value]) => ` ${COMPONENT_CSS_VAR_PREFIX}-${toKebab(key)}: ${value};`).join("\n");
1184
+ }
1185
+ function sizeVars() {
1186
+ 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);
1213
+ ${COMPONENT_CSS_VAR_PREFIX}-message-max-width: 65%;
1214
+ ${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);
1228
+ `;
1229
+ }
1230
+ function generateCSS(tokens, p) {
1231
+ return `
1232
+ /* @composy/chat-core styles */
1233
+ .${p}-container,
1234
+ .${p}-container * {
1235
+ box-sizing: border-box;
1236
+ }
1237
+
1238
+ .${p}-container {
1239
+ ${tokenVars(tokens)}
1240
+ ${sizeVars()}
1241
+ display: flex;
1242
+ flex-direction: column;
1243
+ width: 100%;
1244
+ height: 100%;
1245
+ overflow: hidden;
1246
+ position: relative;
1247
+ background: ${v("bg-primary")};
1248
+ color: ${v("text-primary")};
1249
+ font-family: ${v("font-family")};
1250
+ font-size: ${v("font-size")};
1251
+ line-height: ${v("line-height")};
1252
+ border: ${v("border-width")} solid ${v("border-primary")};
1253
+ border-radius: ${v("radius-md")};
1254
+ box-shadow: ${v("shadow-panel")};
1255
+ }
1256
+
1257
+ .${p}-message-list {
1258
+ flex: 1;
1259
+ min-width: 0;
1260
+ overflow-y: auto;
1261
+ overflow-x: hidden;
1262
+ padding: ${v("space-6")};
1263
+ scroll-behavior: smooth;
1264
+ }
1265
+
1266
+ .${p}-message-list::-webkit-scrollbar {
1267
+ width: var(--size-3);
1268
+ }
1269
+
1270
+ .${p}-message-list::-webkit-scrollbar-thumb {
1271
+ background: ${v("scrollbar-thumb")};
1272
+ border-radius: ${v("radius-pill")};
1273
+ }
1274
+
1275
+ .${p}-message-list::-webkit-scrollbar-track {
1276
+ background: ${v("scrollbar-track")};
1277
+ }
1278
+
1279
+ .${p}-load-more {
1280
+ margin-bottom: ${v("space-5")};
1281
+ padding: ${v("space-5")};
1282
+ text-align: center;
1283
+ color: ${v("color-link")};
1284
+ font-size: ${v("font-size-sm")};
1285
+ background: ${v("bg-load-more")};
1286
+ border-radius: ${v("radius-sm")};
1287
+ cursor: pointer;
1288
+ user-select: none;
1289
+ transition: opacity ${v("transition-fast")}, background ${v("transition-fast")};
1290
+ }
1291
+
1292
+ .${p}-load-more:hover {
1293
+ background: ${v("bg-mention-hover")};
1294
+ }
1295
+
1296
+ .${p}-load-more--loading {
1297
+ cursor: default;
1298
+ opacity: .6;
1299
+ }
1300
+
1301
+ .${p}-time-divider {
1302
+ padding: ${v("space-4")} 0;
1303
+ color: ${v("text-timestamp")};
1304
+ font-size: ${v("font-size-sm")};
1305
+ text-align: center;
1306
+ user-select: none;
1307
+ }
1308
+
1309
+ .${p}-time-divider span {
1310
+ padding: ${v("space-1")} ${v("space-5")};
1311
+ background: ${v("bg-secondary")};
1312
+ border-radius: ${v("radius-pill")};
1313
+ }
1314
+
1315
+ .${p}-message-row {
1316
+ display: flex;
1317
+ align-items: flex-start;
1318
+ gap: ${v("space-4")};
1319
+ margin-bottom: ${v("space-5")};
1320
+ }
1321
+
1322
+ .${p}-message-row--outgoing {
1323
+ flex-direction: row-reverse;
1324
+ }
1325
+
1326
+ .${p}-message-row--system {
1327
+ justify-content: center;
1328
+ }
1329
+
1330
+ .${p}-avatar {
1331
+ display: flex;
1332
+ align-items: center;
1333
+ justify-content: center;
1334
+ width: ${v("avatar-size")};
1335
+ height: ${v("avatar-size")};
1336
+ overflow: hidden;
1337
+ flex-shrink: 0;
1338
+ color: ${v("avatar-text")};
1339
+ background: ${v("avatar-bg")};
1340
+ border-radius: ${v("radius-circle")};
1341
+ font-size: ${v("font-size")};
1342
+ font-weight: ${v("font-weight-semibold")};
1343
+ }
1344
+
1345
+ .${p}-avatar img {
1346
+ width: 100%;
1347
+ height: 100%;
1348
+ object-fit: cover;
1349
+ }
1350
+
1351
+ .${p}-message-body {
1352
+ display: flex;
1353
+ flex-direction: column;
1354
+ max-width: ${v("message-max-width")};
1355
+ min-width: 0;
1356
+ }
1357
+
1358
+ .${p}-message-row--outgoing .${p}-message-body {
1359
+ align-items: flex-end;
1360
+ }
1361
+
1362
+ .${p}-nickname {
1363
+ margin-bottom: ${v("space-2")};
1364
+ padding: 0 ${v("space-2")};
1365
+ color: ${v("text-secondary")};
1366
+ font-size: ${v("font-size-sm")};
1367
+ }
1368
+
1369
+ .${p}-bubble {
1370
+ position: relative;
1371
+ max-width: 100%;
1372
+ padding: ${v("space-5")} ${v("space-6")};
1373
+ border-radius: ${v("radius-lg")};
1374
+ word-break: break-word;
1375
+ overflow-wrap: unknownwhere;
1376
+ white-space: pre-wrap;
1377
+ line-height: ${v("line-height-bubble")};
1378
+ }
1379
+
1380
+ .${p}-bubble--incoming {
1381
+ color: ${v("text-bubble-incoming")};
1382
+ background: ${v("bg-bubble-incoming")};
1383
+ border-top-left-radius: ${v("radius-xs")};
1384
+ }
1385
+
1386
+ .${p}-bubble--outgoing {
1387
+ color: ${v("text-bubble-outgoing")};
1388
+ background: ${v("bg-bubble-outgoing")};
1389
+ border-top-right-radius: ${v("radius-xs")};
1390
+ }
1391
+
1392
+ .${p}-bubble--image {
1393
+ padding: ${v("space-2")};
1394
+ background: unset;
1395
+ }
1396
+
1397
+ .${p}-bubble--image img {
1398
+ display: block;
1399
+ max-width: ${v("image-max-size")};
1400
+ max-height: ${v("image-max-size")};
1401
+ border-radius: ${v("radius-md")};
1402
+ cursor: pointer;
1403
+ }
1404
+
1405
+ .${p}-bubble--file {
1406
+ display: flex;
1407
+ align-items: center;
1408
+ gap: ${v("space-5")};
1409
+ min-width: ${v("file-bubble-min-width")};
1410
+ }
1411
+
1412
+ .${p}-file-icon {
1413
+ display: flex;
1414
+ align-items: center;
1415
+ justify-content: center;
1416
+ width: ${v("file-icon-size")};
1417
+ height: ${v("file-icon-size")};
1418
+ flex-shrink: 0;
1419
+ background: ${v("bg-secondary")};
1420
+ border-radius: ${v("radius-sm")};
1421
+ font-size: ${v("font-size-lg")};
1422
+ }
1423
+
1424
+ .${p}-file-info {
1425
+ flex: 1;
1426
+ min-width: 0;
1427
+ overflow: hidden;
1428
+ }
1429
+
1430
+ .${p}-file-name {
1431
+ overflow: hidden;
1432
+ font-size: ${v("font-size")};
1433
+ text-overflow: ellipsis;
1434
+ white-space: nowrap;
1435
+ }
1436
+
1437
+ .${p}-file-size {
1438
+ margin-top: ${v("space-1")};
1439
+ color: ${v("text-secondary")};
1440
+ font-size: ${v("font-size-sm")};
1441
+ }
1442
+
1443
+ .${p}-system-message {
1444
+ max-width: ${v("system-message-max-width")};
1445
+ padding: ${v("space-2")} ${v("space-6")};
1446
+ color: ${v("text-system")};
1447
+ background: ${v("bg-system-message")};
1448
+ border-radius: ${v("radius-pill")};
1449
+ font-size: ${v("font-size-sm")};
1450
+ text-align: center;
1451
+ }
1452
+
1453
+ .${p}-message-footer {
1454
+ display: flex;
1455
+ align-items: center;
1456
+ gap: ${v("space-3")};
1457
+ margin-top: ${v("space-1")};
1458
+ padding: 0 ${v("space-2")};
1459
+ }
1460
+
1461
+ .${p}-message-row--outgoing .${p}-message-footer {
1462
+ flex-direction: row-reverse;
1463
+ }
1464
+
1465
+ .${p}-timestamp,
1466
+ .${p}-status {
1467
+ font-size: ${v("font-size-xs")};
1468
+ }
1469
+
1470
+ .${p}-timestamp {
1471
+ color: ${v("text-timestamp")};
1472
+ }
1473
+
1474
+ .${p}-status--sending { color: ${v("color-sending")}; }
1475
+ .${p}-status--sent { color: ${v("color-sent")}; }
1476
+ .${p}-status--delivered { color: ${v("color-delivered")}; }
1477
+ .${p}-status--read { color: ${v("color-read")}; }
1478
+ .${p}-status--failed {
1479
+ color: ${v("color-failed")};
1480
+ cursor: pointer;
1481
+ }
1482
+
1483
+ .${p}-mention-tag,
1484
+ .${p}-link {
1485
+ color: ${v("text-mention-tag")};
1486
+ font-weight: ${v("font-weight-medium")};
1487
+ }
1488
+
1489
+ .${p}-mention-tag {
1490
+ cursor: pointer;
1491
+ }
1492
+
1493
+ .${p}-mention-tag:hover,
1494
+ .${p}-link:hover {
1495
+ text-decoration: underline;
1496
+ }
1497
+
1498
+ .${p}-input-area {
1499
+ display: flex;
1500
+ align-items: flex-end;
1501
+ gap: ${v("space-4")};
1502
+ position: relative;
1503
+ padding: ${v("space-5")} ${v("space-6")};
1504
+ border-top: ${v("border-width")} solid ${v("border-primary")};
1505
+ background: ${v("bg-input")};
1506
+ }
1507
+
1508
+ .${p}-input-wrapper {
1509
+ flex: 1;
1510
+ min-width: 0;
1511
+ position: relative;
1512
+ }
1513
+
1514
+ .${p}-textarea {
1515
+ width: 100%;
1516
+ min-height: ${v("textarea-min-height")};
1517
+ max-height: ${v("textarea-max-height")};
1518
+ padding: ${v("space-4")} ${v("space-5")};
1519
+ overflow-y: auto;
1520
+ resize: none;
1521
+ outline: none;
1522
+ color: ${v("text-primary")};
1523
+ background: ${v("bg-primary")};
1524
+ border: ${v("border-width")} solid ${v("border-input")};
1525
+ border-radius: ${v("radius-md")};
1526
+ font-family: inherit;
1527
+ font-size: ${v("font-size")};
1528
+ line-height: ${v("line-height")};
1529
+ transition: border-color ${v("transition-fast")}, box-shadow ${v("transition-fast")};
1530
+ }
1531
+
1532
+ .${p}-textarea:focus {
1533
+ border-color: ${v("border-input-focus")};
1534
+ box-shadow: 0 0 0 ${v("border-width-drop")} ${v("bg-drop-overlay")};
1535
+ }
1536
+
1537
+ .${p}-textarea::placeholder {
1538
+ color: ${v("text-placeholder")};
1539
+ }
1540
+
1541
+ .${p}-send-btn {
1542
+ display: flex;
1543
+ align-items: center;
1544
+ justify-content: center;
1545
+ width: ${v("input-button-size")};
1546
+ height: ${v("input-button-size")};
1547
+ flex-shrink: 0;
1548
+ color: ${v("btn-primary-text")};
1549
+ background: ${v("btn-primary-bg")};
1550
+ border: 0;
1551
+ border-radius: ${v("radius-md")};
1552
+ cursor: pointer;
1553
+ transition: background ${v("transition-fast")}, opacity ${v("transition-fast")};
1554
+ }
1555
+
1556
+ .${p}-send-btn:hover {
1557
+ background: ${v("btn-primary-hover")};
1558
+ }
1559
+
1560
+ .${p}-send-btn:disabled {
1561
+ cursor: default;
1562
+ opacity: .5;
1563
+ }
1564
+
1565
+ .${p}-send-btn svg {
1566
+ width: var(--size-7);
1567
+ height: var(--size-7);
1568
+ }
1569
+
1570
+ .${p}-mention-panel {
1571
+ position: absolute;
1572
+ left: 0;
1573
+ right: 0;
1574
+ bottom: 100%;
1575
+ z-index: ${v("z-mention")};
1576
+ max-height: ${v("panel-max-height")};
1577
+ margin-bottom: ${v("space-2")};
1578
+ overflow-y: auto;
1579
+ background: ${v("bg-mention")};
1580
+ border: ${v("border-width")} solid ${v("border-mention")};
1581
+ border-radius: ${v("radius-md")};
1582
+ box-shadow: ${v("shadow-mention")};
1583
+ }
1584
+
1585
+ .${p}-mention-item {
1586
+ display: flex;
1587
+ align-items: center;
1588
+ gap: ${v("space-4")};
1589
+ padding: ${v("space-4")} ${v("space-5")};
1590
+ color: ${v("text-mention")};
1591
+ cursor: pointer;
1592
+ transition: background ${v("transition-fast")};
1593
+ }
1594
+
1595
+ .${p}-mention-item:hover {
1596
+ background: ${v("bg-mention-hover")};
1597
+ }
1598
+
1599
+ .${p}-mention-item--active {
1600
+ background: ${v("bg-mention-active")};
1601
+ }
1602
+
1603
+ .${p}-mention-item-avatar {
1604
+ display: flex;
1605
+ align-items: center;
1606
+ justify-content: center;
1607
+ width: ${v("mention-avatar-size")};
1608
+ height: ${v("mention-avatar-size")};
1609
+ flex-shrink: 0;
1610
+ overflow: hidden;
1611
+ color: ${v("avatar-text")};
1612
+ background: ${v("avatar-bg")};
1613
+ border-radius: ${v("radius-circle")};
1614
+ font-size: ${v("font-size-sm")};
1615
+ font-weight: ${v("font-weight-semibold")};
1616
+ }
1617
+
1618
+ .${p}-mention-item-avatar img {
1619
+ width: 100%;
1620
+ height: 100%;
1621
+ object-fit: cover;
1622
+ }
1623
+
1624
+ .${p}-mention-item-name {
1625
+ font-size: ${v("font-size")};
1626
+ }
1627
+
1628
+ .${p}-empty {
1629
+ display: flex;
1630
+ align-items: center;
1631
+ justify-content: center;
1632
+ height: 100%;
1633
+ color: ${v("text-secondary")};
1634
+ font-size: ${v("font-size")};
1635
+ }
1636
+
1637
+ .${p}-toolbar {
1638
+ display: flex;
1639
+ align-items: center;
1640
+ gap: ${v("space-1")};
1641
+ padding: ${v("space-3")} ${v("space-4")};
1642
+ background: ${v("bg-toolbar")};
1643
+ border-bottom: ${v("border-width")} solid ${v("border-toolbar")};
1644
+ border-radius: ${v("radius-md")} ${v("radius-md")} 0 0;
1645
+ }
1646
+
1647
+ .${p}-toolbar-btn {
1648
+ display: flex;
1649
+ align-items: center;
1650
+ justify-content: center;
1651
+ width: ${v("toolbar-button-size")};
1652
+ height: ${v("toolbar-button-size")};
1653
+ flex-shrink: 0;
1654
+ padding: 0;
1655
+ color: ${v("toolbar-icon-color")};
1656
+ background: unset;
1657
+ border: 0;
1658
+ border-radius: ${v("radius-sm")};
1659
+ cursor: pointer;
1660
+ transition: background ${v("transition-fast")}, color ${v("transition-fast")};
1661
+ }
1662
+
1663
+ .${p}-toolbar-btn svg {
1664
+ width: var(--size-6);
1665
+ height: var(--size-6);
1666
+ }
1667
+
1668
+ .${p}-toolbar-btn:hover {
1669
+ color: ${v("toolbar-icon-hover")};
1670
+ background: ${v("bg-drop-overlay")};
1671
+ }
1672
+
1673
+ .${p}-toolbar-btn--active {
1674
+ color: ${v("toolbar-voice-active")};
1675
+ background: ${v("bg-danger-soft")};
1676
+ }
1677
+
1678
+ .${p}-toolbar-btn--active:hover {
1679
+ color: ${v("toolbar-voice-active")};
1680
+ background: ${v("bg-danger-soft")};
1681
+ }
1682
+
1683
+ .${p}-drop-overlay {
1684
+ position: absolute;
1685
+ inset: 0;
1686
+ z-index: ${v("z-overlay")};
1687
+ display: flex;
1688
+ align-items: center;
1689
+ justify-content: center;
1690
+ pointer-events: none;
1691
+ background: ${v("bg-drop-overlay")};
1692
+ border: ${v("border-width-drop")} dashed ${v("toolbar-icon-hover")};
1693
+ border-radius: ${v("radius-md")};
1694
+ }
1695
+
1696
+ .${p}-drop-overlay span {
1697
+ color: ${v("toolbar-icon-hover")};
1698
+ font-size: ${v("font-size-sm")};
1699
+ font-weight: ${v("font-weight-medium")};
1700
+ }
1701
+
1702
+ @keyframes ${p}-blink {
1703
+ 0%, 100% { opacity: 1; }
1704
+ 50% { opacity: 0; }
1705
+ }
1706
+
1707
+ .${p}-bubble--streaming::after {
1708
+ content: '';
1709
+ display: inline-block;
1710
+ width: ${v("border-width-drop")};
1711
+ height: 1em;
1712
+ margin-left: ${v("space-1")};
1713
+ vertical-align: text-bottom;
1714
+ background: currentColor;
1715
+ animation: ${p}-blink 800ms infinite;
1716
+ }
1717
+ `;
1718
+ }
1719
+ function v(name) {
1720
+ return `var(${COMPONENT_CSS_VAR_PREFIX}-${name})`;
1721
+ }
1722
+ function toKebab(key) {
1723
+ return key.replace(/[A-Z]/g, (match) => `-${match.toLowerCase()}`);
1724
+ }
1725
+ //#endregion
1726
+ //#region src/index.ts
1727
+ const version = "0.1.0";
1728
+ function createChat(config) {
1729
+ const container = typeof config.container === "string" ? document.querySelector(config.container) : config.container;
1730
+ if (!container) throw new Error(`[Chat] Container not found: ${config.container}`);
1731
+ const prefix = normalizeClassPrefix(config.classPrefix ?? "ldesign-chat");
1732
+ const normalizedConfig = {
1733
+ ...config,
1734
+ classPrefix: prefix
1735
+ };
1736
+ const emitter = new EventEmitter();
1737
+ const chatManager = new ChatManager(emitter, normalizedConfig.messages);
1738
+ const mentionEngine = new MentionEngine(emitter, normalizedConfig.mentionMembers);
1739
+ const themeManager = new ThemeManager(emitter, normalizedConfig.theme ?? "light");
1740
+ const renderer = new DOMRenderer(container, normalizedConfig, emitter, mentionEngine);
1741
+ injectStyles(themeManager.getTokens(), prefix);
1742
+ renderer.init();
1743
+ renderer.renderMessages(chatManager.getMessages());
1744
+ requestAnimationFrame(() => {
1745
+ renderer.scrollToBottom(false);
1746
+ });
1747
+ emitter.on("theme-change", (_resolved, tokens) => {
1748
+ updateStyles(tokens, prefix);
1749
+ });
1750
+ emitter.on("message-added", (message) => {
1751
+ renderer.appendMessage(message);
1752
+ requestAnimationFrame(() => {
1753
+ renderer.scrollToBottom(true);
1754
+ });
1755
+ });
1756
+ emitter.on("messages-prepended", (messages) => {
1757
+ renderer.prependMessages(messages);
1758
+ });
1759
+ emitter.on("message-updated", (message) => {
1760
+ renderer.updateMessage(message.id, message);
1761
+ });
1762
+ emitter.on("message-removed", (message) => {
1763
+ renderer.removeMessage(message.id);
1764
+ });
1765
+ emitter.on("message-content-appended", (id, content) => {
1766
+ renderer.updateMessageContent(id, content);
1767
+ requestAnimationFrame(() => {
1768
+ renderer.scrollToBottom(false);
1769
+ });
1770
+ });
1771
+ return {
1772
+ addMessage(message) {
1773
+ chatManager.addMessage(message);
1774
+ },
1775
+ prependMessages(messages) {
1776
+ chatManager.prependMessages(messages);
1777
+ },
1778
+ updateMessage(id, updates) {
1779
+ chatManager.updateMessage(id, updates);
1780
+ },
1781
+ removeMessage(id) {
1782
+ chatManager.removeMessage(id);
1783
+ },
1784
+ setMessages(messages) {
1785
+ chatManager.setMessages(messages);
1786
+ renderer.renderMessages(chatManager.getMessages());
1787
+ requestAnimationFrame(() => {
1788
+ renderer.scrollToBottom(false);
1789
+ });
1790
+ },
1791
+ clearMessages() {
1792
+ chatManager.clearMessages();
1793
+ renderer.renderMessages([]);
1794
+ },
1795
+ scrollToBottom(smooth = true) {
1796
+ renderer.scrollToBottom(smooth);
1797
+ },
1798
+ setTheme(theme) {
1799
+ themeManager.setTheme(theme);
1800
+ },
1801
+ setMentionMembers(members) {
1802
+ mentionEngine.setMembers(members);
1803
+ },
1804
+ setLoadingMore(loading) {
1805
+ renderer.setLoadingMore(loading);
1806
+ },
1807
+ focusInput() {
1808
+ renderer.focusInput();
1809
+ },
1810
+ setInputContent(content) {
1811
+ renderer.setInputContent(content);
1812
+ },
1813
+ getInputContent() {
1814
+ return renderer.getInputContent();
1815
+ },
1816
+ clearInput() {
1817
+ renderer.clearInput();
1818
+ },
1819
+ isAtBottom(threshold) {
1820
+ return renderer.isAtBottom(threshold);
1821
+ },
1822
+ on(event, handler) {
1823
+ return emitter.on(event, handler);
1824
+ },
1825
+ getMessages() {
1826
+ return chatManager.getMessages();
1827
+ },
1828
+ getMessage(id) {
1829
+ return chatManager.getMessage(id);
1830
+ },
1831
+ appendMessageContent(id, chunk) {
1832
+ chatManager.appendMessageContent(id, chunk);
1833
+ },
1834
+ finishStreaming(id) {
1835
+ chatManager.finishStreaming(id);
1836
+ },
1837
+ destroy() {
1838
+ themeManager.destroy();
1839
+ renderer.destroy();
1840
+ emitter.removeAll();
1841
+ removeStyles(prefix);
1842
+ }
1843
+ };
1844
+ }
1845
+ //#endregion
1846
+ exports.COMPONENT_CSS_VAR_PREFIX = COMPONENT_CSS_VAR_PREFIX;
1847
+ exports.ChatManager = ChatManager;
1848
+ exports.DEFAULT_CLASS_PREFIX = DEFAULT_CLASS_PREFIX;
1849
+ exports.DOMRenderer = DOMRenderer;
1850
+ exports.EventEmitter = EventEmitter;
1851
+ exports.MentionEngine = MentionEngine;
1852
+ exports.STYLE_ELEMENT_ID = STYLE_ELEMENT_ID;
1853
+ exports.ThemeManager = ThemeManager;
1854
+ exports.cls = cls;
1855
+ exports.clsx = clsx;
1856
+ exports.createChat = createChat;
1857
+ exports.cx = cx;
1858
+ exports.darkTokens = darkTokens;
1859
+ exports.getStyleElementId = getStyleElementId;
1860
+ exports.injectStyles = injectStyles;
1861
+ exports.lightTokens = lightTokens;
1862
+ exports.normalizeClassPrefix = normalizeClassPrefix;
1863
+ exports.removeStyles = removeStyles;
1864
+ exports.updateStyles = updateStyles;
1865
+ exports.version = version;
1866
+ });
1867
+ /*! End of @composy/chat-core | Powered by @composy/builder */
1868
+ //# sourceMappingURL=index.js.map