Add appending of external files, fiz appending of internal files - now via metadata (attachments)

This commit is contained in:
dimitrievgs 2025-11-05 01:37:40 +03:00
parent aa805a5505
commit 145417cfea
8 changed files with 1231 additions and 389 deletions

341
main.js

File diff suppressed because one or more lines are too long

View File

@ -3,7 +3,7 @@
* отображение истории и подсказки команд для взаимодействия с пользователем.
*/
import { ChatHistoryItem } from './models/ChatHistoryItem';
import { ChatHistoryItem, Attachment } from './models/ChatHistoryItem';
import { AutocompleteManager } from '../references/AutocompleteManager';
import { ReferenceAutocomplete } from '../references/ReferenceAutocomplete';
import { ReferenceExpander } from '../references/ReferenceExpander';
@ -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, setIcon, Notice } from 'obsidian'; // Добавляем импорт MarkdownRenderer
import { MarkdownRenderer, setIcon, Notice, TFile } from 'obsidian';
import { Dialog } from 'src/utils/Dialog';
interface ChatPanelProps {
@ -38,6 +38,12 @@ export class ChatPanel {
currentStreamingNodeId: string | null;
stopButton: HTMLButtonElement;
// Элементы для файловых вложений
attachFileButton: HTMLButtonElement;
fileInput: HTMLInputElement;
attachedFilesContainer: HTMLDivElement;
externalAttachments: Attachment[] = [];
// ----------------------------------------------- Reference System Fields ---------------------------------------------------------------
private autocompleteManager: AutocompleteManager;
private referenceAutocomplete: ReferenceAutocomplete;
@ -198,14 +204,14 @@ export class ChatPanel {
*/
async renderHistory(): Promise<void> {
const historyContainer = this.container.querySelector('#chat-history') as HTMLElement;
historyContainer.empty(); // Очищаем контейнер перед добавлением новых сообщений
historyContainer.empty();
for (const msg of this.chatHistory) {
const messageEl = historyContainer.createDiv({
cls: `message ${msg.role} message-type-${msg.type || 'default'} group/turn-messages`
});
// Информация об отправителе
// Метаинформация
const metaDiv = messageEl.createDiv({ cls: 'chat-meta' });
const senderInfoDiv = metaDiv.createDiv({ cls: 'chat-sender-info' });
const iconDiv = senderInfoDiv.createDiv({ cls: 'chat-message-icon' });
@ -215,20 +221,24 @@ export class ChatPanel {
: `<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>`;
senderInfoDiv.createSpan({ cls: 'chat-role-label', text: this.getRoleLabel(msg.role) });
// Контент сообщения, рендерится Markdown
// Отображаем вложения как боксы
if (msg.attachments && msg.attachments.length > 0) {
const attachmentsContainer = messageEl.createDiv({ cls: 'message-attachments' });
for (const attachment of msg.attachments) {
const box = this.createAttachmentBox(attachment);
attachmentsContainer.appendChild(box);
}
}
// Контент сообщения
const contentDiv = messageEl.createDiv({ cls: 'chat-message-content' });
await MarkdownRenderer.renderMarkdown(
msg.content,
contentDiv,
'', // Путь к источнику, может быть пустым для сообщений чата
this.props.plugin // Компонент, необходимый для управления жизненным циклом рендера
);
// Передаем graphId и nodeId
// msg.node_id и this.props.plugin.activeGraphId используются для кнопок удаления
await MarkdownRenderer.renderMarkdown(msg.content, contentDiv, '', this.props.plugin);
this.createMessageToolset(messageEl, msg.content, msg.node_id, msg.graph_id || '');
}
// Прокручиваем к самому низу родительского контейнера (view-content)
// Прокрутка
if (this.chatHistory.length > 0) {
setTimeout(() => {
const scrollContainer = this.container.closest('.view-content') as HTMLElement;
@ -239,6 +249,115 @@ export class ChatPanel {
}
}
/**
* Создает визуальный бокс для отображения вложения в истории.
*/
private createAttachmentBox(attachment: Attachment): HTMLElement {
const box = document.createElement('span');
box.className = 'reference-box attachment-box clickable';
box.setAttribute('data-type', attachment.type);
box.setAttribute('data-name', attachment.name);
box.setAttribute('data-path', attachment.path);
box.setAttribute('data-permanent', String(attachment.permanent));
// НОВОЕ: Добавляем обработчик клика
box.addEventListener('click', () => {
this.handleAttachmentClick(attachment);
});
const icon = document.createElement('span');
icon.className = 'reference-icon';
// ИЗМЕНЕНО: Разные иконки для разных типов
if (attachment.type === 'prompt') {
icon.textContent = '⚡';
} else if (attachment.type === 'external') {
icon.textContent = '📎';
} else {
icon.textContent = '📄';
}
const name = document.createElement('span');
name.className = 'reference-name';
name.textContent = attachment.name;
box.appendChild(icon);
box.appendChild(name);
return box;
}
/**
* Обрабатывает клик по боксу вложения
*/
private handleAttachmentClick(attachment: Attachment): void {
if (attachment.type === 'external') {
// Для внешних файлов открываем по абсолютному пути
if (attachment.path) {
this.openExternalFile(attachment.path);
} else {
new Notice(`Не указан путь к файлу: ${attachment.name}`, 3000);
}
} else {
// Для файлов из хранилища Obsidian открываем через API
this.openVaultFile(attachment.path);
}
}
/**
* Открывает файл из хранилища Obsidian
*/
private openVaultFile(path: string): void {
const file = this.props.plugin.app.vault.getAbstractFileByPath(path);
if (file instanceof TFile) {
this.props.plugin.app.workspace.getLeaf(false).openFile(file);
} else {
new Notice(`Файл не найден: ${path}`, 3000);
}
}
// TODO: Простой window.electron.shell.openPath(absolutePath) не работает
/**
* Открывает внешний файл по абсолютному пути
*/
private openExternalFile(absolutePath: string): void {
if (!window.require) {
console.warn('Невозможно открыть внешний файл в веб-версии:', absolutePath);
new Notice('Открытие внешних файлов доступно только в desktop-версии', 3000);
return;
}
try {
const { exec } = window.require('child_process');
const path = window.require('path');
// Определяем команду в зависимости от платформы
let command: string;
const platform = process.platform;
if (platform === 'win32') {
// Windows: используем start
command = `start "" "${absolutePath}"`;
} else if (platform === 'darwin') {
// macOS: используем open
command = `open "${absolutePath}"`;
} else {
// Linux: используем xdg-open
command = `xdg-open "${absolutePath}"`;
}
exec(command, (error: Error | null) => {
if (error) {
console.error('Ошибка при открытии файла:', error);
new Notice(`Не удалось открыть файл: ${error.message}`, 3000);
}
});
} catch (error) {
console.error('Ошибка при попытке открыть файл:', error);
new Notice(`Не удалось открыть файл: ${error.message}`, 3000);
}
}
/**
* Возвращает удобочитаемую метку для роли сообщения.
* @param {string} role - Роль отправителя сообщения (например, 'user', 'assistant').
@ -307,9 +426,22 @@ export class ChatPanel {
tabindex="0"
data-placeholder="Введите сообщение, @файл, #промпт или команду (/)"
></div>
<!-- НОВОЕ: Контейнер для отображения прикрепленных файлов -->
<div class="attached-files-container" id="attached-files-container" style="display: none;"></div>
</div>
<div class="chat-controls">
<div class="control-buttons-left">
<!-- НОВОЕ: Кнопка прикрепления файла -->
<button class="attach-file-button" id="attach-file-button"
aria-label="Прикрепить файл"
data-tooltip-position="top">
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24"
fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="m21.44 11.05-9.19 9.19a6 6 0 0 1-8.49-8.49l8.57-8.57A4 4 0 1 1 18 8.84l-8.59 8.57a2 2 0 0 1-2.83-2.83l8.49-8.48"></path>
</svg>
</button>
<input type="file" id="file-input" style="display: none;" multiple />
<div class="model-select-container">
<button class="model-select-button" id="model-select-button">
<span class="model-name" id="selected-model-name">${this.selectedModel}</span>
@ -346,12 +478,17 @@ export class ChatPanel {
this.customPromptIndicator = this.container.querySelector('#custom-prompt-indicator') as HTMLSpanElement;
this.stopButton = this.container.querySelector('#stop-button') as HTMLButtonElement;
// НОВОЕ: Элементы для файловых вложений
this.attachFileButton = this.container.querySelector('#attach-file-button') as HTMLButtonElement;
this.fileInput = this.container.querySelector('#file-input') as HTMLInputElement;
this.attachedFilesContainer = this.container.querySelector('#attached-files-container') as HTMLDivElement;
this.setupEventListeners();
this.setupReferenceSystem();
this.renderHistory();
this.updateModelDropdown();
this.updatePlaceholder();
this.updateCustomSystemPromptStatus(this.props.currentGraphCustomSystemPrompt); // Инициализируем статус
this.updateCustomSystemPromptStatus(this.props.currentGraphCustomSystemPrompt);
}
// ----------------------------------------------- Reference System Setup ---------------------------------------------------------------
@ -376,11 +513,9 @@ export class ChatPanel {
this.sendButton.addEventListener('click', () => this.handleSendWithReferences());
this.messageInput.addEventListener('keydown', (e) => {
// Сначала проверяем автокомплит ссылок
if (this.referenceAutocomplete && this.referenceAutocomplete.handleKeydown(e)) {
return; // Событие обработано автокомплитом
return;
}
if (e.key === 'Enter' && !e.shiftKey) {
if (this.suggestionsContainer.style.display === 'block') {
const firstSuggestion = this.suggestionsContainer.querySelector('.suggestion-item');
@ -410,7 +545,6 @@ export class ChatPanel {
this.updatePlaceholder();
});
// Model selection
this.modelSelectButton.addEventListener('click', (e) => {
e.stopPropagation();
this.toggleModelDropdown();
@ -418,6 +552,11 @@ export class ChatPanel {
this.stopButton.addEventListener('click', () => this.handleStopGeneration());
// НОВОЕ: Обработчики для файловых вложений
this.attachFileButton.addEventListener('click', () => {
this.openFileDialog();
});
// Close dropdown when clicking outside
document.addEventListener('click', (e) => {
if (!this.modelSelectButton.contains(e.target as Node) && !this.modelDropdown.contains(e.target as Node)) {
@ -745,30 +884,30 @@ export class ChatPanel {
const rawText = this.extractTextWithReferences();
const message = rawText.trim();
if (!message) return;
if (!message && this.externalAttachments.length === 0) return; // ИЗМЕНЕНО: разрешаем отправку только с файлами
if (!this.referenceExpander) {
// Если система ссылок не инициализирована, отправляем как обычно
this.handleSend();
return;
}
try {
// Раскрываем ссылки перед отправкой
const expandedText = await this.referenceExpander.expandReferences(rawText, this.currentReferences);
// Извлекаем вложения из ссылок
const { originalText, attachments } = await this.referenceExpander.extractAttachments(rawText, this.currentReferences);
// Отправляем expandedText вместо rawText
// НОВОЕ: Объединяем вложения из ссылок и внешние файлы
const allAttachments = [...attachments, ...this.externalAttachments];
// Отправляем оригинальный текст и все вложения
if (this.props.onSendMessage) {
this.props.onSendMessage(expandedText, this.selectedModel);
this.props.onSendMessage(originalText || '', this.selectedModel, allAttachments);
}
// Очищаем состояние
this.clearInput();
} catch (error) {
console.error('Ошибка при раскрытии ссылок:', error);
// В случае ошибки отправляем исходный текст
console.error('Ошибка при обработке ссылок:', error);
if (this.props.onSendMessage) {
this.props.onSendMessage(rawText, this.selectedModel);
this.props.onSendMessage(rawText, this.selectedModel, this.externalAttachments);
}
this.clearInput();
}
@ -780,6 +919,8 @@ export class ChatPanel {
private clearInput(): void {
this.messageInput.innerHTML = '';
this.currentReferences = [];
this.externalAttachments = [];
this.renderAttachedFiles();
this.updatePlaceholder();
this.hideSuggestions();
if (this.referenceAutocomplete) {
@ -932,4 +1073,159 @@ export class ChatPanel {
}
}
}
// ----------------------------------------------- File Attachments ---------------------------------------------------------------
private async openFileDialog(): Promise<void> {
if (!window.electron) {
new Notice('Прикрепление внешних файлов доступно только в desktop-версии', 3000);
return;
}
try {
const result = await window.electron.remote.dialog.showOpenDialog({
properties: ['openFile', 'multiSelections'],
title: 'Выберите файлы для прикрепления'
});
if (result.canceled || !result.filePaths || result.filePaths.length === 0) {
return;
}
for (const filePath of result.filePaths) {
await this.addExternalAttachmentByPath(filePath);
}
} catch (error) {
console.error('Ошибка при открытии диалога выбора файлов:', error);
new Notice('Не удалось открыть диалог выбора файлов', 3000);
}
}
private async addExternalAttachmentByPath(absolutePath: string): Promise<void> {
if (!window.require) {
new Notice('Функция недоступна в этой версии приложения', 3000);
return;
}
try {
// Используем Node.js модули через require
const fs = window.require('fs');
const path = window.require('path');
// Читаем файл
const content = await fs.promises.readFile(absolutePath, 'utf-8');
const fileName = path.basename(absolutePath);
// Определяем MIME-type
const mimeType = this.getMimeType(fileName);
const attachment: Attachment = {
type: 'external',
name: fileName,
path: absolutePath,
permanent: false,
content: content,
mimeType: mimeType
};
this.externalAttachments.push(attachment);
this.renderAttachedFiles();
new Notice(`Файл "${fileName}" прикреплен`, 2000);
} catch (error) {
console.error('Ошибка при чтении файла:', error);
new Notice(`Не удалось прикрепить файл: ${error.message}`, 3000);
}
}
private getMimeType(fileName: string): string {
const ext = fileName.split('.').pop()?.toLowerCase();
const mimeTypes: Record<string, string> = {
txt: 'text/plain',
md: 'text/markdown',
json: 'application/json',
js: 'text/javascript',
ts: 'text/typescript',
html: 'text/html',
css: 'text/css',
pdf: 'application/pdf',
png: 'image/png',
jpg: 'image/jpeg',
jpeg: 'image/jpeg',
gif: 'image/gif',
svg: 'image/svg+xml',
xml: 'application/xml',
csv: 'text/csv',
zip: 'application/zip',
py: 'text/x-python',
java: 'text/x-java',
cpp: 'text/x-c++src',
c: 'text/x-csrc',
h: 'text/x-chdr',
sh: 'application/x-sh',
yaml: 'text/yaml',
yml: 'text/yaml'
};
return mimeTypes[ext || ''] || 'application/octet-stream';
}
/**
* Отображает прикрепленные файлы
*/
private renderAttachedFiles(): void {
if (this.externalAttachments.length === 0) {
this.attachedFilesContainer.style.display = 'none';
this.attachedFilesContainer.innerHTML = '';
return;
}
this.attachedFilesContainer.style.display = 'flex';
this.attachedFilesContainer.innerHTML = '';
for (const attachment of this.externalAttachments) {
const box = this.createRemovableAttachmentBox(attachment);
this.attachedFilesContainer.appendChild(box);
}
}
/**
* Создает бокс вложения с кнопкой удаления
*/
private createRemovableAttachmentBox(attachment: Attachment): HTMLElement {
const box = document.createElement('div');
box.className = 'attachment-box removable';
const icon = document.createElement('span');
icon.className = 'attachment-icon';
icon.textContent = '📎';
const name = document.createElement('span');
name.className = 'attachment-name';
name.textContent = attachment.name;
const removeBtn = document.createElement('button');
removeBtn.className = 'attachment-remove';
removeBtn.innerHTML = '×';
removeBtn.setAttribute('aria-label', 'Удалить вложение');
removeBtn.addEventListener('click', () => {
this.removeExternalAttachment(attachment);
});
box.appendChild(icon);
box.appendChild(name);
box.appendChild(removeBtn);
return box;
}
/**
* Удаляет внешнее вложение
*/
private removeExternalAttachment(attachment: Attachment): void {
const index = this.externalAttachments.indexOf(attachment);
if (index > -1) {
this.externalAttachments.splice(index, 1);
this.renderAttachedFiles();
}
}
}

View File

@ -9,7 +9,14 @@ export interface ChatHistoryItem {
type?: 'user' | 'llm' | 'tool' | 'error' | 'default';
node_id: string;
graph_id?: string;
// TODO: добавить поля для снапшотов файлов при реализации
// fileSnapshots?: Record<string, string>;
// expandedContent?: string; // содержимое после раскрытия ссылок
attachments?: Attachment[];
}
export interface Attachment {
type: 'file' | 'prompt' | 'external';
name: string;
path: string; // Относительный путь для 'file'/'prompt', абсолютный для 'external'
permanent: boolean;
content?: string;
mimeType?: string;
}

View File

@ -1167,3 +1167,100 @@
background-color: var(--button-secondary-hover-background);
border-color: var(--border-color-light); /* При наведении рамка может стать светлее */
}
/* ----------------------------------------------- Attachment Boxes --------------------------------------------------------------- */
.message-attachments {
display: flex;
flex-wrap: wrap;
gap: 6px;
margin-bottom: 8px;
}
.attachment-box {
display: inline-flex;
align-items: center;
gap: 4px;
padding: 4px 8px;
background-color: var(--background-modifier-border);
border: 1px solid var(--background-modifier-border-hover);
border-radius: 4px;
font-size: 0.9em;
opacity: 0.8;
transition: opacity 0.2s, background-color 0.2s;
}
.attachment-box.clickable {
cursor: pointer;
}
.attachment-box.clickable:hover {
opacity: 1;
background-color: var(--background-modifier-border-hover);
}
.attachment-box .reference-icon {
font-size: 1em;
}
.attachment-box .reference-name {
color: var(--text-muted);
}
/* Прикрепленные файлы в поле ввода */
.attached-files-container {
display: flex;
flex-wrap: wrap;
gap: 6px;
padding: 8px;
border-top: 1px solid var(--background-modifier-border);
}
.attachment-box.removable {
background-color: var(--interactive-normal);
padding-right: 4px;
}
.attachment-box.removable:hover {
background-color: var(--interactive-hover);
}
.attachment-remove {
background: none;
border: none;
color: var(--text-muted);
cursor: pointer;
font-size: 1.2em;
line-height: 1;
padding: 0 4px;
margin-left: 4px;
transition: color 0.2s;
}
.attachment-remove:hover {
color: var(--text-error);
}
/* Кнопка прикрепления файла */
.attach-file-button {
background: none;
border: none;
color: var(--text-muted);
cursor: pointer;
padding: 6px;
display: flex;
align-items: center;
justify-content: center;
border-radius: 4px;
transition: background-color 0.2s, color 0.2s;
}
.attach-file-button:hover {
background-color: var(--background-modifier-hover);
color: var(--text-normal);
}
.attach-file-button svg {
width: 20px;
height: 20px;
}

View File

@ -7,6 +7,7 @@ import { TFile } from 'obsidian';
import LLMAgentPlugin from 'main';
import { FileReference, ReferenceParser } from './ReferenceParser';
import { TemplateEngine } from './TemplateEngine';
import { Attachment, ChatHistoryItem } from 'src/components/models/ChatHistoryItem';
// ----------------------------------------------- Reference Expander ---------------------------------------------------------------
@ -36,8 +37,8 @@ export class ReferenceExpander {
// Обрабатываем ссылки от конца к началу, чтобы не сбивать индексы
const sortedReferences = [...references].sort((a, b) => {
const aMatches = ReferenceParser.parseReferences(text).filter(m => m.name === a.name);
const bMatches = ReferenceParser.parseReferences(text).filter(m => m.name === b.name);
const aMatches = ReferenceParser.parseReferences(text).filter((m) => m.name === a.name);
const bMatches = ReferenceParser.parseReferences(text).filter((m) => m.name === b.name);
const aIndex = aMatches.length > 0 ? aMatches[aMatches.length - 1].startIndex : 0;
const bIndex = bMatches.length > 0 ? bMatches[bMatches.length - 1].startIndex : 0;
return bIndex - aIndex;
@ -73,6 +74,85 @@ export class ReferenceExpander {
return expandedMessages;
}
/**
* Извлекает вложения из текста, НЕ раскрывая их в тексте.
* Возвращает оригинальный текст и массив вложений.
*/
async extractAttachments(
text: string,
references: FileReference[]
): Promise<{
originalText: string;
attachments: Attachment[];
}> {
const attachments: Attachment[] = [];
for (const reference of references) {
const content = await this.loadFileContent(reference);
if (content !== null) {
attachments.push({
type: reference.type,
name: reference.name,
path: reference.path,
permanent: reference.permanent,
content: content
});
}
}
return {
originalText: text,
attachments: attachments
};
}
/**
* Подготавливает сообщения для LLM с учетом вложений.
* Раскрывает только постоянные (@@/##) ссылки и ссылки в последнем сообщении.
*/
async prepareMessagesForLLM(messages: ChatHistoryItem[], isLastMessage: boolean = false): Promise<any[]> {
const preparedMessages = [];
for (let i = 0; i < messages.length; i++) {
const msg = messages[i];
const isLast = i === messages.length - 1;
if (msg.role === 'user' && msg.attachments && msg.attachments.length > 0) {
// Определяем, какие вложения нужно раскрыть
const attachmentsToExpand = msg.attachments.filter((att) => att.permanent || (isLast && isLastMessage));
if (attachmentsToExpand.length > 0) {
// Формируем сообщение с раскрытыми вложениями
let expandedContent = msg.content;
for (const attachment of attachmentsToExpand) {
const formattedContent = this.formatContentForInsertion(attachment.content || '', attachment);
expandedContent += '\n' + formattedContent;
}
preparedMessages.push({
role: msg.role,
content: expandedContent
});
} else {
// Вложения есть, но раскрывать не нужно
preparedMessages.push({
role: msg.role,
content: msg.content
});
}
} else {
// Сообщение без вложений
preparedMessages.push({
role: msg.role,
content: msg.content
});
}
}
return preparedMessages;
}
// TODO: При реализации снапшотов - сохранять содержимое файлов
// async expandReferencesWithSnapshots(text: string, references: FileReference[], messageId: string): Promise<{expandedText: string, snapshots: Record<string, string>}> {
// const snapshots: Record<string, string> = {};
@ -99,7 +179,7 @@ export class ReferenceExpander {
* @returns текст с раскрытой ссылкой
*/
private async expandSingleReference(text: string, reference: FileReference, preloadedContent?: string): Promise<string> {
const content = preloadedContent || await this.loadFileContent(reference);
const content = preloadedContent || (await this.loadFileContent(reference));
if (content === null) {
console.warn(`Не удалось загрузить содержимое для ссылки: ${reference.name}`);
@ -107,9 +187,7 @@ export class ReferenceExpander {
}
// Определяем паттерн для замены
const prefix = reference.permanent
? (reference.type === 'file' ? '@@' : '##')
: (reference.type === 'file' ? '@' : '#');
const prefix = reference.permanent ? (reference.type === 'file' ? '@@' : '##') : reference.type === 'file' ? '@' : '#';
// Создаем регулярное выражение для поиска ссылки
const escapedName = this.escapeRegExp(reference.name);
@ -128,7 +206,7 @@ export class ReferenceExpander {
*/
private async expandPermanentReferences(text: string): Promise<string> {
const matches = ReferenceParser.parseReferences(text);
const permanentMatches = matches.filter(match => match.prefix.length === 2); // @@ или ##
const permanentMatches = matches.filter((match) => match.prefix.length === 2); // @@ или ##
if (permanentMatches.length === 0) {
return text;
@ -150,9 +228,7 @@ export class ReferenceExpander {
const content = await this.loadFileContent(reference);
if (content !== null) {
const formattedContent = this.formatContentForInsertion(content, reference);
expandedText = expandedText.substring(0, match.startIndex) +
formattedContent +
expandedText.substring(match.endIndex);
expandedText = expandedText.substring(0, match.startIndex) + formattedContent + expandedText.substring(match.endIndex);
}
}
@ -203,7 +279,7 @@ export class ReferenceExpander {
}
}
return (file instanceof TFile) ? file : null;
return file instanceof TFile ? file : null;
}
/**
@ -219,7 +295,7 @@ export class ReferenceExpander {
} else {
// Ищем файл в хранилище
const files = this.plugin.app.vault.getMarkdownFiles();
const matchingFile = files.find(f => f.basename === name);
const matchingFile = files.find((f) => f.basename === name);
return matchingFile ? matchingFile.path : `${name}.md`;
}
}

0
src/types/electron.d.ts vendored Normal file
View File

View File

@ -173,7 +173,7 @@ export class ChatView extends ItemView {
// ----------------------------------------------- Event Handlers ---------------------------------------------------------------
async handleSendMessage(message: string, selectedModel?: string): Promise<void> {
async handleSendMessage(message: string, selectedModel?: string, attachments?: Attachment[]): Promise<void> {
try {
if (!this.selectedGraphId) {
new Notice('Граф не выбран или не инициализирован. Пожалуйста, начните новый чат или выберите существующий.', 3000);
@ -184,14 +184,15 @@ export class ChatView extends ItemView {
return;
}
// Шаг 1: Создаём узел пользователя
// Шаг 1: Создаём узел пользователя с вложениями
const sendResponse = await fetch(`${this.plugin.settings.apiBaseUrl}/chat/send`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
message: message,
graph_id: this.selectedGraphId,
parent_node_id: this.currentNode
parent_node_id: this.currentNode,
attachments: attachments || []
})
});
@ -200,10 +201,8 @@ export class ChatView extends ItemView {
}
const sendResult = await sendResponse.json();
// this.selectedGraphId = sendResult.graph_id;
const userNodeId = sendResult.node_id;
// Обновляем граф с новым узлом пользователя
this.plugin.eventBus.emit('graph-selected', {
graphId: this.selectedGraphId,
currentNode: userNodeId
@ -211,9 +210,8 @@ export class ChatView extends ItemView {
// Шаг 2: Запускаем стриминг ответа
if (this.selectedGraphId) {
const systemPromptToUse = this.currentGraphCustomSystemPrompt !== null ?
this.currentGraphCustomSystemPrompt :
this.plugin.settings.systemPrompt;
const systemPromptToUse =
this.currentGraphCustomSystemPrompt !== null ? this.currentGraphCustomSystemPrompt : this.plugin.settings.systemPrompt;
await this.streamLLMResponse(this.selectedGraphId, userNodeId, selectedModel, null, systemPromptToUse);
} else {
@ -236,14 +234,9 @@ export class ChatView extends ItemView {
this.fetchMessagesFromRootToNode(data.nodeId).then(() => {
// После загрузки новой ветки проверяем, есть ли стриминг в ней
if (this.currentStreamingNodeId) {
const isInNewBranch = this.chatHistory.some(
msg => msg.node_id === this.currentStreamingNodeId
);
const isInNewBranch = this.chatHistory.some((msg) => msg.node_id === this.currentStreamingNodeId);
if (this.chatPanel) {
this.chatPanel.updateStopButtonVisibility(
isInNewBranch,
isInNewBranch ? this.currentStreamingNodeId : null
);
this.chatPanel.updateStopButtonVisibility(isInNewBranch, isInNewBranch ? this.currentStreamingNodeId : null);
}
}
});
@ -469,9 +462,8 @@ export class ChatView extends ItemView {
async handleRegenerateMessage(data: { graphId: string; nodeId: string; model: string }): Promise<void> {
try {
// Определяем системный промпт для отправки: кастомный для графа, если есть, иначе глобальный
const systemPromptToUse = this.currentGraphCustomSystemPrompt !== null ?
this.currentGraphCustomSystemPrompt :
this.plugin.settings.systemPrompt;
const systemPromptToUse =
this.currentGraphCustomSystemPrompt !== null ? this.currentGraphCustomSystemPrompt : this.plugin.settings.systemPrompt;
const response = await fetch(`${this.plugin.settings.apiBaseUrl}/chat/regenerate`, {
method: 'POST',

File diff suppressed because one or more lines are too long