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;