@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
@@ -0,0 +1,678 @@
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
+ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
11
+ const require_dom = require("../constants/dom.cjs");
12
+ //#region src/renderer/dom-renderer.ts
13
+ const SEND_ICON_SVG = "<svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><line x1=\"22\" y1=\"2\" x2=\"11\" y2=\"13\"></line><polygon points=\"22 2 15 22 11 13 2 9 22 2\"></polygon></svg>";
14
+ const ICON_IMAGE = "<svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><rect width=\"18\" height=\"18\" x=\"3\" y=\"3\" rx=\"2\" ry=\"2\"/><circle cx=\"9\" cy=\"9\" r=\"2\"/><path d=\"m21 15-3.086-3.086a2 2 0 0 0-2.828 0L6 21\"/></svg>";
15
+ 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>";
16
+ const ICON_MIC = "<svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"M12 2a3 3 0 0 0-3 3v7a3 3 0 0 0 6 0V5a3 3 0 0 0-3-3Z\"/><path d=\"M19 10v2a7 7 0 0 1-14 0v-2\"/><line x1=\"12\" x2=\"12\" y1=\"19\" y2=\"22\"/></svg>";
17
+ const ICON_MIC_OFF = "<svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><line x1=\"2\" x2=\"22\" y1=\"2\" y2=\"22\"/><path d=\"M18.89 13.23A7.12 7.12 0 0 0 19 12v-2\"/><path d=\"M5 10v2a7 7 0 0 0 12 5.29\"/><path d=\"M15 9.34V5a3 3 0 0 0-5.68-1.33\"/><path d=\"M9 9v3a3 3 0 0 0 5.12 2.12\"/><line x1=\"12\" x2=\"12\" y1=\"19\" y2=\"22\"/></svg>";
18
+ const FILE_ICON = "<svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"M15 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V7Z\"/><path d=\"M14 2v4a2 2 0 0 0 2 2h4\"/></svg>";
19
+ const STATUS_MAP = {
20
+ sending: "...",
21
+ sent: "✓",
22
+ delivered: "✓✓",
23
+ read: "✓✓",
24
+ failed: "!"
25
+ };
26
+ var DOMRenderer = class {
27
+ constructor(container, config, emitter, mentionEngine) {
28
+ this.inputArea = null;
29
+ this.textareaEl = null;
30
+ this.sendBtn = null;
31
+ this.mentionPanel = null;
32
+ this.loadMoreEl = null;
33
+ this.toolbarEl = null;
34
+ this.dropOverlay = null;
35
+ this.voiceBtn = null;
36
+ this.loadingMore = false;
37
+ this.isRecording = false;
38
+ this.recognition = null;
39
+ this.lastVoiceTranscript = "";
40
+ this.container = container;
41
+ this.config = config;
42
+ this.emitter = emitter;
43
+ this.mentionEngine = mentionEngine;
44
+ this.prefix = require_dom.normalizeClassPrefix(config.classPrefix ?? "ldesign-chat");
45
+ }
46
+ init() {
47
+ this.container.innerHTML = "";
48
+ this.root = this.el("div", `${this.prefix}-container`);
49
+ this.messageListEl = this.el("div", `${this.prefix}-message-list`);
50
+ this.root.appendChild(this.messageListEl);
51
+ if (this.config.showInput !== false) this.renderInputArea();
52
+ this.container.appendChild(this.root);
53
+ this.setupEventListeners();
54
+ }
55
+ updateMessageContent(id, content) {
56
+ const bubble = this.messageListEl.querySelector(`[data-msg-id="${this.escapeAttr(id)}"]`)?.querySelector(`.${this.prefix}-bubble`);
57
+ if (!bubble) return;
58
+ bubble.innerHTML = this.renderTextContent(content);
59
+ }
60
+ renderMessages(messages) {
61
+ this.messageListEl.innerHTML = "";
62
+ if (this.config.enableLoadMore) this.renderLoadMoreButton();
63
+ if (messages.length === 0) {
64
+ this.renderEmpty();
65
+ return;
66
+ }
67
+ let lastTimestamp = 0;
68
+ const interval = this.config.timeGroupInterval ?? 3e5;
69
+ for (const message of messages) {
70
+ if (this.config.showTimestamp !== false && message.timestamp - lastTimestamp > interval) {
71
+ this.renderTimeDivider(message.timestamp);
72
+ lastTimestamp = message.timestamp;
73
+ }
74
+ this.renderMessageRow(message);
75
+ }
76
+ }
77
+ appendMessage(message) {
78
+ this.removeEmpty();
79
+ const messageEls = this.getMessageElements();
80
+ const lastTime = messageEls.length > 0 ? Number.parseInt(messageEls[messageEls.length - 1].dataset.timestamp ?? "0", 10) : 0;
81
+ const interval = this.config.timeGroupInterval ?? 3e5;
82
+ if (this.config.showTimestamp !== false && message.timestamp - lastTime > interval) this.renderTimeDivider(message.timestamp);
83
+ this.renderMessageRow(message);
84
+ }
85
+ prependMessages(messages) {
86
+ if (!messages.length) return;
87
+ this.removeEmpty();
88
+ const scrollHeight = this.messageListEl.scrollHeight;
89
+ const scrollTop = this.messageListEl.scrollTop;
90
+ const fragment = document.createDocumentFragment();
91
+ let lastTimestamp = 0;
92
+ const interval = this.config.timeGroupInterval ?? 3e5;
93
+ for (const message of messages) {
94
+ if (this.config.showTimestamp !== false && message.timestamp - lastTimestamp > interval) {
95
+ fragment.appendChild(this.createTimeDivider(message.timestamp));
96
+ lastTimestamp = message.timestamp;
97
+ }
98
+ fragment.appendChild(this.createMessageRow(message));
99
+ }
100
+ const firstMessage = this.messageListEl.querySelector(`.${this.prefix}-message-row, .${this.prefix}-time-divider`);
101
+ if (firstMessage) firstMessage.before(fragment);
102
+ else if (this.loadMoreEl) this.loadMoreEl.after(fragment);
103
+ else this.messageListEl.appendChild(fragment);
104
+ this.messageListEl.scrollTop = scrollTop + (this.messageListEl.scrollHeight - scrollHeight);
105
+ }
106
+ updateMessage(id, message) {
107
+ const rowEl = this.messageListEl.querySelector(`[data-msg-id="${this.escapeAttr(id)}"]`);
108
+ if (!rowEl) return;
109
+ rowEl.replaceWith(this.createMessageRow(message));
110
+ }
111
+ removeMessage(id) {
112
+ this.messageListEl.querySelector(`[data-msg-id="${this.escapeAttr(id)}"]`)?.remove();
113
+ if (this.getMessageElements().length === 0) this.renderEmpty();
114
+ }
115
+ scrollToBottom(smooth = true) {
116
+ if (typeof this.messageListEl.scrollTo === "function") {
117
+ this.messageListEl.scrollTo({
118
+ top: this.messageListEl.scrollHeight,
119
+ behavior: smooth ? "smooth" : "auto"
120
+ });
121
+ return;
122
+ }
123
+ this.messageListEl.scrollTop = this.messageListEl.scrollHeight;
124
+ }
125
+ isAtBottom(threshold = 4) {
126
+ return this.messageListEl.scrollHeight - this.messageListEl.scrollTop - this.messageListEl.clientHeight <= threshold;
127
+ }
128
+ setLoadingMore(loading) {
129
+ this.loadingMore = loading;
130
+ if (!this.loadMoreEl) return;
131
+ this.loadMoreEl.textContent = loading ? "加载中..." : this.config.loadMoreText ?? "加载更多";
132
+ this.loadMoreEl.classList.toggle(`${this.prefix}-load-more--loading`, loading);
133
+ }
134
+ focusInput() {
135
+ this.textareaEl?.focus();
136
+ }
137
+ setInputContent(content) {
138
+ if (!this.textareaEl) return;
139
+ this.textareaEl.value = this.limitInput(content);
140
+ this.autoResize();
141
+ this.updateSendButtonState();
142
+ this.config.onInputChange?.(this.textareaEl.value);
143
+ }
144
+ getInputContent() {
145
+ return this.textareaEl?.value ?? "";
146
+ }
147
+ clearInput() {
148
+ this.setInputContent("");
149
+ }
150
+ renderMentionPanel(members, activeIndex) {
151
+ if (!this.mentionPanel) return;
152
+ if (members.length === 0) {
153
+ this.mentionPanel.style.display = "none";
154
+ return;
155
+ }
156
+ this.mentionPanel.style.display = "block";
157
+ this.mentionPanel.innerHTML = "";
158
+ members.forEach((member, index) => {
159
+ const item = this.el("div", require_dom.cx(`${this.prefix}-mention-item`, index === activeIndex && `${this.prefix}-mention-item--active`));
160
+ item.setAttribute("role", "option");
161
+ item.setAttribute("aria-selected", String(index === activeIndex));
162
+ const avatar = this.el("div", `${this.prefix}-mention-item-avatar`);
163
+ if (member.avatar) {
164
+ const img = document.createElement("img");
165
+ img.src = member.avatar;
166
+ img.alt = "";
167
+ avatar.appendChild(img);
168
+ } else avatar.textContent = member.name[0]?.toUpperCase() ?? "";
169
+ item.appendChild(avatar);
170
+ const name = this.el("span", `${this.prefix}-mention-item-name`);
171
+ name.textContent = member.name;
172
+ item.appendChild(name);
173
+ item.addEventListener("click", () => {
174
+ this.mentionEngine.select(index);
175
+ });
176
+ this.mentionPanel?.appendChild(item);
177
+ });
178
+ }
179
+ hideMentionPanel() {
180
+ if (this.mentionPanel) this.mentionPanel.style.display = "none";
181
+ }
182
+ destroy() {
183
+ this.stopVoice();
184
+ this.container.innerHTML = "";
185
+ }
186
+ renderInputArea() {
187
+ this.inputArea = this.el("div", `${this.prefix}-input-area`);
188
+ const wrapper = this.el("div", `${this.prefix}-input-wrapper`);
189
+ this.mentionPanel = this.el("div", `${this.prefix}-mention-panel`);
190
+ this.mentionPanel.style.display = "none";
191
+ this.mentionPanel.setAttribute("role", "listbox");
192
+ wrapper.appendChild(this.mentionPanel);
193
+ if (this.config.enableToolbar !== false) this.renderToolbar(wrapper);
194
+ this.textareaEl = document.createElement("textarea");
195
+ this.textareaEl.className = `${this.prefix}-textarea`;
196
+ this.textareaEl.placeholder = this.config.inputPlaceholder ?? "输入消息...";
197
+ this.textareaEl.rows = 1;
198
+ if (this.config.maxInputLength && this.config.maxInputLength > 0) this.textareaEl.maxLength = this.config.maxInputLength;
199
+ wrapper.appendChild(this.textareaEl);
200
+ this.dropOverlay = this.el("div", `${this.prefix}-drop-overlay`);
201
+ this.dropOverlay.innerHTML = "<span>拖放文件到这里</span>";
202
+ this.dropOverlay.style.display = "none";
203
+ wrapper.appendChild(this.dropOverlay);
204
+ this.inputArea.appendChild(wrapper);
205
+ this.sendBtn = document.createElement("button");
206
+ this.sendBtn.className = `${this.prefix}-send-btn`;
207
+ this.sendBtn.innerHTML = SEND_ICON_SVG;
208
+ this.sendBtn.title = "发送";
209
+ this.sendBtn.type = "button";
210
+ this.sendBtn.disabled = true;
211
+ this.inputArea.appendChild(this.sendBtn);
212
+ this.root.appendChild(this.inputArea);
213
+ }
214
+ renderToolbar(wrapper) {
215
+ const toolbar = this.config.toolbarConfig ?? {};
216
+ this.toolbarEl = this.el("div", `${this.prefix}-toolbar`);
217
+ if (toolbar.image !== false) {
218
+ const imageBtn = this.createToolbarBtn(ICON_IMAGE, "插入图片");
219
+ imageBtn.addEventListener("click", () => this.openFilePicker("image/*"));
220
+ this.toolbarEl.appendChild(imageBtn);
221
+ }
222
+ if (toolbar.file !== false) {
223
+ const fileBtn = this.createToolbarBtn(ICON_PAPERCLIP, "插入文件");
224
+ fileBtn.addEventListener("click", () => this.openFilePicker("*/*"));
225
+ this.toolbarEl.appendChild(fileBtn);
226
+ }
227
+ if (toolbar.voice !== false) {
228
+ this.voiceBtn = this.createToolbarBtn(ICON_MIC, "语音输入");
229
+ this.voiceBtn.addEventListener("click", () => this.toggleVoice());
230
+ this.toolbarEl.appendChild(this.voiceBtn);
231
+ }
232
+ wrapper.insertBefore(this.toolbarEl, wrapper.firstChild);
233
+ }
234
+ createToolbarBtn(iconSvg, title) {
235
+ const btn = document.createElement("button");
236
+ btn.className = `${this.prefix}-toolbar-btn`;
237
+ btn.innerHTML = iconSvg;
238
+ btn.title = title;
239
+ btn.type = "button";
240
+ return btn;
241
+ }
242
+ openFilePicker(accept) {
243
+ const input = document.createElement("input");
244
+ input.type = "file";
245
+ input.accept = accept;
246
+ input.multiple = true;
247
+ input.style.display = "none";
248
+ input.addEventListener("change", () => {
249
+ const files = Array.from(input.files ?? []);
250
+ input.remove();
251
+ if (!files.length) return;
252
+ const isImage = accept.startsWith("image");
253
+ this.emitter.emit(isImage ? "toolbar-image" : "toolbar-file", files);
254
+ if (isImage) this.config.onImageUpload?.(files);
255
+ else this.config.onFileUpload?.(files);
256
+ });
257
+ document.body.appendChild(input);
258
+ input.click();
259
+ }
260
+ toggleVoice() {
261
+ if (this.isRecording) this.stopVoice();
262
+ else this.startVoice();
263
+ }
264
+ startVoice() {
265
+ if (typeof window === "undefined") return;
266
+ const SpeechRecognition = window.SpeechRecognition || window.webkitSpeechRecognition;
267
+ if (!SpeechRecognition) {
268
+ this.emitter.emit("toolbar-voice-unsupported");
269
+ this.config.onVoiceUnsupported?.();
270
+ return;
271
+ }
272
+ try {
273
+ this.lastVoiceTranscript = "";
274
+ this.recognition = new SpeechRecognition();
275
+ this.recognition.continuous = false;
276
+ this.recognition.interimResults = true;
277
+ this.recognition.lang = this.config.voiceLang ?? "zh-CN";
278
+ this.recognition.onresult = (event) => {
279
+ let transcript = "";
280
+ for (let i = 0; i < event.results.length; i++) transcript += event.results[i][0].transcript;
281
+ this.lastVoiceTranscript = transcript;
282
+ if (event.results[event.results.length - 1]?.isFinal && this.textareaEl) this.insertTextAtCursor(transcript);
283
+ };
284
+ this.recognition.onend = () => {
285
+ this.finishVoice();
286
+ this.emitter.emit("toolbar-voice-end", this.lastVoiceTranscript);
287
+ this.config.onVoiceEnd?.(this.lastVoiceTranscript);
288
+ };
289
+ this.recognition.onerror = (error) => {
290
+ this.finishVoice();
291
+ this.emitter.emit("toolbar-voice-error", error);
292
+ this.config.onVoiceError?.(error);
293
+ };
294
+ this.isRecording = true;
295
+ this.voiceBtn?.classList.add(`${this.prefix}-toolbar-btn--active`);
296
+ if (this.voiceBtn) this.voiceBtn.innerHTML = ICON_MIC_OFF;
297
+ this.recognition.start();
298
+ this.emitter.emit("toolbar-voice-start");
299
+ this.config.onVoiceStart?.();
300
+ } catch (error) {
301
+ this.finishVoice();
302
+ this.emitter.emit("toolbar-voice-error", error);
303
+ this.config.onVoiceError?.(error);
304
+ }
305
+ }
306
+ stopVoice() {
307
+ if (!this.recognition) return;
308
+ const recognition = this.recognition;
309
+ this.recognition = null;
310
+ try {
311
+ recognition.stop();
312
+ } catch {
313
+ this.finishVoice();
314
+ }
315
+ }
316
+ finishVoice() {
317
+ this.isRecording = false;
318
+ this.recognition = null;
319
+ this.voiceBtn?.classList.remove(`${this.prefix}-toolbar-btn--active`);
320
+ if (this.voiceBtn) this.voiceBtn.innerHTML = ICON_MIC;
321
+ }
322
+ setupEventListeners() {
323
+ if (this.textareaEl) {
324
+ this.textareaEl.addEventListener("input", () => {
325
+ const limited = this.limitInput(this.textareaEl?.value ?? "");
326
+ if (this.textareaEl && limited !== this.textareaEl.value) this.textareaEl.value = limited;
327
+ this.autoResize();
328
+ this.updateSendButtonState();
329
+ this.config.onInputChange?.(this.textareaEl?.value ?? "");
330
+ if (this.config.enableMention && this.textareaEl) this.mentionEngine.handleInput(this.textareaEl.value, this.textareaEl.selectionStart);
331
+ });
332
+ this.textareaEl.addEventListener("keydown", (event) => {
333
+ if (this.mentionEngine.getState().active) {
334
+ if (event.key === "ArrowUp") {
335
+ event.preventDefault();
336
+ this.mentionEngine.moveUp();
337
+ } else if (event.key === "ArrowDown") {
338
+ event.preventDefault();
339
+ this.mentionEngine.moveDown();
340
+ } else if (event.key === "Enter" && !event.shiftKey) {
341
+ event.preventDefault();
342
+ const member = this.mentionEngine.selectCurrent();
343
+ if (member) this.insertMention(member);
344
+ } else if (event.key === "Escape") {
345
+ event.preventDefault();
346
+ this.mentionEngine.deactivate();
347
+ }
348
+ return;
349
+ }
350
+ if (this.config.enterToSend !== false && event.key === "Enter" && !event.shiftKey) {
351
+ event.preventDefault();
352
+ this.handleSend();
353
+ }
354
+ });
355
+ this.textareaEl.addEventListener("paste", (event) => {
356
+ const items = event.clipboardData?.items;
357
+ if (!items) return;
358
+ const files = [];
359
+ for (let i = 0; i < items.length; i++) if (items[i].kind === "file") {
360
+ const file = items[i].getAsFile();
361
+ if (file) files.push(file);
362
+ }
363
+ if (files.length > 0) {
364
+ event.preventDefault();
365
+ this.emitter.emit("toolbar-paste", files);
366
+ this.config.onPaste?.(files);
367
+ }
368
+ });
369
+ }
370
+ if (this.inputArea) {
371
+ const wrapper = this.inputArea.querySelector(`.${this.prefix}-input-wrapper`);
372
+ if (wrapper) this.setupDropEvents(wrapper);
373
+ }
374
+ this.sendBtn?.addEventListener("click", () => {
375
+ this.handleSend();
376
+ });
377
+ this.emitter.on("mention-filter", (state) => {
378
+ this.renderMentionPanel(state.filtered, state.activeIndex);
379
+ });
380
+ this.emitter.on("mention-active-change", (index) => {
381
+ (this.mentionPanel?.querySelectorAll(`.${this.prefix}-mention-item`))?.forEach((el, i) => {
382
+ el.classList.toggle(`${this.prefix}-mention-item--active`, i === index);
383
+ el.setAttribute("aria-selected", String(i === index));
384
+ });
385
+ });
386
+ this.emitter.on("mention-deactivate", () => {
387
+ this.hideMentionPanel();
388
+ });
389
+ }
390
+ setupDropEvents(wrapper) {
391
+ let dragCounter = 0;
392
+ wrapper.addEventListener("dragenter", (event) => {
393
+ event.preventDefault();
394
+ dragCounter++;
395
+ if (this.dropOverlay) this.dropOverlay.style.display = "flex";
396
+ });
397
+ wrapper.addEventListener("dragleave", (event) => {
398
+ event.preventDefault();
399
+ dragCounter--;
400
+ if (dragCounter <= 0) {
401
+ dragCounter = 0;
402
+ if (this.dropOverlay) this.dropOverlay.style.display = "none";
403
+ }
404
+ });
405
+ wrapper.addEventListener("dragover", (event) => {
406
+ event.preventDefault();
407
+ });
408
+ wrapper.addEventListener("drop", (event) => {
409
+ event.preventDefault();
410
+ dragCounter = 0;
411
+ if (this.dropOverlay) this.dropOverlay.style.display = "none";
412
+ const files = Array.from(event.dataTransfer?.files ?? []);
413
+ if (files.length > 0) {
414
+ this.emitter.emit("toolbar-drop", files);
415
+ this.config.onDrop?.(files);
416
+ }
417
+ });
418
+ }
419
+ handleSend() {
420
+ const content = (this.textareaEl?.value ?? "").trim();
421
+ if (!content) return;
422
+ const mentions = this.mentionEngine.parseMentions(content);
423
+ this.emitter.emit("send", content, mentions);
424
+ this.config.onSend?.(content, mentions);
425
+ if (this.config.clearInputOnSend !== false) this.clearInput();
426
+ this.mentionEngine.resetSelected();
427
+ }
428
+ insertMention(member) {
429
+ if (!this.textareaEl) return;
430
+ const state = this.mentionEngine.getState();
431
+ const text = this.textareaEl.value;
432
+ const pos = this.textareaEl.selectionStart;
433
+ const insertText = this.mentionEngine.getInsertText(member);
434
+ const before = text.slice(0, state.triggerPosition);
435
+ const after = text.slice(pos);
436
+ this.textareaEl.value = this.limitInput(before + insertText + after);
437
+ const newPos = Math.min(before.length + insertText.length, this.textareaEl.value.length);
438
+ this.textareaEl.setSelectionRange(newPos, newPos);
439
+ this.textareaEl.focus();
440
+ this.autoResize();
441
+ this.updateSendButtonState();
442
+ this.config.onInputChange?.(this.textareaEl.value);
443
+ }
444
+ insertTextAtCursor(text) {
445
+ if (!this.textareaEl) return;
446
+ const current = this.textareaEl.value;
447
+ const start = this.textareaEl.selectionStart;
448
+ const end = this.textareaEl.selectionEnd;
449
+ this.textareaEl.value = this.limitInput(current.slice(0, start) + text + current.slice(end));
450
+ const nextCursor = Math.min(start + text.length, this.textareaEl.value.length);
451
+ this.textareaEl.setSelectionRange(nextCursor, nextCursor);
452
+ this.autoResize();
453
+ this.updateSendButtonState();
454
+ this.config.onInputChange?.(this.textareaEl.value);
455
+ }
456
+ autoResize() {
457
+ if (!this.textareaEl) return;
458
+ this.textareaEl.style.height = "auto";
459
+ const maxHeight = this.getCssSizeValue("--ldesign-chat-textarea-max-height", 120);
460
+ this.textareaEl.style.height = `${Math.min(this.textareaEl.scrollHeight, maxHeight)}px`;
461
+ }
462
+ updateSendButtonState() {
463
+ if (!this.sendBtn) return;
464
+ this.sendBtn.disabled = !this.getInputContent().trim();
465
+ }
466
+ renderLoadMoreButton() {
467
+ this.loadMoreEl = this.el("div", `${this.prefix}-load-more`);
468
+ this.loadMoreEl.textContent = this.config.loadMoreText ?? "加载更多";
469
+ this.loadMoreEl.addEventListener("click", () => {
470
+ if (this.loadingMore) return;
471
+ this.setLoadingMore(true);
472
+ this.emitter.emit("load-more");
473
+ Promise.resolve(this.config.onLoadMore?.()).finally(() => {
474
+ this.setLoadingMore(false);
475
+ });
476
+ });
477
+ this.messageListEl.appendChild(this.loadMoreEl);
478
+ }
479
+ renderTimeDivider(timestamp) {
480
+ this.messageListEl.appendChild(this.createTimeDivider(timestamp));
481
+ }
482
+ createTimeDivider(timestamp) {
483
+ const divider = this.el("div", `${this.prefix}-time-divider`);
484
+ const span = document.createElement("span");
485
+ span.textContent = this.formatTime(timestamp);
486
+ divider.appendChild(span);
487
+ return divider;
488
+ }
489
+ renderMessageRow(message) {
490
+ this.messageListEl.appendChild(this.createMessageRow(message));
491
+ }
492
+ createMessageRow(message) {
493
+ const p = this.prefix;
494
+ if (message.type === "system") {
495
+ const row2 = this.el("div", require_dom.cx(`${p}-message-row`, `${p}-message-row--system`));
496
+ row2.dataset.msgId = message.id;
497
+ row2.dataset.timestamp = String(message.timestamp);
498
+ const system = this.el("div", `${p}-system-message`);
499
+ system.textContent = message.content ?? "";
500
+ row2.appendChild(system);
501
+ return row2;
502
+ }
503
+ const isOutgoing = message.direction === "outgoing";
504
+ const row = this.el("div", require_dom.cx(`${p}-message-row`, isOutgoing && `${p}-message-row--outgoing`));
505
+ row.dataset.msgId = message.id;
506
+ row.dataset.timestamp = String(message.timestamp);
507
+ if (this.config.showAvatar !== false) row.appendChild(this.createAvatar(message.sender.nickname, message.sender.avatar));
508
+ const body = this.el("div", `${p}-message-body`);
509
+ if (this.config.showNickname !== false && !isOutgoing) {
510
+ const nickname = this.el("div", `${p}-nickname`);
511
+ nickname.textContent = message.sender.nickname;
512
+ body.appendChild(nickname);
513
+ }
514
+ body.appendChild(this.createBubble(message, isOutgoing));
515
+ body.appendChild(this.createMessageFooter(message, isOutgoing));
516
+ row.appendChild(body);
517
+ row.addEventListener("click", () => {
518
+ this.emitter.emit("message-click", message);
519
+ this.config.onMessageClick?.(message);
520
+ });
521
+ return row;
522
+ }
523
+ createAvatar(name, avatarUrl) {
524
+ const avatar = this.el("div", `${this.prefix}-avatar`);
525
+ if (avatarUrl) {
526
+ const img = document.createElement("img");
527
+ img.src = avatarUrl;
528
+ img.alt = name;
529
+ avatar.appendChild(img);
530
+ } else avatar.textContent = name[0]?.toUpperCase() ?? "";
531
+ return avatar;
532
+ }
533
+ createMessageFooter(message, isOutgoing) {
534
+ const footer = this.el("div", `${this.prefix}-message-footer`);
535
+ if (this.config.showTimestamp !== false) {
536
+ const time = this.el("span", `${this.prefix}-timestamp`);
537
+ time.textContent = this.formatShortTime(message.timestamp);
538
+ footer.appendChild(time);
539
+ }
540
+ if (this.config.showStatus !== false && isOutgoing && message.status) {
541
+ const status = this.el("span", require_dom.cx(`${this.prefix}-status`, `${this.prefix}-status--${message.status}`));
542
+ status.textContent = STATUS_MAP[message.status] ?? "";
543
+ if (message.status === "failed") status.addEventListener("click", (event) => {
544
+ event.stopPropagation();
545
+ this.emitter.emit("message-retry", message);
546
+ });
547
+ footer.appendChild(status);
548
+ }
549
+ return footer;
550
+ }
551
+ createBubble(message, isOutgoing) {
552
+ const p = this.prefix;
553
+ const dirClass = isOutgoing ? `${p}-bubble--outgoing` : `${p}-bubble--incoming`;
554
+ if (message.revoked) {
555
+ const bubble = this.el("div", require_dom.cx(`${p}-bubble`, dirClass, `${p}-bubble--revoked`));
556
+ bubble.textContent = "消息已撤回";
557
+ return bubble;
558
+ }
559
+ switch (message.type) {
560
+ case "image": {
561
+ const bubble = this.el("div", require_dom.cx(`${p}-bubble`, dirClass, `${p}-bubble--image`));
562
+ if (message.image) {
563
+ const img = document.createElement("img");
564
+ img.src = message.image.thumbUrl ?? message.image.url;
565
+ img.alt = message.content ?? "";
566
+ if (message.image.width) img.width = Math.min(message.image.width, 240);
567
+ bubble.appendChild(img);
568
+ }
569
+ return bubble;
570
+ }
571
+ case "file": {
572
+ const bubble = this.el("div", require_dom.cx(`${p}-bubble`, dirClass, `${p}-bubble--file`));
573
+ const icon = this.el("div", `${p}-file-icon`);
574
+ icon.innerHTML = FILE_ICON;
575
+ bubble.appendChild(icon);
576
+ const info = this.el("div", `${p}-file-info`);
577
+ const name = this.el("div", `${p}-file-name`);
578
+ name.textContent = message.file?.name ?? "file";
579
+ info.appendChild(name);
580
+ if (message.file?.size) {
581
+ const size = this.el("div", `${p}-file-size`);
582
+ size.textContent = this.formatFileSize(message.file.size);
583
+ info.appendChild(size);
584
+ }
585
+ bubble.appendChild(info);
586
+ return bubble;
587
+ }
588
+ case "custom": {
589
+ const bubble = this.el("div", require_dom.cx(`${p}-bubble`, dirClass));
590
+ if (message.customHtml) bubble.innerHTML = message.customHtml;
591
+ else bubble.textContent = message.content ?? "";
592
+ return bubble;
593
+ }
594
+ default: {
595
+ const bubble = this.el("div", require_dom.cx(`${p}-bubble`, dirClass, message.streaming && `${p}-bubble--streaming`));
596
+ bubble.innerHTML = this.renderTextContent(message.content ?? "", message.mentions);
597
+ return bubble;
598
+ }
599
+ }
600
+ }
601
+ renderTextContent(content, mentions) {
602
+ let html = this.escapeHtml(content);
603
+ if (mentions?.length) for (const mention of mentions) {
604
+ const tag = `<span class="${this.prefix}-mention-tag" data-mention-id="${this.escapeAttr(mention.id)}">@${this.escapeHtml(mention.name)}</span>`;
605
+ html = html.replace(new RegExp(`@${this.escapeRegex(mention.name)}`, "g"), tag);
606
+ }
607
+ return html.replace(/(https?:\/\/[^\s<]+)/g, `<a class="${this.prefix}-link" href="$1" target="_blank" rel="noopener noreferrer">$1</a>`);
608
+ }
609
+ renderEmpty() {
610
+ if (this.messageListEl.querySelector(`.${this.prefix}-empty`)) return;
611
+ const empty = this.el("div", `${this.prefix}-empty`);
612
+ empty.textContent = "暂无消息";
613
+ this.messageListEl.appendChild(empty);
614
+ }
615
+ removeEmpty() {
616
+ this.messageListEl.querySelector(`.${this.prefix}-empty`)?.remove();
617
+ }
618
+ el(tag, className) {
619
+ const el = document.createElement(tag);
620
+ el.className = className;
621
+ return el;
622
+ }
623
+ getMessageElements() {
624
+ return Array.from(this.messageListEl.querySelectorAll(`.${this.prefix}-message-row`));
625
+ }
626
+ formatTime(timestamp) {
627
+ if (this.config.formatTime) return this.config.formatTime(timestamp);
628
+ const date = new Date(timestamp);
629
+ const now = /* @__PURE__ */ new Date();
630
+ if (date.toDateString() === now.toDateString()) return date.toLocaleTimeString("zh-CN", {
631
+ hour: "2-digit",
632
+ minute: "2-digit"
633
+ });
634
+ return date.toLocaleDateString("zh-CN", {
635
+ month: "2-digit",
636
+ day: "2-digit",
637
+ hour: "2-digit",
638
+ minute: "2-digit"
639
+ });
640
+ }
641
+ formatShortTime(timestamp) {
642
+ return new Date(timestamp).toLocaleTimeString("zh-CN", {
643
+ hour: "2-digit",
644
+ minute: "2-digit"
645
+ });
646
+ }
647
+ formatFileSize(bytes) {
648
+ if (bytes < 1024) return `${bytes} B`;
649
+ if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
650
+ return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
651
+ }
652
+ limitInput(content) {
653
+ const max = this.config.maxInputLength;
654
+ if (!max || max <= 0) return content;
655
+ return content.slice(0, max);
656
+ }
657
+ getCssSizeValue(name, fallback) {
658
+ if (typeof window === "undefined" || !this.root) return fallback;
659
+ const value = window.getComputedStyle(this.root).getPropertyValue(name).trim();
660
+ const parsed = Number.parseFloat(value);
661
+ return Number.isFinite(parsed) ? parsed : fallback;
662
+ }
663
+ escapeHtml(str) {
664
+ const div = document.createElement("div");
665
+ div.textContent = str;
666
+ return div.innerHTML;
667
+ }
668
+ escapeAttr(str) {
669
+ return str.replace(/"/g, "&quot;").replace(/'/g, "&#39;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
670
+ }
671
+ escapeRegex(str) {
672
+ return str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
673
+ }
674
+ };
675
+ //#endregion
676
+ exports.DOMRenderer = DOMRenderer;
677
+ /*! End of @composy/chat-core | Powered by @composy/builder */
678
+ //# sourceMappingURL=dom-renderer.cjs.map