main
js 780 lines 22.9 KB
Raw
1 import { createStore } from "/js/AlpineStore.js";
2 import * as shortcuts from "/js/shortcuts.js";
3 import { store as fileBrowserStore } from "/components/modals/file-browser/file-browser-store.js";
4 import { openLatest as openLatestSurface } from "/js/surfaces.js";
5 import { store as messageQueueStore } from "/components/chat/message-queue/message-queue-store.js";
6 import { store as attachmentsStore } from "/components/chat/attachments/attachmentsStore.js";
7 import { store as chatsStore } from "/components/sidebar/chats/chats-store.js";
8
9 const ICON_MARKER_RE = /icon:\/\/([a-zA-Z0-9_]+)(\[(?:\\.|[^\]])*\])?/g;
10 const FENCE_LINE_RE = /^```([A-Za-z0-9_-]*)?$/;
11 const BLOCK_TAGS = new Set(["DIV", "P", "LI"]);
12 const DRAFT_STORAGE_PREFIX = "a0:chat-draft:";
13
14 function escapeHTML(value) {
15 return String(value ?? "")
16 .replace(/&/g, "&")
17 .replace(/</g, "&lt;")
18 .replace(/>/g, "&gt;")
19 .replace(/"/g, "&quot;")
20 .replace(/'/g, "&#039;");
21 }
22
23 function unescapeIconTooltip(block) {
24 if (!block) return "";
25 return block
26 .slice(1, -1)
27 .replace(/\\\[/g, "[")
28 .replace(/\\\]/g, "]")
29 .replace(/\\\\/g, "\\");
30 }
31
32 function convertIconMarkersToHtml(value) {
33 const text = String(value ?? "");
34 let html = "";
35 let lastIndex = 0;
36
37 text.replace(ICON_MARKER_RE, (match, iconName, tooltipBlock, offset) => {
38 html += escapeHTML(text.slice(lastIndex, offset));
39 const tooltip = unescapeIconTooltip(tooltipBlock) || iconName;
40 html += (
41 `<x-icon class="icon chat-input-progress-icon" ` +
42 `title="${escapeHTML(tooltip)}" name="${escapeHTML(iconName)}"></x-icon>`
43 );
44 lastIndex = offset + match.length;
45 return match;
46 });
47
48 html += escapeHTML(text.slice(lastIndex));
49 return html;
50 }
51
52 const model = {
53 paused: false,
54 _message: "",
55 _editorEl: null,
56 _history: [],
57 _historyIndex: null,
58 _draft: "",
59 _historyCtxid: null,
60 _draftCtxid: null,
61 /** Composer + menu (bottom actions moved into dropdown) */
62 chatMoreMenuOpen: false,
63 progressText: "",
64 progressActive: false,
65
66 get message() {
67 return this._message;
68 },
69
70 set message(value) {
71 this._message = String(value ?? "");
72 this._renderEditorFromText(this._message);
73 this._saveDraft();
74 },
75
76 toggleChatMoreMenu() {
77 this.chatMoreMenuOpen = !this.chatMoreMenuOpen;
78 },
79
80 closeChatMoreMenu() {
81 this.chatMoreMenuOpen = false;
82 },
83
84 _getSendState() {
85 const hasInput = this.message.trim() || attachmentsStore?.attachments?.length > 0;
86 const hasQueue = !!messageQueueStore?.hasQueue;
87 const running = !!chatsStore.selectedContext?.running;
88
89 if (running && !hasInput) return "stop";
90 if (hasQueue && !hasInput) return "all";
91 if ((running || hasQueue) && hasInput) return "queue";
92 return "normal";
93 },
94
95 get inputPlaceholder() {
96 if (!chatsStore.selected) return "Ask anything to start a new chat";
97 const state = this._getSendState();
98 if ((state === "all" || state === "stop") && messageQueueStore?.hasQueue) {
99 return "Press Enter to send queued messages";
100 }
101 if (this.showProgressPlaceholder) return "";
102 return "Type your message here...";
103 },
104
105 get showProgressPlaceholder() {
106 const state = this._getSendState();
107 return (
108 !!chatsStore.selected &&
109 state !== "all" &&
110 !(state === "stop" && messageQueueStore?.hasQueue) &&
111 !!this.progressText &&
112 !this.message
113 );
114 },
115
116 get progressPlaceholderHtml() {
117 return convertIconMarkersToHtml(this.progressText);
118 },
119
120 // Computed: send button icon type
121 get sendButtonIcon() {
122 const state = this._getSendState();
123 if (state === "stop") return "stop";
124 if (state === "all") return "send_and_archive";
125 if (state === "queue") return "schedule_send";
126 return "arrow_forward";
127 },
128
129 // Computed: send button CSS class
130 get sendButtonClass() {
131 const state = this._getSendState();
132 if (state === "stop") return "stop";
133 if (state === "all") return "send-queue send-all";
134 if (state === "queue") return "send-queue queue";
135 return "";
136 },
137
138 // Computed: send button title
139 get sendButtonTitle() {
140 const state = this._getSendState();
141 if (state === "stop") return "Stop agent";
142 if (state === "all") return "Send all queued messages";
143 if (state === "queue") return "Add to queue";
144 return "Send message";
145 },
146
147 init() {
148 console.log("Input store initialized");
149 // Event listeners are now handled via Alpine directives in the component
150 },
151
152 async sendMessage() {
153 this._syncMessageFromEditor();
154 const pendingMessage = this.message;
155
156 if (!chatsStore.selected && (this.message.trim() || attachmentsStore?.attachments?.length > 0)) {
157 const ctxid = await chatsStore.newChat();
158 if (!ctxid && !chatsStore.selected) return;
159 this.message = pendingMessage;
160 }
161
162 // Capture sent prompt to per-chat history (bash-style)
163 try { this._pushHistory(this.message); } catch (_e) { /* ignore */ }
164
165 // Delegate to the global function
166 if (globalThis.sendMessage) {
167 await globalThis.sendMessage();
168 }
169 },
170
171 async activateSendButton() {
172 this._syncMessageFromEditor();
173 if (this._getSendState() === "stop") {
174 await this.stopAgent();
175 return;
176 }
177 await this.sendMessage();
178 },
179
180 mountEditor(editor) {
181 this._editorEl = editor;
182 this.setDraftContext(shortcuts.getCurrentContextId());
183 this._renderEditorFromText(this._message);
184 this.adjustTextareaHeight({ target: editor });
185 },
186
187 unmountEditor(editor) {
188 if (this._editorEl === editor) this._editorEl = null;
189 },
190
191 _composerTextareas(target = null) {
192 const editor = target?.closest?.("#chat-input") || this._editorEl || document.getElementById("chat-input");
193 return editor ? [editor] : [];
194 },
195
196 _activeTextarea() {
197 const active = document.activeElement;
198 const activeEditor = active?.closest?.("#chat-input");
199 if (activeEditor) {
200 return activeEditor;
201 }
202 return this._editorEl || document.getElementById("chat-input");
203 },
204
205 _renderEditorFromText(text) {
206 const editor = this._editorEl;
207 if (!editor) return;
208 if (editor.textContent !== text || editor.querySelector("[data-code-block]")) {
209 editor.textContent = text;
210 }
211 this._setEditorEmptyState();
212 },
213
214 _setEditorEmptyState() {
215 const editor = this._editorEl;
216 if (editor) editor.classList.toggle("is-empty", !this._message);
217 },
218
219 _createCodeBlock(lang = "") {
220 const block = document.createElement("div");
221 block.className = "composer-code-block";
222 block.dataset.codeBlock = "true";
223 block.dataset.lang = lang;
224 block.contentEditable = "false";
225
226 const code = document.createElement("pre");
227 code.className = "composer-code-content";
228 code.dataset.codeContent = "true";
229 code.contentEditable = "true";
230 code.spellcheck = false;
231 code.setAttribute("aria-label", "Code block");
232 block.append(code);
233
234 return block;
235 },
236
237 _plainText(node) {
238 if (!node) return "";
239 if (node.nodeType === Node.TEXT_NODE) return node.nodeValue || "";
240 if (node.nodeType !== Node.ELEMENT_NODE) return "";
241 if (node.matches?.("[data-code-block]")) {
242 const lang = node.dataset.lang || "";
243 const code = this._plainText(node.querySelector("[data-code-content]")).replace(/\n$/, "");
244 return "```" + lang + "\n" + code + "\n```";
245 }
246 if (node.tagName === "BR") return "\n";
247
248 let text = "";
249 for (const child of node.childNodes) text += this._plainText(child);
250 if (node !== this._editorEl && BLOCK_TAGS.has(node.tagName) && text && !text.endsWith("\n")) {
251 text += "\n";
252 }
253 return text;
254 },
255
256 _editorToMarkdown() {
257 return this._plainText(this._editorEl).replace(/\n$/, "");
258 },
259
260 _syncMessageFromEditor() {
261 if (!this._editorEl) return;
262 this._message = this._editorToMarkdown();
263 this._setEditorEmptyState();
264 this._saveDraft();
265 },
266
267 _isInCodeBlock(target) {
268 return Boolean(target?.closest?.("[data-code-content]"));
269 },
270
271 _selectionOffsets(editor) {
272 const selection = document.getSelection?.();
273 if (!selection || selection.rangeCount === 0 || !editor) {
274 return { start: this._message.length, end: this._message.length };
275 }
276 const range = selection.getRangeAt(0);
277 if (!editor.contains(range.startContainer) || !editor.contains(range.endContainer)) {
278 return { start: this._message.length, end: this._message.length };
279 }
280
281 const before = range.cloneRange();
282 before.selectNodeContents(editor);
283 before.setEnd(range.startContainer, range.startOffset);
284
285 const selected = range.cloneRange();
286 return {
287 start: before.toString().length,
288 end: before.toString().length + selected.toString().length,
289 };
290 },
291
292 _setEditorCaret(offset) {
293 const editor = this._activeTextarea();
294 const selection = document.getSelection?.();
295 if (!editor || !selection) return;
296
297 editor.focus();
298 const range = document.createRange();
299 let remaining = Math.max(0, Number(offset) || 0);
300 const walker = document.createTreeWalker(editor, NodeFilter.SHOW_TEXT);
301 let node = walker.nextNode();
302
303 while (node) {
304 const length = node.nodeValue.length;
305 if (remaining <= length) {
306 range.setStart(node, remaining);
307 range.collapse(true);
308 selection.removeAllRanges();
309 selection.addRange(range);
310 return;
311 }
312 remaining -= length;
313 node = walker.nextNode();
314 }
315
316 range.selectNodeContents(editor);
317 range.collapse(false);
318 selection.removeAllRanges();
319 selection.addRange(range);
320 },
321
322 _focusCodeBlock(block) {
323 queueMicrotask(() => {
324 const code = block.querySelector("[data-code-content]");
325 const selection = document.getSelection?.();
326 if (!code || !selection) return;
327 code.focus();
328 const range = document.createRange();
329 range.selectNodeContents(code);
330 range.collapse(false);
331 selection.removeAllRanges();
332 selection.addRange(range);
333 });
334 },
335
336 _insertPlainText(text) {
337 const selection = document.getSelection?.();
338 if (!selection || selection.rangeCount === 0) return false;
339 const range = selection.getRangeAt(0);
340 range.deleteContents();
341 const node = document.createTextNode(text);
342 range.insertNode(node);
343 range.setStart(node, node.nodeValue.length);
344 range.collapse(true);
345 selection.removeAllRanges();
346 selection.addRange(range);
347 return true;
348 },
349
350 _tryCreateCodeBlock($event) {
351 const editor = this._activeTextarea();
352 const selection = document.getSelection?.();
353 if (!editor || !selection || selection.rangeCount === 0) return false;
354
355 const range = selection.getRangeAt(0);
356 if (!range.collapsed || !editor.contains(range.startContainer)) return false;
357 if (this._isInCodeBlock(range.startContainer.parentElement)) return false;
358
359 const before = range.cloneRange();
360 before.selectNodeContents(editor);
361 before.setEnd(range.startContainer, range.startOffset);
362
363 const after = range.cloneRange();
364 after.selectNodeContents(editor);
365 after.setStart(range.startContainer, range.startOffset);
366
367 const lineBefore = before.toString().split("\n").pop() || "";
368 const lineAfter = (after.toString().split("\n")[0] || "");
369 const match = (lineBefore + lineAfter).match(FENCE_LINE_RE);
370 if (!match || lineAfter) return false;
371 if (range.startContainer.nodeType !== Node.TEXT_NODE || range.startOffset < lineBefore.length) return false;
372
373 $event.preventDefault();
374 const editRange = range.cloneRange();
375 editRange.setStart(range.startContainer, range.startOffset - lineBefore.length);
376 editRange.deleteContents();
377
378 const block = this._createCodeBlock(match[1] || "");
379 editRange.insertNode(block);
380 block.after(document.createTextNode("\n"));
381 this._focusCodeBlock(block);
382 this._syncMessageFromEditor();
383 this.adjustTextareaHeight({ target: editor });
384 return true;
385 },
386
387 handleInput($event) {
388 this._syncMessageFromEditor();
389 this.adjustTextareaHeight($event);
390 },
391
392 handlePaste($event) {
393 const text = $event.clipboardData?.getData("text/plain");
394 if (text === undefined) return;
395 $event.preventDefault();
396 this._insertPlainText(text);
397 this.handleInput($event);
398 },
399
400 handleKeydown($event) {
401 if ($event.isComposing || $event.keyCode === 229) return;
402
403 if ($event.key === "Enter") {
404 if (this._isInCodeBlock($event.target)) return;
405 if (!$event.shiftKey) {
406 if (this._tryCreateCodeBlock($event)) return;
407 $event.preventDefault();
408 this.sendMessage();
409 }
410 return;
411 }
412
413 if ($event.key === "ArrowUp") this.historyPrev($event);
414 if ($event.key === "ArrowDown") this.historyNext($event);
415 },
416
417 adjustTextareaHeight($event = null) {
418 const target = $event?.target || null;
419 for (const chatInput of this._composerTextareas(target)) {
420 chatInput.style.height = "auto";
421 chatInput.style.height = chatInput.scrollHeight + "px";
422 // pick up any layout shift triggered by the height assignment
423 chatInput.style.height = Math.max(chatInput.scrollHeight, parseInt(chatInput.style.height)) + "px";
424 }
425 },
426
427 async pauseAgent(paused) {
428 const prev = this.paused;
429 this.paused = paused;
430 try {
431 const context = globalThis.getContext?.();
432 if (!globalThis.sendJsonData)
433 throw new Error("sendJsonData not available");
434 await globalThis.sendJsonData("/pause", { paused, context });
435 } catch (e) {
436 this.paused = prev;
437 if (globalThis.toastFetchError) {
438 globalThis.toastFetchError("Error pausing agent", e);
439 }
440 }
441 },
442
443 async stopAgent() {
444 try {
445 const context = globalThis.getContext?.();
446 if (!context || !globalThis.sendJsonData) return;
447 await globalThis.sendJsonData("/stop", { context });
448 } catch (e) {
449 if (globalThis.toastFetchError) {
450 globalThis.toastFetchError("Error stopping agent", e);
451 }
452 }
453 },
454
455 async nudge() {
456 try {
457 const context = globalThis.getContext();
458 await globalThis.sendJsonData("/nudge", { ctxid: context });
459 } catch (e) {
460 if (globalThis.toastFetchError) {
461 globalThis.toastFetchError("Error nudging agent", e);
462 }
463 }
464 },
465
466 async loadKnowledge() {
467 try {
468 const resp = await shortcuts.callJsonApi(
469 "/plugins/_memory/knowledge_path_get",
470 { ctxid: shortcuts.getCurrentContextId() }
471 );
472 if (!resp.ok) throw new Error("Error getting knowledge path");
473 const path = resp.path;
474
475 // open file browser and wait for it to close
476 await fileBrowserStore.open(path);
477
478 // progress notification
479 shortcuts.frontendNotification({
480 type: shortcuts.NotificationType.PROGRESS,
481 message: "Loading knowledge...",
482 priority: shortcuts.NotificationPriority.NORMAL,
483 displayTime: 999,
484 group: "knowledge_load",
485 frontendOnly: true,
486 });
487
488 // then reindex knowledge
489 await globalThis.sendJsonData("/plugins/_memory/knowledge_reindex", {
490 ctxid: shortcuts.getCurrentContextId(),
491 });
492
493 // finished notification
494 shortcuts.frontendNotification({
495 type: shortcuts.NotificationType.SUCCESS,
496 message: "Knowledge loaded successfully",
497 priority: shortcuts.NotificationPriority.NORMAL,
498 displayTime: 2,
499 group: "knowledge_load",
500 frontendOnly: true,
501 });
502 } catch (e) {
503 // error notification
504 shortcuts.frontendNotification({
505 type: shortcuts.NotificationType.ERROR,
506 message: "Error loading knowledge",
507 priority: shortcuts.NotificationPriority.NORMAL,
508 displayTime: 5,
509 group: "knowledge_load",
510 frontendOnly: true,
511 });
512 }
513 },
514
515 // previous implementation without projects
516 async _loadKnowledge() {
517 const input = document.createElement("input");
518 input.type = "file";
519 input.accept = ".txt,.pdf,.csv,.html,.json,.md";
520 input.multiple = true;
521
522 input.onchange = async () => {
523 try {
524 const formData = new FormData();
525 for (let file of input.files) {
526 formData.append("files[]", file);
527 }
528
529 formData.append("ctxid", globalThis.getContext());
530
531 const response = await globalThis.fetchApi("/import_knowledge", {
532 method: "POST",
533 body: formData,
534 });
535
536 if (!response.ok) {
537 if (globalThis.toast)
538 globalThis.toast(await response.text(), "error");
539 } else {
540 const data = await response.json();
541 if (globalThis.toast) {
542 globalThis.toast(
543 "Knowledge files imported: " + data.filenames.join(", "),
544 "success"
545 );
546 }
547 }
548 } catch (e) {
549 if (globalThis.toastFetchError) {
550 globalThis.toastFetchError("Error loading knowledge", e);
551 }
552 }
553 };
554
555 input.click();
556 },
557
558 async browseFiles(path) {
559 if (!path) {
560 const ctxid = shortcuts.getCurrentContextId();
561
562 if (ctxid) {
563 try {
564 const resp = await shortcuts.callJsonApi("/chat_files_path_get", {
565 ctxid,
566 });
567 if (resp.ok) path = resp.path;
568 } catch (_e) {
569 console.error("Error getting chat files path", _e);
570 }
571 }
572 }
573 let opened = false;
574 try {
575 opened = await openLatestSurface("files", { path, source: "sidebar" });
576 } catch (error) {
577 console.error("Error opening Files surface", error);
578 }
579 if (!opened) await fileBrowserStore.open(path);
580 },
581
582 focus() {
583 const chatInput = this._activeTextarea();
584 if (chatInput) {
585 chatInput.focus();
586 }
587 },
588
589 setDraftContext(ctxid) {
590 const nextCtxid = String(ctxid || "");
591 if (nextCtxid === this._draftCtxid) return;
592 if (this._draftCtxid !== null) this._syncMessageFromEditor();
593
594 this._draftCtxid = nextCtxid;
595 this._historyIndex = null;
596 this._draft = "";
597
598 let draft = "";
599 if (nextCtxid) {
600 try { draft = sessionStorage.getItem(DRAFT_STORAGE_PREFIX + nextCtxid) || ""; } catch (_e) { /* ignore */ }
601 }
602 this._message = draft;
603 this._renderEditorFromText(draft);
604 queueMicrotask(() => this.adjustTextareaHeight());
605 },
606
607 _saveDraft() {
608 if (!this._draftCtxid) return;
609 try {
610 const key = DRAFT_STORAGE_PREFIX + this._draftCtxid;
611 if (this._message) sessionStorage.setItem(key, this._message);
612 else sessionStorage.removeItem(key);
613 } catch (_e) { /* ignore unavailable storage */ }
614 },
615
616 _loadHistory() {
617 let ctxid = null;
618 try { ctxid = shortcuts.getCurrentContextId(); } catch (_e) { ctxid = null; }
619 this._historyCtxid = ctxid;
620 this._history = [];
621 this._historyIndex = null;
622 this._draft = "";
623 if (!ctxid) return;
624 let raw = null;
625 try { raw = localStorage.getItem("a0:chat-history:" + ctxid); } catch (_e) { raw = null; }
626 if (raw !== null) {
627 try {
628 const arr = JSON.parse(raw);
629 if (Array.isArray(arr)) {
630 this._history = arr.filter((s) => typeof s === "string");
631 }
632 } catch (_e) { /* ignore */ }
633 return;
634 }
635 // No entry yet for this chat: seed from rendered chat DOM (one-time bootstrap)
636 try {
637 const seeded = this._seedFromChatDom();
638 if (seeded.length > 0) {
639 this._history = seeded;
640 this._saveHistory();
641 } else {
642 // Persist an empty array so we don't re-seed on every nav; respects user clearing
643 this._saveHistory();
644 }
645 } catch (_e) { /* ignore */ }
646 },
647
648 _seedFromChatDom() {
649 const out = [];
650 let nodes;
651 try {
652 nodes = document.querySelectorAll(".user-container .message-user .message-text pre");
653 } catch (_e) {
654 return out;
655 }
656 for (const pre of nodes) {
657 const text = (pre.textContent || "").trim();
658 if (!text) continue;
659 if (out.length > 0 && out[out.length - 1] === text) continue; // ignoredups
660 out.push(text);
661 }
662 if (out.length > 50) return out.slice(-50);
663 return out;
664 },
665
666 _saveHistory() {
667 if (!this._historyCtxid) return;
668 try {
669 localStorage.setItem(
670 "a0:chat-history:" + this._historyCtxid,
671 JSON.stringify(this._history)
672 );
673 } catch (_e) { /* ignore quota / disabled */ }
674 },
675
676 _ensureHistoryLoaded() {
677 let ctxid = null;
678 try { ctxid = shortcuts.getCurrentContextId(); } catch (_e) { ctxid = null; }
679 if (ctxid !== this._historyCtxid) {
680 this._loadHistory();
681 }
682 },
683
684 _pushHistory(text) {
685 if (typeof text !== "string") return;
686 const trimmed = text.trim();
687 if (!trimmed) return;
688 this._ensureHistoryLoaded();
689 if (this._history.length > 0 && this._history[this._history.length - 1] === trimmed) {
690 this._historyIndex = null;
691 this._draft = "";
692 return;
693 }
694 this._history.push(trimmed);
695 if (this._history.length > 50) {
696 this._history = this._history.slice(-50);
697 }
698 this._saveHistory();
699 this._historyIndex = null;
700 this._draft = "";
701 },
702
703 _setCaretStart() {
704 queueMicrotask(() => {
705 const editor = this._activeTextarea();
706 if (editor) {
707 this._setEditorCaret(0);
708 try { editor.scrollTop = 0; } catch (_e) { /* ignore */ }
709 }
710 this.adjustTextareaHeight();
711 });
712 },
713
714 _setCaretEnd() {
715 queueMicrotask(() => {
716 const editor = this._activeTextarea();
717 if (editor) {
718 this._setEditorCaret(editor.innerText.length);
719 try { editor.scrollTop = editor.scrollHeight; } catch (_e) { /* ignore */ }
720 }
721 this.adjustTextareaHeight();
722 });
723 },
724
725 historyPrev($event) {
726 if ($event && ($event.isComposing || $event.keyCode === 229)) return;
727 if (this._isInCodeBlock($event?.target)) return;
728 const editor = this._activeTextarea();
729 if (!editor) return;
730 const { start, end } = this._selectionOffsets(editor);
731 if (start !== 0 || end !== 0) return;
732 $event.preventDefault();
733 this._ensureHistoryLoaded();
734 if (this._history.length === 0) return;
735 if (this._historyIndex === null) {
736 this._draft = this.message || "";
737 this._historyIndex = this._history.length - 1;
738 } else if (this._historyIndex > 0) {
739 this._historyIndex -= 1;
740 } else {
741 return;
742 }
743 this.message = this._history[this._historyIndex];
744 this._setCaretStart();
745 },
746
747 historyNext($event) {
748 if ($event && ($event.isComposing || $event.keyCode === 229)) return;
749 if (this._isInCodeBlock($event?.target)) return;
750 const editor = this._activeTextarea();
751 if (!editor) return;
752 const { start, end } = this._selectionOffsets(editor);
753 const valueLength = editor.innerText.length;
754 if (start !== valueLength || end !== valueLength) return;
755 $event.preventDefault();
756 if (this._historyIndex === null) return;
757 if (this._historyIndex < this._history.length - 1) {
758 this._historyIndex += 1;
759 this.message = this._history[this._historyIndex];
760 } else {
761 this._historyIndex = null;
762 this.message = this._draft || "";
763 this._draft = "";
764 }
765 this._setCaretEnd();
766 },
767
768 reset() {
769 this.message = "";
770 attachmentsStore.clearAttachments();
771 this.chatMoreMenuOpen = false;
772 this._historyIndex = null;
773 this._draft = "";
774 this.adjustTextareaHeight();
775 }
776 };
777
778 const store = createStore("chatInput", model);
779
780 export { store };