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 { ReferenceBox } from '../references/ReferenceBox';
|
||||||
import { AutocompleteItem } from '../references/AutocompleteManager';
|
import { AutocompleteItem } from '../references/AutocompleteManager';
|
||||||
import LLMAgentPlugin from 'main';
|
import LLMAgentPlugin from 'main';
|
||||||
import { MarkdownRenderer } from 'obsidian'; // Добавляем импорт MarkdownRenderer
|
import { MarkdownRenderer, setIcon } from 'obsidian'; // Добавляем импорт MarkdownRenderer
|
||||||
|
|
||||||
export class ChatPanel {
|
export class ChatPanel {
|
||||||
container: HTMLElement;
|
container: HTMLElement;
|
||||||
|
|
@ -119,7 +119,7 @@ export class ChatPanel {
|
||||||
this.modelDropdown.style.left = `${left}px`;
|
this.modelDropdown.style.left = `${left}px`;
|
||||||
this.modelDropdown.style.right = 'auto'; // Сброс right, чтобы left работал корректно
|
this.modelDropdown.style.right = 'auto'; // Сброс right, чтобы left работал корректно
|
||||||
|
|
||||||
// ДОБАВЛЕНО: Устанавливаем минимальную ширину дропдауна, равную ширине кнопки
|
// Устанавливаем минимальную ширину дропдауна, равную ширине кнопки
|
||||||
let minWidth = Math.min(containerRect.width / 2, dropdownRect.width);
|
let minWidth = Math.min(containerRect.width / 2, dropdownRect.width);
|
||||||
this.modelDropdown.style.minWidth = `${minWidth}px`;
|
this.modelDropdown.style.minWidth = `${minWidth}px`;
|
||||||
this.modelDropdown.style.width = 'auto'; // Позволяем контенту определить ширину, но не меньше minWidth
|
this.modelDropdown.style.width = 'auto'; // Позволяем контенту определить ширину, но не меньше minWidth
|
||||||
|
|
@ -203,7 +203,7 @@ export class ChatPanel {
|
||||||
historyContainer.empty(); // Очищаем контейнер перед добавлением новых сообщений
|
historyContainer.empty(); // Очищаем контейнер перед добавлением новых сообщений
|
||||||
|
|
||||||
for (const msg of this.chatHistory) {
|
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' });
|
const metaDiv = messageEl.createDiv({ cls: 'chat-meta' });
|
||||||
|
|
@ -217,12 +217,13 @@ export class ChatPanel {
|
||||||
|
|
||||||
// Контент сообщения, рендерится Markdown
|
// Контент сообщения, рендерится Markdown
|
||||||
const contentDiv = messageEl.createDiv({ cls: 'chat-message-content' });
|
const contentDiv = messageEl.createDiv({ cls: 'chat-message-content' });
|
||||||
await MarkdownRenderer.renderMarkdown(
|
await MarkdownRenderer.renderMarkdown(
|
||||||
msg.content,
|
msg.content,
|
||||||
contentDiv,
|
contentDiv,
|
||||||
'', // Путь к источнику, может быть пустым для сообщений чата
|
'', // Путь к источнику, может быть пустым для сообщений чата
|
||||||
this.props.plugin // Компонент, необходимый для управления жизненным циклом рендера
|
this.props.plugin // Компонент, необходимый для управления жизненным циклом рендера
|
||||||
);
|
);
|
||||||
|
this.createMessageToolset(messageEl, msg.content);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Прокручиваем к самому низу родительского контейнера (view-content)
|
// Прокручиваем к самому низу родительского контейнера (view-content)
|
||||||
|
|
@ -677,46 +678,6 @@ export class ChatPanel {
|
||||||
selection.removeAllRanges();
|
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 для более точного определения текстового смещения)
|
* (теперь используем findNodesAndOffsetsForMatch для более точного определения текстового смещения)
|
||||||
|
|
@ -765,21 +726,6 @@ export class ChatPanel {
|
||||||
return currentTextOffset;
|
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),
|
* Получает текстовое содержимое input элемента (без HTML),
|
||||||
* заменяя reference-box их текстовым представлением.
|
* заменяя 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() для совместимости:
|
||||||
handleSend(): void {
|
handleSend(): void {
|
||||||
const message = this.messageInput.textContent?.trim() || '';
|
const message = this.messageInput.textContent?.trim() || '';
|
||||||
|
|
@ -915,4 +851,64 @@ export class ChatPanel {
|
||||||
this.clearInput();
|
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