diff --git a/src/components/ChatPanel.ts b/src/components/ChatPanel.ts index 77d2aee..83aa5c4 100644 --- a/src/components/ChatPanel.ts +++ b/src/components/ChatPanel.ts @@ -4,7 +4,6 @@ */ import { ChatHistoryItem } from './models/ChatHistoryItem'; - import { AutocompleteManager } from '../references/AutocompleteManager'; import { ReferenceAutocomplete } from '../references/ReferenceAutocomplete'; import { ReferenceExpander } from '../references/ReferenceExpander'; @@ -81,30 +80,35 @@ export class ChatPanel { // Показываем дропдаун, чтобы получить его размеры и положение this.modelDropdown.style.display = 'block'; - const modelSelectButtonRect = this.modelSelectButton.getBoundingClientRect(); - const modelDropdownRect = this.modelDropdown.getBoundingClientRect(); - const chatPanelRect = this.container.getBoundingClientRect(); // Общие границы чат-панели - const viewportHeight = window.innerHeight || document.documentElement.clientHeight; + const container = this.modelSelectButton.closest('.chat-input-container') as HTMLElement; - // ----------------------------------------------- Секция логики позиционирования дропдауна ---------------------------------------------------- - // Позиция относительно контейнера chatPanel (или inputElement) - let top = modelSelectButtonRect.bottom - chatPanelRect.top + 4; // Отступ 4px от кнопки - let left = modelSelectButtonRect.left - chatPanelRect.left; + const modelSelectButtonRect = this.modelSelectButton.getBoundingClientRect(); + const containerRect = container.getBoundingClientRect(); // Общие границы чат-панели + const dropdownRect = this.modelDropdown.getBoundingClientRect(); + const viewportHeight = window.innerHeight; + + // Позиция относительно контейнера chatPanel + // Левый край дропдауна совпадает с левым краем кнопки выбора модели + let left = modelSelectButtonRect.left - containerRect.left - 5; + + // Дефолтное положение - под кнопкой + let top = modelSelectButtonRect.height; // Проверяем, помещается ли dropdown снизу const spaceBelow = viewportHeight - modelSelectButtonRect.bottom; const spaceAbove = modelSelectButtonRect.top; - if (spaceBelow < modelDropdownRect.height + 10 && spaceAbove > modelDropdownRect.height + 10) { + if (spaceBelow < dropdownRect.height + 10 && spaceAbove > dropdownRect.height + 10) { // Если снизу мало места и сверху достаточно, размещаем сверху - top = modelSelectButtonRect.top - chatPanelRect.top - modelDropdownRect.height - 4; // Отступ 4px от кнопки + top = - dropdownRect.height; } // Убедимся, что дропдаун не выходит за границы контейнера справа - const maxRight = chatPanelRect.width; - if (left + modelDropdownRect.width > maxRight) { - left = maxRight - modelDropdownRect.width; + const maxRight = containerRect.right - containerRect.left; + if (left + dropdownRect.width > maxRight) { + left = maxRight - dropdownRect.width - 5; } + // Убедимся, что дропдаун не выходит за границы контейнера слева if (left < 0) { left = 0; @@ -113,7 +117,11 @@ export class ChatPanel { this.modelDropdown.style.top = `${top}px`; this.modelDropdown.style.left = `${left}px`; this.modelDropdown.style.right = 'auto'; // Сброс right, чтобы left работал корректно - // ----------------------------------------------- End of Секция логики позиционирования дропдауна --------------------------------------------- + + // ДОБАВЛЕНО: Устанавливаем минимальную ширину дропдауна, равную ширине кнопки + let minWidth = Math.min(containerRect.width / 2, dropdownRect.width); + this.modelDropdown.style.minWidth = `${minWidth}px`; + this.modelDropdown.style.width = 'auto'; // Позволяем контенту определить ширину, но не меньше minWidth } updateModelDropdown(): void { @@ -435,232 +443,228 @@ export class ChatPanel { /** * Заменяет текст ссылки на визуальный бокс */ - private replaceReferenceWithBox(match: ReferenceMatch, item: AutocompleteItem): void { - this.isProcessingReference = true; + private replaceReferenceWithBox(match: ReferenceMatch, item: AutocompleteItem): void { + this.isProcessingReference = true; - const reference: FileReference = { - type: item.type, - name: item.name, - path: item.path, - permanent: match.prefix.length === 2 // @@ или ## - }; + const reference: FileReference = { + type: item.type, + name: item.name, + path: item.path, + permanent: match.prefix.length === 2 // @@ или ## + }; - // Создаем бокс - const box = ReferenceBox.createBox(reference); + // Создаем бокс + const box = ReferenceBox.createBox(reference); - // Используем текущий Range для замены - const selection = window.getSelection(); - if (!selection || selection.rangeCount === 0) { - this.isProcessingReference = false; - return; - } - - const currentRange = selection.getRangeAt(0); - - // Создаем новый диапазон для замены, основываясь на match.startIndex и match.endIndex - // Необходимо получить текстовые узлы, соответствующие этим индексам - const { startNode, startOffset, endNode, endOffset } = this.findNodesAndOffsetsForMatch(match); + // Используем текущий Range для замены + const selection = window.getSelection(); + if (!selection || selection.rangeCount === 0) { + this.isProcessingReference = false; + return; + } - if (!startNode || !endNode) { - console.error("Не удалось найти узлы для замены ссылки", match); - this.isProcessingReference = false; - return; - } + const currentRange = selection.getRangeAt(0); - const replacementRange = document.createRange(); - replacementRange.setStart(startNode, startOffset); - replacementRange.setEnd(endNode, endOffset); + // Создаем новый диапазон для замены, основываясь на match.startIndex и match.endIndex + // Необходимо получить текстовые узлы, соответствующие этим индексам + const { startNode, startOffset, endNode, endOffset } = this.findNodesAndOffsetsForMatch(match); - replacementRange.deleteContents(); // Удаляем текст ссылки - replacementRange.insertNode(box); // Вставляем бокс + if (!startNode || !endNode) { + console.error('Не удалось найти узлы для замены ссылки', match); + this.isProcessingReference = false; + return; + } - // Очищаем выделение - selection.removeAllRanges(); + const replacementRange = document.createRange(); + replacementRange.setStart(startNode, startOffset); + replacementRange.setEnd(endNode, endOffset); - // Добавляем ссылку в список текущих ссылок - this.currentReferences.push(reference); + replacementRange.deleteContents(); // Удаляем текст ссылки + replacementRange.insertNode(box); // Вставляем бокс - this.isProcessingReference = false; + // Очищаем выделение + selection.removeAllRanges(); - // Вставляем пробел после бокса и перемещаем курсор за него - this.insertSpaceAndSetCursorAfterElement(box); - } + // Добавляем ссылку в список текущих ссылок + this.currentReferences.push(reference); - /** - * Находит узлы и смещения для заданного match в contenteditable элементе. - * @param match - объект ReferenceMatch. - * @returns Объект с startNode, startOffset, endNode, endOffset. - */ - private findNodesAndOffsetsForMatch(match: ReferenceMatch): { - startNode: Node | null, - startOffset: number, - endNode: Node | null, - endOffset: number - } { - let startNode: Node | null = null; - let startOffset = 0; - let endNode: Node | null = null; - let endOffset = 0; - let currentLength = 0; + this.isProcessingReference = false; - const walker = document.createTreeWalker(this.messageInput, NodeFilter.SHOW_ALL); // SHOW_ALL, чтобы учитывать и элементы - let node; + // Вставляем пробел после бокса и перемещаем курсор за него + this.insertSpaceAndSetCursorAfterElement(box); + } - while ((node = walker.nextNode())) { - const nodeTextLength = this.getNodeEquivalentLengthForReferenceParsing(node); + /** + * Находит узлы и смещения для заданного match в contenteditable элементе. + * @param match - объект ReferenceMatch. + * @returns Объект с startNode, startOffset, endNode, endOffset. + */ + private findNodesAndOffsetsForMatch(match: ReferenceMatch): { + startNode: Node | null; + startOffset: number; + endNode: Node | null; + endOffset: number; + } { + let startNode: Node | null = null; + let startOffset = 0; + let endNode: Node | null = null; + let endOffset = 0; + let currentLength = 0; - // Проверяем, начинается ли ссылка в этом узле - if (currentLength <= match.startIndex && match.startIndex < currentLength + nodeTextLength) { - startNode = node; - startOffset = match.startIndex - currentLength; - } + const walker = document.createTreeWalker(this.messageInput, NodeFilter.SHOW_ALL); // SHOW_ALL, чтобы учитывать и элементы + let node; - // Проверяем, заканчивается ли ссылка в этом узле - if (currentLength <= match.endIndex && match.endIndex <= currentLength + nodeTextLength) { - endNode = node; - endOffset = match.endIndex - currentLength; - break; // Нашли оба, выходим - } - - // Если ссылка охватывает несколько узлов, может быть, endNode еще не найден, - // но мы уже прошли startNode. - if (startNode && !endNode && match.endIndex <= currentLength + nodeTextLength) { - // Это случай, когда endMatch.endIndex может быть на границе или внутри следующего узла - endNode = node; - endOffset = match.endIndex - currentLength; - break; - } + while ((node = walker.nextNode())) { + const nodeTextLength = this.getNodeEquivalentLengthForReferenceParsing(node); + // Проверяем, начинается ли ссылка в этом узле + if (currentLength <= match.startIndex && match.startIndex < currentLength + nodeTextLength) { + startNode = node; + startOffset = match.startIndex - currentLength; + } - currentLength += nodeTextLength; - } + // Проверяем, заканчивается ли ссылка в этом узле + if (currentLength <= match.endIndex && match.endIndex <= currentLength + nodeTextLength) { + endNode = node; + endOffset = match.endIndex - currentLength; + break; // Нашли оба, выходим + } - // Если endNode не найден, возможно, match.endIndex указывает на конец inputElement - if (!endNode && startNode && currentLength <= match.endIndex) { - if (this.messageInput.lastChild) { - endNode = this.messageInput.lastChild; - endOffset = this.messageInput.lastChild.textContent?.length || 0; // Или конец всего контента - } - } + // Если ссылка охватывает несколько узлов, может быть, endNode еще не найден, + // но мы уже прошли startNode. + if (startNode && !endNode && match.endIndex <= currentLength + nodeTextLength) { + // Это случай, когда endMatch.endIndex может быть на границе или внутри следующего узла + endNode = node; + endOffset = match.endIndex - currentLength; + break; + } + currentLength += nodeTextLength; + } - return { startNode, startOffset, endNode, endOffset }; - } + // Если endNode не найден, возможно, match.endIndex указывает на конец inputElement + if (!endNode && startNode && currentLength <= match.endIndex) { + if (this.messageInput.lastChild) { + endNode = this.messageInput.lastChild; + endOffset = this.messageInput.lastChild.textContent?.length || 0; // Или конец всего контента + } + } - /** - * Вычисляет эквивалентную текстовую длину узла для целей парсинга ссылок. - * Это отличается от getNodeEquivalentLength, так как здесь reference-box уже считается как его текстовое представление, - * а не 0, так как мы ищем где находится match. - * @param node - узел DOM. - * @returns эквивалентная текстовая длина узла. - */ - private getNodeEquivalentLengthForReferenceParsing(node: Node): number { - if (node.nodeType === Node.TEXT_NODE) { - return node.textContent?.length || 0; - } else if (node.nodeType === Node.ELEMENT_NODE && (node as HTMLElement).classList.contains('reference-box')) { - const reference = ReferenceBox.extractReference(node as HTMLElement); - if (reference) { - // Если это reference-box, его "текстовая длина" для парсинга - это его исходная @/@@#name - return (reference.permanent ? 2 : 1) + reference.name.length; - } - } - return 0; // Другие элементы не влияют на текстовую длину для этого контекста - } + return { startNode, startOffset, endNode, endOffset }; + } - /** - * Вставляет пробел после указанного элемента и устанавливает курсор. - * @param element - HTML элемент, после которого нужно вставить пробел и установить курсор. - */ - private insertSpaceAndSetCursorAfterElement(element: HTMLElement): void { - const selection = window.getSelection(); - if (!selection) return; + /** + * Вычисляет эквивалентную текстовую длину узла для целей парсинга ссылок. + * Это отличается от getNodeEquivalentLength, так как здесь reference-box уже считается как его текстовое представление, + * а не 0, так как мы ищем где находится match. + * @param node - узел DOM. + * @returns эквивалентная текстовая длина узла. + */ + private getNodeEquivalentLengthForReferenceParsing(node: Node): number { + if (node.nodeType === Node.TEXT_NODE) { + return node.textContent?.length || 0; + } else if (node.nodeType === Node.ELEMENT_NODE && (node as HTMLElement).classList.contains('reference-box')) { + const reference = ReferenceBox.extractReference(node as HTMLElement); + if (reference) { + // Если это reference-box, его "текстовая длина" для парсинга - это его исходная @/@@#name + return (reference.permanent ? 2 : 1) + reference.name.length; + } + } + return 0; // Другие элементы не влияют на текстовую длину для этого контекста + } - const range = document.createRange(); - range.setStartAfter(element); - range.collapse(true); // Схлопываем диапазон до одной точки + /** + * Вставляет пробел после указанного элемента и устанавливает курсор. + * @param element - HTML элемент, после которого нужно вставить пробел и установить курсор. + */ + private insertSpaceAndSetCursorAfterElement(element: HTMLElement): void { + const selection = window.getSelection(); + if (!selection) return; - // Вставляем неразрывный пробел - const spaceNode = document.createTextNode('\u00A0'); - range.insertNode(spaceNode); + const range = document.createRange(); + range.setStartAfter(element); + range.collapse(true); // Схлопываем диапазон до одной точки - // Перемещаем курсор после вставленного пробела - range.setStartAfter(spaceNode); - range.collapse(true); + // Вставляем неразрывный пробел + const spaceNode = document.createTextNode('\u00A0'); + range.insertNode(spaceNode); - selection.removeAllRanges(); - selection.addRange(range); - } + // Перемещаем курсор после вставленного пробела + range.setStartAfter(spaceNode); + range.collapse(true); + selection.removeAllRanges(); + selection.addRange(range); + } /** * Заменяет текст на HTML элемент */ - private replaceTextWithElement(match: ReferenceMatch, element: HTMLElement): void { - const selection = window.getSelection(); - if (!selection || selection.rangeCount === 0) return; + private replaceTextWithElement(match: ReferenceMatch, element: HTMLElement): void { + const selection = window.getSelection(); + if (!selection || selection.rangeCount === 0) return; - // Находим текстовый узел с совпадением - const range = document.createRange(); - range.selectNodeContents(this.messageInput); - range.setEnd(selection.focusNode!, selection.focusOffset); // Устанавливаем конец диапазона на текущую позицию курсора + // Находим текстовый узел с совпадением + const range = document.createRange(); + range.selectNodeContents(this.messageInput); + range.setEnd(selection.focusNode!, selection.focusOffset); // Устанавливаем конец диапазона на текущую позицию курсора - let textBeforeMatch = ''; - let nodeBeforeMatch: Node | null = null; - let offsetBeforeMatch = 0; + let textBeforeMatch = ''; + let nodeBeforeMatch: Node | null = null; + let offsetBeforeMatch = 0; - // Проходим по дочерним элементам до match.startIndex - const walker = document.createTreeWalker(this.messageInput, NodeFilter.SHOW_TEXT); - let node: Node | null; - let currentLength = 0; + // Проходим по дочерним элементам до match.startIndex + const walker = document.createTreeWalker(this.messageInput, NodeFilter.SHOW_TEXT); + let node: Node | null; + let currentLength = 0; - while ((node = walker.nextNode())) { - const nodeText = node.textContent || ''; - if (currentLength + nodeText.length >= match.startIndex) { - nodeBeforeMatch = node; - offsetBeforeMatch = match.startIndex - currentLength; - break; - } - currentLength += nodeText.length; - } + while ((node = walker.nextNode())) { + const nodeText = node.textContent || ''; + if (currentLength + nodeText.length >= match.startIndex) { + nodeBeforeMatch = node; + offsetBeforeMatch = match.startIndex - currentLength; + break; + } + currentLength += nodeText.length; + } - if (!nodeBeforeMatch) { - console.warn("Не удалось найти текстовый узел для начала замены."); - return; - } + if (!nodeBeforeMatch) { + console.warn('Не удалось найти текстовый узел для начала замены.'); + return; + } - // Удаляем диапазон, соответствующий тексту ссылки - const deleteRange = document.createRange(); - deleteRange.setStart(nodeBeforeMatch, offsetBeforeMatch); + // Удаляем диапазон, соответствующий тексту ссылки + const deleteRange = document.createRange(); + deleteRange.setStart(nodeBeforeMatch, offsetBeforeMatch); - // Расширяем диапазон удаления до конца 'fullMatch' - let remainingLengthToDelete = match.fullMatch.length; - let currentNode = nodeBeforeMatch; - let currentOffset = offsetBeforeMatch; + // Расширяем диапазон удаления до конца 'fullMatch' + let remainingLengthToDelete = match.fullMatch.length; + let currentNode = nodeBeforeMatch; + let currentOffset = offsetBeforeMatch; - while (currentNode && remainingLengthToDelete > 0) { - const nodeText = currentNode.textContent || ''; - const canDelete = Math.min(remainingLengthToDelete, nodeText.length - currentOffset); - remainingLengthToDelete -= canDelete; - currentOffset += canDelete; + while (currentNode && remainingLengthToDelete > 0) { + const nodeText = currentNode.textContent || ''; + const canDelete = Math.min(remainingLengthToDelete, nodeText.length - currentOffset); + remainingLengthToDelete -= canDelete; + currentOffset += canDelete; - if (remainingLengthToDelete > 0) { - // Если не всё удалено в текущем узле, переходим к следующему - currentNode = walker.nextNode(); - currentOffset = 0; // Начинаем с начала следующего узла - } - } - deleteRange.setEnd(currentNode || nodeBeforeMatch, currentOffset); - deleteRange.deleteContents(); + if (remainingLengthToDelete > 0) { + // Если не всё удалено в текущем узле, переходим к следующему + currentNode = walker.nextNode(); + currentOffset = 0; // Начинаем с начала следующего узла + } + } + deleteRange.setEnd(currentNode || nodeBeforeMatch, currentOffset); + deleteRange.deleteContents(); + // Вставляем новый элемент на место удаленного текста + deleteRange.insertNode(element); - // Вставляем новый элемент на место удаленного текста - deleteRange.insertNode(element); + // Очищаем выделение + selection.removeAllRanges(); + } - // Очищаем выделение - selection.removeAllRanges(); - } - - /** + /** * Вставляет пробел после указанного элемента. * @param element - HTML элемент, после которого нужно вставить пробел. */ @@ -681,28 +685,28 @@ export class ChatPanel { const selection = window.getSelection(); if (!selection) return; - // Проверяем, есть ли после элемента текстовый узел (который мы только что вставили) - let targetNode = element.nextSibling; - if (targetNode?.nodeType === Node.TEXT_NODE) { - const range = document.createRange(); - range.setStart(targetNode, targetNode.textContent!.length); - range.collapse(true); - selection.removeAllRanges(); - selection.addRange(range); - } else { - // Если нет текстового узла (такого быть не должно после insertSpaceAfterElement), - // ставим курсор максимально близко к элементу. - const range = document.createRange(); - range.setStartAfter(element); - range.collapse(true); - selection.removeAllRanges(); - selection.addRange(range); - } + // Проверяем, есть ли после элемента текстовый узел (который мы только что вставили) + let targetNode = element.nextSibling; + if (targetNode?.nodeType === Node.TEXT_NODE) { + const range = document.createRange(); + range.setStart(targetNode, targetNode.textContent!.length); + range.collapse(true); + selection.removeAllRanges(); + selection.addRange(range); + } else { + // Если нет текстового узла (такого быть не должно после insertSpaceAfterElement), + // ставим курсор максимально близко к элементу. + const range = document.createRange(); + range.setStartAfter(element); + range.collapse(true); + selection.removeAllRanges(); + selection.addRange(range); + } } /** * Получает позицию курсора в тексте - * (теперь используем findNodesAndOffsetsForMatch для более точного определения текстового смещения) + * (теперь используем findNodesAndOffsetsForMatch для более точного определения текстового смещения) */ private getCursorPositionInText(): number { const selection = window.getSelection(); @@ -710,90 +714,88 @@ export class ChatPanel { const range = selection.getRangeAt(0); // Получаем "абсолютный" индекс курсора в контексте всего текстового содержимого `messageInput` - // с учетом того, что reference-box'ы представлены их текстовыми аналогами в getTextContent() - let currentTextOffset = 0; - const walker = document.createTreeWalker(this.messageInput, NodeFilter.SHOW_ALL); - let node; + // с учетом того, что reference-box'ы представлены их текстовыми аналогами в getTextContent() + let currentTextOffset = 0; + const walker = document.createTreeWalker(this.messageInput, NodeFilter.SHOW_ALL); + let node; - while ((node = walker.nextNode())) { - if (node === range.endContainer) { - if (node.nodeType === Node.TEXT_NODE) { - currentTextOffset += range.endOffset; - } else if (node === this.messageInput) { - // Cлучай, когда курсор в корневом элементе, но не в текстовом узле. - // Может быть, курсор находится между элементами или в конце. - // Нам нужно определить, сколько узлов до курсора. - let nodeBeforeCursorElementCount = 0; - for (let i = 0; i < range.endOffset; i++) { - const childNode = this.messageInput.childNodes[i]; - currentTextOffset += this.getNodeEquivalentLengthForReferenceParsing(childNode); - } - } else { - // Если endContainer - это другой элемент (например,
), - // то курсор находится до него. - // Мы не должны добавлять его длину, но должны учесть, сколько текста было до него. - // В данном случае `range.endOffset` будет 0, если курсор перед ним. - // Если курсор внутри элемента, нужно по-другому считать. - // В контексте `contenteditable` это обычно означает, что мы либо в текстовом узле, - // либо `endContainer` является родителем, а `endOffset` - индексом дочернего элемента. - // Для `reference-box` курсор внутри быть не должен (т.к. он ``). - // Для других элементов, если курсор внутри них, `range.endOffset` будет смещением. - } - break; - } else { - currentTextOffset += this.getNodeEquivalentLengthForReferenceParsing(node); - } - } - - return currentTextOffset; + while ((node = walker.nextNode())) { + if (node === range.endContainer) { + if (node.nodeType === Node.TEXT_NODE) { + currentTextOffset += range.endOffset; + } else if (node === this.messageInput) { + // Cлучай, когда курсор в корневом элементе, но не в текстовом узле. + // Может быть, курсор находится между элементами или в конце. + // Нам нужно определить, сколько узлов до курсора. + let nodeBeforeCursorElementCount = 0; + for (let i = 0; i < range.endOffset; i++) { + const childNode = this.messageInput.childNodes[i]; + currentTextOffset += this.getNodeEquivalentLengthForReferenceParsing(childNode); + } + } else { + // Если endContainer - это другой элемент (например,
), + // то курсор находится до него. + // Мы не должны добавлять его длину, но должны учесть, сколько текста было до него. + // В данном случае `range.endOffset` будет 0, если курсор перед ним. + // Если курсор внутри элемента, нужно по-другому считать. + // В контексте `contenteditable` это обычно означает, что мы либо в текстовом узле, + // либо `endContainer` является родителем, а `endOffset` - индексом дочернего элемента. + // Для `reference-box` курсор внутри быть не должен (т.к. он ``). + // Для других элементов, если курсор внутри них, `range.endOffset` будет смещением. + } + break; + } else { + currentTextOffset += this.getNodeEquivalentLengthForReferenceParsing(node); + } + } + + return currentTextOffset; } - /** - * Вычисляет эквивалентную текстовую длину HTML-элемента, такую как reference-box. - * @param node - HTML-элемент. - * @returns эквивалентная текстовая длина элемента. - */ - private getNodeEquivalentLength(node: Node): number { - if (node.nodeType === Node.TEXT_NODE) { - return node.textContent?.length || 0; - } else if (node.nodeType === Node.ELEMENT_NODE && (node as HTMLElement).classList.contains('reference-box')) { - return 1; // Учитываем как 1 позицию, т.к. курсор не может быть внутри - } - // Для других элементов, считаем 0, если они не содержат текст - return 0; - } + /** + * Вычисляет эквивалентную текстовую длину HTML-элемента, такую как reference-box. + * @param node - HTML-элемент. + * @returns эквивалентная текстовая длина элемента. + */ + private getNodeEquivalentLength(node: Node): number { + if (node.nodeType === Node.TEXT_NODE) { + return node.textContent?.length || 0; + } else if (node.nodeType === Node.ELEMENT_NODE && (node as HTMLElement).classList.contains('reference-box')) { + return 1; // Учитываем как 1 позицию, т.к. курсор не может быть внутри + } + // Для других элементов, считаем 0, если они не содержат текст + return 0; + } /** * Получает текстовое содержимое input элемента (без HTML), * заменяя reference-box их текстовым представлением. */ private getTextContent(): string { - let result = ''; - const walker = document.createTreeWalker(this.messageInput, NodeFilter.SHOW_ALL); // SHOW_ALL для корректного обхода - let node; + let result = ''; + const walker = document.createTreeWalker(this.messageInput, NodeFilter.SHOW_ALL); // SHOW_ALL для корректного обхода + let node; - while ((node = walker.nextNode())) { - if (node.nodeType === Node.TEXT_NODE) { - result += node.textContent || ''; - } else if (node.nodeType === Node.ELEMENT_NODE && (node as HTMLElement).classList.contains('reference-box')) { - const reference = ReferenceBox.extractReference(node as HTMLElement); - if (reference) { - const prefix = reference.permanent - ? reference.type === 'file' ? '@@' : '##' - : reference.type === 'file' ? '@' : '#'; - result += prefix + reference.name; - } - } else if (node.nodeType === Node.ELEMENT_NODE) { - // Для других элементов, которые могут быть в contenteditable (например,
), - // нужно решить, как их представлять в текстовом содержимом. - // В данном случае, если это
, его можно игнорировать или заменить на \n. - // Для простоты, пока игнорируем, если это не бокс. - // Если внутри inputElement есть другие элементы, они также будут частью getTextContent, - // поэтому walker.nextNode() будет их учитывать. - // Возможно, здесь нужно быть более точным, основываясь на ожидаемой структуре. - } - } - return result; + while ((node = walker.nextNode())) { + if (node.nodeType === Node.TEXT_NODE) { + result += node.textContent || ''; + } else if (node.nodeType === Node.ELEMENT_NODE && (node as HTMLElement).classList.contains('reference-box')) { + const reference = ReferenceBox.extractReference(node as HTMLElement); + if (reference) { + const prefix = reference.permanent ? (reference.type === 'file' ? '@@' : '##') : reference.type === 'file' ? '@' : '#'; + result += prefix + reference.name; + } + } else if (node.nodeType === Node.ELEMENT_NODE) { + // Для других элементов, которые могут быть в contenteditable (например,
), + // нужно решить, как их представлять в текстовом содержимом. + // В данном случае, если это
, его можно игнорировать или заменить на \n. + // Для простоты, пока игнорируем, если это не бокс. + // Если внутри inputElement есть другие элементы, они также будут частью getTextContent, + // поэтому walker.nextNode() будет их учитывать. + // Возможно, здесь нужно быть более точным, основываясь на ожидаемой структуре. + } + } + return result; } /** @@ -803,7 +805,7 @@ export class ChatPanel { let result = ''; const childNodes = Array.from(this.messageInput.childNodes); - + for (const childNode of childNodes) { if (childNode.nodeType === Node.TEXT_NODE) { result += childNode.textContent;