diff --git a/src/components/ChatPanel.ts b/src/components/ChatPanel.ts index 3a0a6f9..bf3321f 100644 --- a/src/components/ChatPanel.ts +++ b/src/components/ChatPanel.ts @@ -12,7 +12,7 @@ import { ReferenceParser, FileReference, ReferenceMatch } from '../references/Re import { ReferenceBox } from '../references/ReferenceBox'; import { AutocompleteItem } from '../references/AutocompleteManager'; import LLMAgentPlugin from 'main'; -import { MarkdownRenderer } from 'obsidian'; // Добавляем импорт MarkdownRenderer +import { MarkdownRenderer, setIcon } from 'obsidian'; // Добавляем импорт MarkdownRenderer export class ChatPanel { container: HTMLElement; @@ -119,7 +119,7 @@ export class ChatPanel { this.modelDropdown.style.left = `${left}px`; this.modelDropdown.style.right = 'auto'; // Сброс right, чтобы left работал корректно - // ДОБАВЛЕНО: Устанавливаем минимальную ширину дропдауна, равную ширине кнопки + // Устанавливаем минимальную ширину дропдауна, равную ширине кнопки let minWidth = Math.min(containerRect.width / 2, dropdownRect.width); this.modelDropdown.style.minWidth = `${minWidth}px`; this.modelDropdown.style.width = 'auto'; // Позволяем контенту определить ширину, но не меньше minWidth @@ -203,7 +203,7 @@ export class ChatPanel { historyContainer.empty(); // Очищаем контейнер перед добавлением новых сообщений for (const msg of this.chatHistory) { - const messageEl = historyContainer.createDiv({ cls: `message ${msg.role} message-type-${msg.type || 'default'}` }); + const messageEl = historyContainer.createDiv({ cls: `message ${msg.role} message-type-${msg.type || 'default'} group/turn-messages` }); // Информация об отправителе const metaDiv = messageEl.createDiv({ cls: 'chat-meta' }); @@ -217,12 +217,13 @@ export class ChatPanel { // Контент сообщения, рендерится Markdown const contentDiv = messageEl.createDiv({ cls: 'chat-message-content' }); - await MarkdownRenderer.renderMarkdown( - msg.content, - contentDiv, - '', // Путь к источнику, может быть пустым для сообщений чата - this.props.plugin // Компонент, необходимый для управления жизненным циклом рендера - ); + await MarkdownRenderer.renderMarkdown( + msg.content, + contentDiv, + '', // Путь к источнику, может быть пустым для сообщений чата + this.props.plugin // Компонент, необходимый для управления жизненным циклом рендера + ); + this.createMessageToolset(messageEl, msg.content); } // Прокручиваем к самому низу родительского контейнера (view-content) @@ -677,46 +678,6 @@ export class ChatPanel { selection.removeAllRanges(); } - /** - * Вставляет пробел после указанного элемента. - * @param element - HTML элемент, после которого нужно вставить пробел. - */ - private insertSpaceAfterElement(element: HTMLElement): void { - const spaceNode = document.createTextNode('\u00A0'); // Неразрывный пробел - if (element.nextSibling) { - element.parentNode?.insertBefore(spaceNode, element.nextSibling); - } else { - element.parentNode?.appendChild(spaceNode); - } - } - - /** - * Устанавливает курсор после указанного элемента. - * @param element - HTML элемент, после которого нужно установить курсор. - */ - private setCursorAfterElement(element: HTMLElement): void { - 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); - } - } - /** * Получает позицию курсора в тексте * (теперь используем findNodesAndOffsetsForMatch для более точного определения текстового смещения) @@ -765,21 +726,6 @@ export class ChatPanel { 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; - } - /** * Получает текстовое содержимое input элемента (без HTML), * заменяя reference-box их текстовым представлением. @@ -897,16 +843,6 @@ export class ChatPanel { } } - /*handleSend(): void { - const message = this.messageInput.textContent?.trim() || ''; - if (message && this.props.onSendMessage) { - this.props.onSendMessage(message, this.selectedModel); - this.messageInput.textContent = ''; - this.updatePlaceholder(); - this.hideSuggestions(); - } - }*/ - // Обновить метод handleSend() для совместимости: handleSend(): void { const message = this.messageInput.textContent?.trim() || ''; @@ -915,4 +851,64 @@ export class ChatPanel { this.clearInput(); } } + + /** + * Создает панель инструментов под сообщением с кнопками для взаимодействия. + * @param messageElement - Элемент сообщения, к которому добавляется панель инструментов. + * @param messageContent - Содержимое сообщения для функции копирования. + */ + private createMessageToolset(messageElement: HTMLElement, messageContent: string): void { + const toolsetWrapper = messageElement.createDiv({ + cls: 'z-0 flex min-h-[46px] justify-start' + }); + const toolsetContainer = toolsetWrapper.createDiv({ + cls: 'touch:-me-2 touch:-ms-3.5 -ms-2.5 -me-1 flex flex-wrap items-center gap-y-4 p-1 select-none touch:w-[calc(100%+--spacing(3.5))] -mt-1 w-[calc(100%+--spacing(2.5))] duration-[1.5s] focus-within:transition-none hover:transition-none pointer-events-none [mask-image:linear-gradient(to_right,black_33%,transparent_66%)] [mask-size:300%_100%] [mask-position:100%_0%] motion-safe:transition-[mask-position] group-hover/turn-messages:pointer-events-auto group-hover/turn-messages:[mask-position:0_0] group-focus-within/turn-messages:pointer-events-auto group-focus-within/turn-messages:[mask-position:0_0] has-data-[state=open]:pointer-events-auto has-data-[state=open]:[mask-position:0_0]', + }); + + // Кнопка "Копировать" + const copyButton = toolsetContainer.createEl('button', { + cls: 'text-token-text-secondary hover:bg-token-bg-secondary rounded-lg', + attr: { + 'aria-label': 'Копировать', + 'aria-pressed': 'false', + 'data-testid': 'copy-turn-action-button', + 'data-state': 'closed', + }, + }); + const copyIconContainer = copyButton.createSpan({ cls: 'flex items-center justify-center touch:w-10 h-8 w-8' }); + setIcon(copyIconContainer, 'copy'); // Используем импортированную функцию setIcon + copyButton.addEventListener('click', () => { + this.copyMessageContent(messageContent); + }); + + // Кнопка "Попробовать снова" + /*const retryButton = toolsetContainer.createEl('button', { + cls: 'text-token-text-secondary hover:bg-token-bg-secondary rounded-lg', + attr: { + 'aria-label': 'Попробовать снова', + 'aria-pressed': 'false', + 'data-testid': 'refresh-turn-action-button', + 'data-state': 'closed', + }, + }); + const retryIconContainer = retryButton.createSpan({ cls: 'flex items-center justify-center touch:w-10 h-8 w-8' }); + setIcon(retryIconContainer, 'refresh-cw'); // Используем импортированную функцию setIcon + retryButton.addEventListener('click', () => { + this.copyMessageContent(messageContent); + });*/ // TODO: реализовать переотправку сообщения + } + + private copyMessageContent(content: string): void { + navigator.clipboard.writeText(content) + .then(() => { + console.log('Message content copied to clipboard.'); + // Optionally: show a notice to the user + // new Notice('Сообщение скопировано!'); + }) + .catch(err => { + console.error('Failed to copy message content:', err); + // Optionally: show an error notice + // new Notice('Не удалось скопировать сообщение.'); + }); + } }