Markdown rendering of messages in chat panel

This commit is contained in:
dimitrievgs 2025-09-28 23:45:52 +03:00
parent 23b4ca4571
commit 326198bb1d
3 changed files with 43 additions and 31 deletions

View File

@ -12,6 +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
export class ChatPanel { export class ChatPanel {
container: HTMLElement; container: HTMLElement;
@ -193,29 +194,36 @@ export class ChatPanel {
this.suggestionsContainer.style.display = 'none'; this.suggestionsContainer.style.display = 'none';
} }
renderHistory(): void { /**
* Рендерит историю сообщений в контейнер чата.
* @returns {Promise<void>}
*/
async renderHistory(): Promise<void> {
const historyContainer = this.container.querySelector('#chat-history') as HTMLElement; const historyContainer = this.container.querySelector('#chat-history') as HTMLElement;
historyContainer.innerHTML = this.chatHistory historyContainer.empty(); // Очищаем контейнер перед добавлением новых сообщений
.map(
(msg, index) => ` for (const msg of this.chatHistory) {
<div class="message ${msg.role} message-type-${msg.type || 'default'} "> const messageEl = historyContainer.createDiv({ cls: `message ${msg.role} message-type-${msg.type || 'default'}` });
<div class="chat-meta">
<div class="chat-sender-info"> // Информация об отправителе
<div class="chat-message-icon"> const metaDiv = messageEl.createDiv({ cls: 'chat-meta' });
${ const senderInfoDiv = metaDiv.createDiv({ cls: 'chat-sender-info' });
msg.role === 'user' const iconDiv = senderInfoDiv.createDiv({ cls: 'chat-message-icon' });
? `<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-user"><path d="M19 21v-2a4 4 0 0 0-4-4H9a4 4 0 0 0-4 4v2"></path><circle cx="12" cy="7" r="4"></circle></svg>` iconDiv.innerHTML =
: `<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-bot"><path d="M12 8V4H8"></path><rect width="16" height="12" x="4" y="8" rx="2"></rect><path d="M2 14h2"></path><path d="M20 14h2"></path><path d="M15 13v2"></path><path d="M9 13v2"></path></svg>` msg.role === 'user'
} ? `<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-user"><path d="M19 21v-2a4 4 0 0 0-4-4H9a4 4 0 0 0-4 4v2"></path><circle cx="12" cy="7" r="4"></circle></svg>`
</div> : `<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-bot"><path d="M12 8V4H8"></path><rect width="16" height="12" x="4" y="8" rx="2"></rect><path d="M2 14h2"></path><path d="M20 14h2"></path><path d="M15 13v2"></path><path d="M9 13v2"></path></svg>`;
<span class="chat-role-label">${this.getRoleLabel(msg.role)}</span> senderInfoDiv.createSpan({ cls: 'chat-role-label', text: this.getRoleLabel(msg.role) });
</div>
</div> // Контент сообщения, рендерится Markdown
<div class="chat-message-content">${this.processMessageContent(msg.content)}</div> const contentDiv = messageEl.createDiv({ cls: 'chat-message-content' });
</div> await MarkdownRenderer.renderMarkdown(
` msg.content,
) contentDiv,
.join(''); '', // Путь к источнику, может быть пустым для сообщений чата
this.props.plugin // Компонент, необходимый для управления жизненным циклом рендера
);
}
// Прокручиваем к самому низу родительского контейнера (view-content) // Прокручиваем к самому низу родительского контейнера (view-content)
if (this.chatHistory.length > 0) { if (this.chatHistory.length > 0) {
@ -249,9 +257,14 @@ export class ChatPanel {
return content.replace(/<img([^>]*?)>/g, '<img$1 style="max-width: 100%; height: auto;">').replace(/\n/g, '<br>'); return content.replace(/<img([^>]*?)>/g, '<img$1 style="max-width: 100%; height: auto;">').replace(/\n/g, '<br>');
} }
updateHistory(newHistory: ChatHistoryItem[]): void { /**
* Обновляет историю чата и перерисовывает её.
* @param {ChatHistoryItem[]} newHistory - Новый массив элементов истории чата.
* @returns {Promise<void>}
*/
async updateHistory(newHistory: ChatHistoryItem[]): Promise<void> {
this.chatHistory = newHistory; this.chatHistory = newHistory;
this.renderHistory(); await this.renderHistory();
} }
updateAvailableModels(models: string[]): void { updateAvailableModels(models: string[]): void {

View File

@ -114,7 +114,7 @@ export class ChatView extends ItemView {
type: msg.type || msg.role // Используем type, если есть, иначе role как fallback type: msg.type || msg.role // Используем type, если есть, иначе role как fallback
})) || []; })) || [];
if (this.chatPanel) { if (this.chatPanel) {
this.chatPanel.updateHistory(this.chatHistory); await this.chatPanel.updateHistory(this.chatHistory);
} }
} catch (error) { } catch (error) {
console.error('Ошибка загрузки сообщений:', error); console.error('Ошибка загрузки сообщений:', error);
@ -181,9 +181,9 @@ export class ChatView extends ItemView {
this.titleUpdateTimer = setTimeout(() => { this.titleUpdateTimer = setTimeout(() => {
console.log('Обновление графа для получения сгенерированных заголовков...'); console.log('Обновление графа для получения сгенерированных заголовков...');
if (this.selectedGraphId && this.currentNode) { if (this.selectedGraphId && this.currentNode) {
this.plugin.eventBus.emit('graph-selected', { this.plugin.eventBus.emit('graph-selected', {
graphId: this.selectedGraphId, graphId: this.selectedGraphId,
currentNode: this.currentNode currentNode: this.currentNode
}); });
} }
this.titleUpdateTimer = null; this.titleUpdateTimer = null;
@ -220,12 +220,12 @@ export class ChatView extends ItemView {
* Обрабатывает событие начала нового чата. Сбрасывает текущее состояние чата * Обрабатывает событие начала нового чата. Сбрасывает текущее состояние чата
* и очищает отображение истории. * и очищает отображение истории.
*/ */
handleNewChat(): void { async handleNewChat(): Promise<void> {
this.chatHistory = []; this.chatHistory = [];
this.currentNode = null; this.currentNode = null;
this.selectedGraphId = null; this.selectedGraphId = null;
if (this.chatPanel) { if (this.chatPanel) {
this.chatPanel.updateHistory(this.chatHistory); await this.chatPanel.updateHistory(this.chatHistory);
} }
} }

View File

@ -303,7 +303,6 @@ export class GraphView extends ItemView {
*/ */
async handleGraphSelected(data: { graphId: string; currentNode: string | null }): Promise<void> { async handleGraphSelected(data: { graphId: string; currentNode: string | null }): Promise<void> {
this.selectedGraphId = data.graphId; this.selectedGraphId = data.graphId;
console.log("11111")
// currentNode здесь может быть неактуальным, fetchGraphData загрузит актуальный current_node_id // currentNode здесь может быть неактуальным, fetchGraphData загрузит актуальный current_node_id
await this.fetchGraphData(data.graphId); await this.fetchGraphData(data.graphId);
// Note: Больше не нужно эмитить 'graph-selected' обратно в ChatView, т.к. ChatView уже инициировал это. // Note: Больше не нужно эмитить 'graph-selected' обратно в ChatView, т.к. ChatView уже инициировал это.