Add chat message toolset with copy text button
This commit is contained in:
parent
326198bb1d
commit
6595ab0eb1
|
|
@ -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('Не удалось скопировать сообщение.');
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user