Add сaching of files, content of files no longer stored in db, add $ and !$ shoftcuts
This commit is contained in:
parent
145417cfea
commit
b773fdbc40
25
main.ts
25
main.ts
|
|
@ -12,10 +12,11 @@ interface LLMAgentSettings {
|
||||||
apiBaseUrl: string;
|
apiBaseUrl: string;
|
||||||
systemPrompt: string;
|
systemPrompt: string;
|
||||||
defaultModel: string;
|
defaultModel: string;
|
||||||
promptsFolder: string; // Новое поле
|
promptsFolder: string;
|
||||||
enableFileReferences: boolean; // Новое поле
|
enableFileReferences: boolean;
|
||||||
enablePromptReferences: boolean; // Новое поле
|
enablePromptReferences: boolean;
|
||||||
excludedFolders: string[]; // Новое поле: Список папок для исключения из автокомплита
|
excludedFolders: string[];
|
||||||
|
refCacheFolder: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
const DEFAULT_SETTINGS: LLMAgentSettings = {
|
const DEFAULT_SETTINGS: LLMAgentSettings = {
|
||||||
|
|
@ -25,7 +26,8 @@ const DEFAULT_SETTINGS: LLMAgentSettings = {
|
||||||
promptsFolder: 'prompts', // Папка с промптами по умолчанию
|
promptsFolder: 'prompts', // Папка с промптами по умолчанию
|
||||||
enableFileReferences: true,
|
enableFileReferences: true,
|
||||||
enablePromptReferences: true,
|
enablePromptReferences: true,
|
||||||
excludedFolders: ['templates'] // Папка "templates" по умолчанию исключена
|
excludedFolders: ['templates'], // Папка "templates" по умолчанию исключена
|
||||||
|
refCacheFolder: 'ref-cache' // По умолчанию относительно vault
|
||||||
};
|
};
|
||||||
|
|
||||||
// ----------------------------------------------- Main Plugin Class ---------------------------------------------------------------
|
// ----------------------------------------------- Main Plugin Class ---------------------------------------------------------------
|
||||||
|
|
@ -207,6 +209,19 @@ class SampleSettingTab extends PluginSettingTab {
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
|
new Setting(containerEl)
|
||||||
|
.setName('Папка кеша для вложений')
|
||||||
|
.setDesc('Абсолютный или относительный путь к папке для кеширования файлов')
|
||||||
|
.addText((text) =>
|
||||||
|
text
|
||||||
|
.setPlaceholder('ref-cache')
|
||||||
|
.setValue(this.plugin.settings.refCacheFolder)
|
||||||
|
.onChange(async (value) => {
|
||||||
|
this.plugin.settings.refCacheFolder = value;
|
||||||
|
await this.plugin.saveSettings();
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
// Настройки ссылок на файлы
|
// Настройки ссылок на файлы
|
||||||
new Setting(containerEl)
|
new Setting(containerEl)
|
||||||
.setName('Включить ссылки на файлы')
|
.setName('Включить ссылки на файлы')
|
||||||
|
|
|
||||||
|
|
@ -14,10 +14,11 @@ import { AutocompleteItem } from '../references/AutocompleteManager';
|
||||||
import LLMAgentPlugin from 'main';
|
import LLMAgentPlugin from 'main';
|
||||||
import { MarkdownRenderer, setIcon, Notice, TFile } from 'obsidian';
|
import { MarkdownRenderer, setIcon, Notice, TFile } from 'obsidian';
|
||||||
import { Dialog } from 'src/utils/Dialog';
|
import { Dialog } from 'src/utils/Dialog';
|
||||||
|
import { CacheManager } from 'src/references/CacheManager';
|
||||||
|
|
||||||
interface ChatPanelProps {
|
interface ChatPanelProps {
|
||||||
chatHistory: ChatHistoryItem[];
|
chatHistory: ChatHistoryItem[];
|
||||||
onSendMessage: (message: string, selectedModel?: string) => void;
|
onSendMessage: (message: string, selectedModel?: string, attachments?: Attachment[]) => void;
|
||||||
availableModels?: string[];
|
availableModels?: string[];
|
||||||
selectedModel?: string;
|
selectedModel?: string;
|
||||||
plugin: LLMAgentPlugin;
|
plugin: LLMAgentPlugin;
|
||||||
|
|
@ -42,9 +43,11 @@ export class ChatPanel {
|
||||||
attachFileButton: HTMLButtonElement;
|
attachFileButton: HTMLButtonElement;
|
||||||
fileInput: HTMLInputElement;
|
fileInput: HTMLInputElement;
|
||||||
attachedFilesContainer: HTMLDivElement;
|
attachedFilesContainer: HTMLDivElement;
|
||||||
externalAttachments: Attachment[] = [];
|
currentAttachments: Attachment[] = [];
|
||||||
|
|
||||||
// ----------------------------------------------- Reference System Fields ---------------------------------------------------------------
|
// ----------------------------------------------- Reference System Fields ---------------------------------------------------------------
|
||||||
|
cacheManager: CacheManager;
|
||||||
|
|
||||||
private autocompleteManager: AutocompleteManager;
|
private autocompleteManager: AutocompleteManager;
|
||||||
private referenceAutocomplete: ReferenceAutocomplete;
|
private referenceAutocomplete: ReferenceAutocomplete;
|
||||||
private referenceExpander: ReferenceExpander;
|
private referenceExpander: ReferenceExpander;
|
||||||
|
|
@ -254,13 +257,18 @@ export class ChatPanel {
|
||||||
*/
|
*/
|
||||||
private createAttachmentBox(attachment: Attachment): HTMLElement {
|
private createAttachmentBox(attachment: Attachment): HTMLElement {
|
||||||
const box = document.createElement('span');
|
const box = document.createElement('span');
|
||||||
box.className = 'reference-box attachment-box clickable';
|
|
||||||
|
// Определяем классы в зависимости от типа и permanent
|
||||||
|
const permanentClass = attachment.permanent ? 'permanent' : 'single';
|
||||||
|
box.className = `reference-box attachment-box ${attachment.type} ${permanentClass} clickable`;
|
||||||
|
|
||||||
box.setAttribute('data-type', attachment.type);
|
box.setAttribute('data-type', attachment.type);
|
||||||
box.setAttribute('data-name', attachment.name);
|
box.setAttribute('data-name', attachment.name);
|
||||||
box.setAttribute('data-path', attachment.path);
|
box.setAttribute('data-source-path', attachment.sourcePath);
|
||||||
|
box.setAttribute('data-cached-path', attachment.cachedPath);
|
||||||
|
box.setAttribute('data-uuid', attachment.uuid);
|
||||||
box.setAttribute('data-permanent', String(attachment.permanent));
|
box.setAttribute('data-permanent', String(attachment.permanent));
|
||||||
|
|
||||||
// НОВОЕ: Добавляем обработчик клика
|
|
||||||
box.addEventListener('click', () => {
|
box.addEventListener('click', () => {
|
||||||
this.handleAttachmentClick(attachment);
|
this.handleAttachmentClick(attachment);
|
||||||
});
|
});
|
||||||
|
|
@ -268,7 +276,6 @@ export class ChatPanel {
|
||||||
const icon = document.createElement('span');
|
const icon = document.createElement('span');
|
||||||
icon.className = 'reference-icon';
|
icon.className = 'reference-icon';
|
||||||
|
|
||||||
// ИЗМЕНЕНО: Разные иконки для разных типов
|
|
||||||
if (attachment.type === 'prompt') {
|
if (attachment.type === 'prompt') {
|
||||||
icon.textContent = '⚡';
|
icon.textContent = '⚡';
|
||||||
} else if (attachment.type === 'external') {
|
} else if (attachment.type === 'external') {
|
||||||
|
|
@ -290,18 +297,29 @@ export class ChatPanel {
|
||||||
/**
|
/**
|
||||||
* Обрабатывает клик по боксу вложения
|
* Обрабатывает клик по боксу вложения
|
||||||
*/
|
*/
|
||||||
private handleAttachmentClick(attachment: Attachment): void {
|
private async handleAttachmentClick(attachment: Attachment): Promise<void> {
|
||||||
|
// Проверяем существование кеша
|
||||||
|
const cacheManager = new CacheManager(this.props.plugin);
|
||||||
|
const cacheExists = await cacheManager.cachedFileExists(attachment.cachedPath);
|
||||||
|
|
||||||
|
let pathToOpen: string;
|
||||||
|
|
||||||
|
if (cacheExists) {
|
||||||
|
// Открываем кешированный файл
|
||||||
|
const cacheFolderPath = this.props.plugin.settings.refCacheFolder;
|
||||||
|
pathToOpen = this.props.plugin.app.vault.adapter.getBasePath() + '/' + cacheFolderPath + '/' + attachment.cachedPath;
|
||||||
|
} else {
|
||||||
|
// Кеш не найден, открываем исходный файл
|
||||||
if (attachment.type === 'external') {
|
if (attachment.type === 'external') {
|
||||||
// Для внешних файлов открываем по абсолютному пути
|
pathToOpen = attachment.sourcePath; // Уже абсолютный
|
||||||
if (attachment.path) {
|
} else if (attachment.type === 'file') {
|
||||||
this.openExternalFile(attachment.path);
|
pathToOpen = this.props.plugin.app.vault.adapter.getBasePath() + '/' + attachment.sourcePath;
|
||||||
} else {
|
} else {
|
||||||
new Notice(`Не указан путь к файлу: ${attachment.name}`, 3000);
|
pathToOpen = this.props.plugin.settings.promptsFolder + '/' + attachment.sourcePath;
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
// Для файлов из хранилища Obsidian открываем через API
|
|
||||||
this.openVaultFile(attachment.path);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this.openExternalFile(pathToOpen);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -409,6 +427,7 @@ export class ChatPanel {
|
||||||
|
|
||||||
this.autocompleteManager = new AutocompleteManager(this.props.plugin);
|
this.autocompleteManager = new AutocompleteManager(this.props.plugin);
|
||||||
this.templateEngine = new TemplateEngine(this.props.plugin);
|
this.templateEngine = new TemplateEngine(this.props.plugin);
|
||||||
|
this.cacheManager = new CacheManager(this.props.plugin);
|
||||||
this.referenceExpander = new ReferenceExpander(this.props.plugin, this.templateEngine);
|
this.referenceExpander = new ReferenceExpander(this.props.plugin, this.templateEngine);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -424,24 +443,12 @@ export class ChatPanel {
|
||||||
data-testid="editor-input-main"
|
data-testid="editor-input-main"
|
||||||
class="message-input-editor"
|
class="message-input-editor"
|
||||||
tabindex="0"
|
tabindex="0"
|
||||||
data-placeholder="Введите сообщение, @файл, #промпт или команду (/)"
|
data-placeholder="Введите сообщение, @файл, #промпт, $внешний_файл или команду (/)"
|
||||||
></div>
|
></div>
|
||||||
<!-- НОВОЕ: Контейнер для отображения прикрепленных файлов -->
|
|
||||||
<div class="attached-files-container" id="attached-files-container" style="display: none;"></div>
|
<div class="attached-files-container" id="attached-files-container" style="display: none;"></div>
|
||||||
</div>
|
</div>
|
||||||
<div class="chat-controls">
|
<div class="chat-controls">
|
||||||
<div class="control-buttons-left">
|
<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">
|
<div class="model-select-container">
|
||||||
<button class="model-select-button" id="model-select-button">
|
<button class="model-select-button" id="model-select-button">
|
||||||
<span class="model-name" id="selected-model-name">${this.selectedModel}</span>
|
<span class="model-name" id="selected-model-name">${this.selectedModel}</span>
|
||||||
|
|
@ -478,9 +485,6 @@ export class ChatPanel {
|
||||||
this.customPromptIndicator = this.container.querySelector('#custom-prompt-indicator') as HTMLSpanElement;
|
this.customPromptIndicator = this.container.querySelector('#custom-prompt-indicator') as HTMLSpanElement;
|
||||||
this.stopButton = this.container.querySelector('#stop-button') as HTMLButtonElement;
|
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.attachedFilesContainer = this.container.querySelector('#attached-files-container') as HTMLDivElement;
|
||||||
|
|
||||||
this.setupEventListeners();
|
this.setupEventListeners();
|
||||||
|
|
@ -552,11 +556,6 @@ export class ChatPanel {
|
||||||
|
|
||||||
this.stopButton.addEventListener('click', () => this.handleStopGeneration());
|
this.stopButton.addEventListener('click', () => this.handleStopGeneration());
|
||||||
|
|
||||||
// НОВОЕ: Обработчики для файловых вложений
|
|
||||||
this.attachFileButton.addEventListener('click', () => {
|
|
||||||
this.openFileDialog();
|
|
||||||
});
|
|
||||||
|
|
||||||
// Close dropdown when clicking outside
|
// Close dropdown when clicking outside
|
||||||
document.addEventListener('click', (e) => {
|
document.addEventListener('click', (e) => {
|
||||||
if (!this.modelSelectButton.contains(e.target as Node) && !this.modelDropdown.contains(e.target as Node)) {
|
if (!this.modelSelectButton.contains(e.target as Node) && !this.modelDropdown.contains(e.target as Node)) {
|
||||||
|
|
@ -884,7 +883,7 @@ export class ChatPanel {
|
||||||
const rawText = this.extractTextWithReferences();
|
const rawText = this.extractTextWithReferences();
|
||||||
const message = rawText.trim();
|
const message = rawText.trim();
|
||||||
|
|
||||||
if (!message && this.externalAttachments.length === 0) return; // ИЗМЕНЕНО: разрешаем отправку только с файлами
|
if (!message && this.currentAttachments.length === 0) return;
|
||||||
|
|
||||||
if (!this.referenceExpander) {
|
if (!this.referenceExpander) {
|
||||||
this.handleSend();
|
this.handleSend();
|
||||||
|
|
@ -892,11 +891,11 @@ export class ChatPanel {
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Извлекаем вложения из ссылок
|
// ИЗМЕНЕНО: Извлекаем вложения из ссылок И кешируем их
|
||||||
const { originalText, attachments } = await this.referenceExpander.extractAttachments(rawText, this.currentReferences);
|
const { originalText, attachments } = await this.referenceExpander.extractAttachments(rawText, this.currentReferences);
|
||||||
|
|
||||||
// НОВОЕ: Объединяем вложения из ссылок и внешние файлы
|
// Объединяем вложения из ссылок и текущие (из attached-files-container)
|
||||||
const allAttachments = [...attachments, ...this.externalAttachments];
|
const allAttachments = [...attachments, ...this.currentAttachments];
|
||||||
|
|
||||||
// Отправляем оригинальный текст и все вложения
|
// Отправляем оригинальный текст и все вложения
|
||||||
if (this.props.onSendMessage) {
|
if (this.props.onSendMessage) {
|
||||||
|
|
@ -907,7 +906,7 @@ export class ChatPanel {
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Ошибка при обработке ссылок:', error);
|
console.error('Ошибка при обработке ссылок:', error);
|
||||||
if (this.props.onSendMessage) {
|
if (this.props.onSendMessage) {
|
||||||
this.props.onSendMessage(rawText, this.selectedModel, this.externalAttachments);
|
this.props.onSendMessage(rawText, this.selectedModel, this.currentAttachments);
|
||||||
}
|
}
|
||||||
this.clearInput();
|
this.clearInput();
|
||||||
}
|
}
|
||||||
|
|
@ -919,7 +918,7 @@ export class ChatPanel {
|
||||||
private clearInput(): void {
|
private clearInput(): void {
|
||||||
this.messageInput.innerHTML = '';
|
this.messageInput.innerHTML = '';
|
||||||
this.currentReferences = [];
|
this.currentReferences = [];
|
||||||
this.externalAttachments = [];
|
this.currentAttachments = [];
|
||||||
this.renderAttachedFiles();
|
this.renderAttachedFiles();
|
||||||
this.updatePlaceholder();
|
this.updatePlaceholder();
|
||||||
this.hideSuggestions();
|
this.hideSuggestions();
|
||||||
|
|
@ -932,7 +931,7 @@ export class ChatPanel {
|
||||||
handleSend(): void {
|
handleSend(): void {
|
||||||
const message = this.messageInput.textContent?.trim() || '';
|
const message = this.messageInput.textContent?.trim() || '';
|
||||||
if (message && this.props.onSendMessage) {
|
if (message && this.props.onSendMessage) {
|
||||||
this.props.onSendMessage(message, this.selectedModel);
|
this.props.onSendMessage(message, this.selectedModel, this.currentAttachments);
|
||||||
this.clearInput();
|
this.clearInput();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -1076,104 +1075,11 @@ export class ChatPanel {
|
||||||
|
|
||||||
// ----------------------------------------------- File Attachments ---------------------------------------------------------------
|
// ----------------------------------------------- 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 {
|
private renderAttachedFiles(): void {
|
||||||
if (this.externalAttachments.length === 0) {
|
if (this.currentAttachments.length === 0) {
|
||||||
this.attachedFilesContainer.style.display = 'none';
|
this.attachedFilesContainer.style.display = 'none';
|
||||||
this.attachedFilesContainer.innerHTML = '';
|
this.attachedFilesContainer.innerHTML = '';
|
||||||
return;
|
return;
|
||||||
|
|
@ -1182,7 +1088,7 @@ export class ChatPanel {
|
||||||
this.attachedFilesContainer.style.display = 'flex';
|
this.attachedFilesContainer.style.display = 'flex';
|
||||||
this.attachedFilesContainer.innerHTML = '';
|
this.attachedFilesContainer.innerHTML = '';
|
||||||
|
|
||||||
for (const attachment of this.externalAttachments) {
|
for (const attachment of this.currentAttachments) {
|
||||||
const box = this.createRemovableAttachmentBox(attachment);
|
const box = this.createRemovableAttachmentBox(attachment);
|
||||||
this.attachedFilesContainer.appendChild(box);
|
this.attachedFilesContainer.appendChild(box);
|
||||||
}
|
}
|
||||||
|
|
@ -1208,7 +1114,7 @@ export class ChatPanel {
|
||||||
removeBtn.innerHTML = '×';
|
removeBtn.innerHTML = '×';
|
||||||
removeBtn.setAttribute('aria-label', 'Удалить вложение');
|
removeBtn.setAttribute('aria-label', 'Удалить вложение');
|
||||||
removeBtn.addEventListener('click', () => {
|
removeBtn.addEventListener('click', () => {
|
||||||
this.removeExternalAttachment(attachment);
|
this.removeAttachment(attachment);
|
||||||
});
|
});
|
||||||
|
|
||||||
box.appendChild(icon);
|
box.appendChild(icon);
|
||||||
|
|
@ -1221,10 +1127,10 @@ export class ChatPanel {
|
||||||
/**
|
/**
|
||||||
* Удаляет внешнее вложение
|
* Удаляет внешнее вложение
|
||||||
*/
|
*/
|
||||||
private removeExternalAttachment(attachment: Attachment): void {
|
private removeAttachment(attachment: Attachment): void {
|
||||||
const index = this.externalAttachments.indexOf(attachment);
|
const index = this.currentAttachments.indexOf(attachment);
|
||||||
if (index > -1) {
|
if (index > -1) {
|
||||||
this.externalAttachments.splice(index, 1);
|
this.currentAttachments.splice(index, 1);
|
||||||
this.renderAttachedFiles();
|
this.renderAttachedFiles();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -15,8 +15,9 @@ export interface ChatHistoryItem {
|
||||||
export interface Attachment {
|
export interface Attachment {
|
||||||
type: 'file' | 'prompt' | 'external';
|
type: 'file' | 'prompt' | 'external';
|
||||||
name: string;
|
name: string;
|
||||||
path: string; // Относительный путь для 'file'/'prompt', абсолютный для 'external'
|
sourcePath: string; // Относительный для 'file'/'prompt', абсолютный для 'external'
|
||||||
permanent: boolean;
|
cachedPath: string; // Относительный путь к кешированному файлу
|
||||||
content?: string;
|
uuid: string; // UUID для идентификации кеша
|
||||||
|
permanent: boolean; // true для !@, !#, !$
|
||||||
mimeType?: string;
|
mimeType?: string;
|
||||||
}
|
}
|
||||||
|
|
@ -542,7 +542,7 @@
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
|
|
||||||
box-shadow: var(--shadow-l);
|
box-shadow: none; /* var(--shadow-l) */
|
||||||
|
|
||||||
z-index: auto;
|
z-index: auto;
|
||||||
position: static;
|
position: static;
|
||||||
|
|
@ -572,6 +572,21 @@
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Внешние файлы - одинарная ссылка $ */
|
||||||
|
.reference-box.external.single {
|
||||||
|
background: rgba(100, 150, 255, 0.1);
|
||||||
|
border: 1px solid rgba(100, 150, 255, 0.3);
|
||||||
|
color: var(--text-normal);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Внешние файлы - постоянная ссылка !$ */
|
||||||
|
.reference-box.external.permanent {
|
||||||
|
background: rgba(100, 150, 255, 0.2);
|
||||||
|
border: 1px solid rgba(100, 150, 255, 0.6);
|
||||||
|
color: var(--text-normal);
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
/* Reference Autocomplete */
|
/* Reference Autocomplete */
|
||||||
.reference-autocomplete {
|
.reference-autocomplete {
|
||||||
/* 200px для 5 элементов, возможно, нужно заменить на расчёт с помощью padding и размером шрифта */
|
/* 200px для 5 элементов, возможно, нужно заменить на расчёт с помощью padding и размером шрифта */
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,7 @@
|
||||||
* Обеспечивает поиск файлов в хранилище и кеширование результатов.
|
* Обеспечивает поиск файлов в хранилище и кеширование результатов.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { TFile } from 'obsidian';
|
import { Notice, TFile } from 'obsidian';
|
||||||
import LLMAgentPlugin from 'main';
|
import LLMAgentPlugin from 'main';
|
||||||
|
|
||||||
// ----------------------------------------------- Interfaces ---------------------------------------------------------------
|
// ----------------------------------------------- Interfaces ---------------------------------------------------------------
|
||||||
|
|
@ -237,4 +237,36 @@ export class AutocompleteManager {
|
||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async handleExternalFileSelection(): Promise<AutocompleteItem | null> {
|
||||||
|
if (!window.electron) {
|
||||||
|
new Notice('Выбор внешних файлов доступен только в desktop-версии', 3000);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const result = await window.electron.remote.dialog.showOpenDialog({
|
||||||
|
properties: ['openFile'],
|
||||||
|
title: 'Выберите файл для прикрепления'
|
||||||
|
});
|
||||||
|
|
||||||
|
if (result.canceled || !result.filePaths || result.filePaths.length === 0) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const filePath = result.filePaths[0];
|
||||||
|
const path = window.require('path');
|
||||||
|
const fileName = path.basename(filePath);
|
||||||
|
|
||||||
|
return {
|
||||||
|
name: fileName,
|
||||||
|
path: filePath,
|
||||||
|
type: 'external',
|
||||||
|
icon: 'paperclip'
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Ошибка при выборе файла:', error);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
219
src/references/CacheManager.ts
Normal file
219
src/references/CacheManager.ts
Normal file
|
|
@ -0,0 +1,219 @@
|
||||||
|
/**
|
||||||
|
* Управляет кешированием файлов (внутренних, внешних, промптов).
|
||||||
|
* Сохраняет файлы в указанной папке с UUID в имени.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { TFile, normalizePath } from 'obsidian';
|
||||||
|
import LLMAgentPlugin from 'main';
|
||||||
|
import { v4 as uuidv4 } from 'uuid'; // Нужно установить: npm install uuid @types/uuid
|
||||||
|
|
||||||
|
export class CacheManager {
|
||||||
|
private plugin: LLMAgentPlugin;
|
||||||
|
|
||||||
|
constructor(plugin: LLMAgentPlugin) {
|
||||||
|
this.plugin = plugin;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Возвращает абсолютный путь к папке кеша
|
||||||
|
*/
|
||||||
|
public getCacheFolderPath(): string {
|
||||||
|
const cacheFolder = this.plugin.settings.refCacheFolder;
|
||||||
|
|
||||||
|
// Если путь относительный, делаем его относительно vault
|
||||||
|
if (!this.isAbsolutePath(cacheFolder)) {
|
||||||
|
return this.plugin.app.vault.adapter.getBasePath() + '/' + cacheFolder;
|
||||||
|
}
|
||||||
|
|
||||||
|
return cacheFolder;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Проверяет, является ли путь абсолютным
|
||||||
|
*/
|
||||||
|
private isAbsolutePath(path: string): boolean {
|
||||||
|
// Windows: C:\, D:\, и т.д.
|
||||||
|
if (/^[a-zA-Z]:\\/.test(path)) return true;
|
||||||
|
// Unix: /
|
||||||
|
if (path.startsWith('/')) return true;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Создает папку кеша, если она не существует
|
||||||
|
*/
|
||||||
|
private async ensureCacheFolderExists(): Promise<void> {
|
||||||
|
const cachePath = this.getCacheFolderPath();
|
||||||
|
|
||||||
|
if (!window.require) return; // Web версия
|
||||||
|
|
||||||
|
const fs = window.require('fs').promises;
|
||||||
|
|
||||||
|
try {
|
||||||
|
await fs.access(cachePath);
|
||||||
|
} catch {
|
||||||
|
await fs.mkdir(cachePath, { recursive: true });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Кеширует файл из vault
|
||||||
|
*/
|
||||||
|
async cacheVaultFile(file: TFile): Promise<{ uuid: string; cachedPath: string }> {
|
||||||
|
await this.ensureCacheFolderExists();
|
||||||
|
|
||||||
|
const uuid = uuidv4();
|
||||||
|
const extension = file.extension;
|
||||||
|
const cachedFileName = `${file.basename}-${uuid}.${extension}`;
|
||||||
|
|
||||||
|
const content = await this.plugin.app.vault.readBinary(file);
|
||||||
|
|
||||||
|
const cacheFolderPath = this.getCacheFolderPath();
|
||||||
|
const cachedFilePath = `${cacheFolderPath}/${cachedFileName}`;
|
||||||
|
|
||||||
|
if (!window.require) {
|
||||||
|
throw new Error('Кеширование доступно только в desktop версии');
|
||||||
|
}
|
||||||
|
|
||||||
|
const fs = window.require('fs').promises;
|
||||||
|
await fs.writeFile(cachedFilePath, Buffer.from(content));
|
||||||
|
|
||||||
|
return {
|
||||||
|
uuid: uuid,
|
||||||
|
cachedPath: cachedFileName // Относительный путь
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Кеширует внешний файл
|
||||||
|
*/
|
||||||
|
async cacheExternalFile(absolutePath: string): Promise<{ uuid: string; cachedPath: string; name: string; mimeType: string }> {
|
||||||
|
await this.ensureCacheFolderExists();
|
||||||
|
|
||||||
|
if (!window.require) {
|
||||||
|
throw new Error('Кеширование доступно только в desktop версии');
|
||||||
|
}
|
||||||
|
|
||||||
|
const fs = window.require('fs').promises;
|
||||||
|
const path = window.require('path');
|
||||||
|
|
||||||
|
const content = await fs.readFile(absolutePath);
|
||||||
|
const fileName = path.basename(absolutePath);
|
||||||
|
const extension = path.extname(fileName).substring(1);
|
||||||
|
const baseName = path.basename(fileName, path.extname(fileName));
|
||||||
|
|
||||||
|
const uuid = uuidv4();
|
||||||
|
const cachedFileName = `${baseName}-${uuid}.${extension}`;
|
||||||
|
|
||||||
|
const cacheFolderPath = this.getCacheFolderPath();
|
||||||
|
const cachedFilePath = `${cacheFolderPath}/${cachedFileName}`;
|
||||||
|
|
||||||
|
await fs.writeFile(cachedFilePath, content);
|
||||||
|
|
||||||
|
return {
|
||||||
|
uuid: uuid,
|
||||||
|
cachedPath: cachedFileName,
|
||||||
|
name: fileName,
|
||||||
|
mimeType: this.getMimeType(fileName)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Восстанавливает кеш из исходного файла
|
||||||
|
*/
|
||||||
|
async restoreCache(sourcePath: string, type: 'file' | 'prompt' | 'external', uuid: string): Promise<string> {
|
||||||
|
if (type === 'external') {
|
||||||
|
// Для внешних файлов sourcePath - абсолютный
|
||||||
|
const result = await this.cacheExternalFile(sourcePath);
|
||||||
|
return result.cachedPath;
|
||||||
|
} else if (type === 'file') {
|
||||||
|
// Для внутренних файлов sourcePath - относительный
|
||||||
|
const file = this.plugin.app.vault.getAbstractFileByPath(sourcePath);
|
||||||
|
if (file instanceof TFile) {
|
||||||
|
const result = await this.cacheVaultFile(file);
|
||||||
|
return result.cachedPath;
|
||||||
|
}
|
||||||
|
} else if (type === 'prompt') {
|
||||||
|
// Для промптов
|
||||||
|
const promptsFolder = this.plugin.settings.promptsFolder;
|
||||||
|
const fullPath = this.isAbsolutePath(sourcePath)
|
||||||
|
? sourcePath
|
||||||
|
: `${promptsFolder}/${sourcePath}`;
|
||||||
|
|
||||||
|
const file = this.plugin.app.vault.getAbstractFileByPath(fullPath);
|
||||||
|
if (file instanceof TFile) {
|
||||||
|
const result = await this.cacheVaultFile(file);
|
||||||
|
return result.cachedPath;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new Error(`Не удалось восстановить кеш для ${sourcePath}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Читает содержимое из кеша
|
||||||
|
*/
|
||||||
|
async readCachedFile(cachedPath: string): Promise<ArrayBuffer> {
|
||||||
|
const cacheFolderPath = this.getCacheFolderPath();
|
||||||
|
const fullPath = `${cacheFolderPath}/${cachedPath}`;
|
||||||
|
|
||||||
|
if (!window.require) {
|
||||||
|
throw new Error('Чтение кеша доступно только в desktop версии');
|
||||||
|
}
|
||||||
|
|
||||||
|
const fs = window.require('fs').promises;
|
||||||
|
const buffer = await fs.readFile(fullPath);
|
||||||
|
return buffer.buffer;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Проверяет существование кешированного файла
|
||||||
|
*/
|
||||||
|
async cachedFileExists(cachedPath: string): Promise<boolean> {
|
||||||
|
const cacheFolderPath = this.getCacheFolderPath();
|
||||||
|
const fullPath = `${cacheFolderPath}/${cachedPath}`;
|
||||||
|
|
||||||
|
if (!window.require) return false;
|
||||||
|
|
||||||
|
const fs = window.require('fs').promises;
|
||||||
|
|
||||||
|
try {
|
||||||
|
await fs.access(fullPath);
|
||||||
|
return true;
|
||||||
|
} catch {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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',
|
||||||
|
webp: 'image/webp',
|
||||||
|
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';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -39,7 +39,16 @@ export class ReferenceAutocomplete {
|
||||||
async showAutocomplete(reference: ReferenceMatch, cursorRect: DOMRect): Promise<void> {
|
async showAutocomplete(reference: ReferenceMatch, cursorRect: DOMRect): Promise<void> {
|
||||||
this.currentReference = reference;
|
this.currentReference = reference;
|
||||||
|
|
||||||
// Получаем опции в зависимости от типа ссылки
|
// Для внешних файлов ($, !$) сразу открываем диалог
|
||||||
|
if (reference.type === 'external') {
|
||||||
|
const selectedFile = await this.autocompleteManager.handleExternalFileSelection();
|
||||||
|
if (selectedFile && this.onItemSelectedCallback) {
|
||||||
|
this.onItemSelectedCallback(selectedFile, reference);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Для файлов и промптов работаем как раньше
|
||||||
const options =
|
const options =
|
||||||
reference.type === 'file'
|
reference.type === 'file'
|
||||||
? await this.autocompleteManager.getFileOptions(reference.name)
|
? await this.autocompleteManager.getFileOptions(reference.name)
|
||||||
|
|
@ -51,7 +60,6 @@ export class ReferenceAutocomplete {
|
||||||
}
|
}
|
||||||
|
|
||||||
this.currentOptions = options;
|
this.currentOptions = options;
|
||||||
// Сбрасываем selection, если новые опции не содержат предыдущий выбранный элемент
|
|
||||||
if (this.selectedIndex >= this.currentOptions.length) {
|
if (this.selectedIndex >= this.currentOptions.length) {
|
||||||
this.selectedIndex = 0;
|
this.selectedIndex = 0;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -9,7 +9,6 @@ import { setIcon } from 'obsidian';
|
||||||
// ----------------------------------------------- Reference Box Component ---------------------------------------------------------------
|
// ----------------------------------------------- Reference Box Component ---------------------------------------------------------------
|
||||||
|
|
||||||
export class ReferenceBox {
|
export class ReferenceBox {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Создаёт визуальный бокс для отображения ссылки в сообщении
|
* Создаёт визуальный бокс для отображения ссылки в сообщении
|
||||||
* @param reference - информация о ссылке
|
* @param reference - информация о ссылке
|
||||||
|
|
@ -17,16 +16,24 @@ export class ReferenceBox {
|
||||||
*/
|
*/
|
||||||
static createBox(reference: FileReference): HTMLElement {
|
static createBox(reference: FileReference): HTMLElement {
|
||||||
const box = document.createElement('span');
|
const box = document.createElement('span');
|
||||||
|
box.contentEditable = 'false'; // ДОБАВЛЕНО: Делаем бокс нередактируемым
|
||||||
box.className = this.getBoxClasses(reference);
|
box.className = this.getBoxClasses(reference);
|
||||||
box.setAttribute('data-reference-type', reference.type);
|
box.setAttribute('data-reference-type', reference.type);
|
||||||
box.setAttribute('data-reference-name', reference.name);
|
box.setAttribute('data-reference-name', reference.name);
|
||||||
box.setAttribute('data-reference-path', reference.path);
|
box.setAttribute('data-reference-path', reference.path);
|
||||||
box.setAttribute('data-reference-permanent', reference.permanent.toString());
|
box.setAttribute('data-reference-permanent', reference.permanent.toString());
|
||||||
|
|
||||||
|
// ДОБАВЛЕНО: Сохраняем UUID, если есть
|
||||||
|
if (reference.uuid) {
|
||||||
|
box.setAttribute('data-reference-uuid', reference.uuid);
|
||||||
|
}
|
||||||
|
|
||||||
// Создаем иконку
|
// Создаем иконку
|
||||||
const icon = document.createElement('span');
|
const icon = document.createElement('span');
|
||||||
icon.className = 'reference-box-icon';
|
icon.className = 'reference-box-icon';
|
||||||
setIcon(icon, this.getIconName(reference));
|
|
||||||
|
// ИЗМЕНЕНО: Используем символы вместо setIcon для более предсказуемого отображения
|
||||||
|
icon.textContent = this.getIconSymbol(reference);
|
||||||
|
|
||||||
// Создаем текст с именем
|
// Создаем текст с именем
|
||||||
const nameSpan = document.createElement('span');
|
const nameSpan = document.createElement('span');
|
||||||
|
|
@ -59,12 +66,17 @@ export class ReferenceBox {
|
||||||
boxElement.setAttribute('data-reference-path', reference.path);
|
boxElement.setAttribute('data-reference-path', reference.path);
|
||||||
boxElement.setAttribute('data-reference-permanent', reference.permanent.toString());
|
boxElement.setAttribute('data-reference-permanent', reference.permanent.toString());
|
||||||
|
|
||||||
|
// ДОБАВЛЕНО: Обновляем UUID, если есть
|
||||||
|
if (reference.uuid) {
|
||||||
|
boxElement.setAttribute('data-reference-uuid', reference.uuid);
|
||||||
|
}
|
||||||
|
|
||||||
const iconElement = boxElement.querySelector('.reference-box-icon');
|
const iconElement = boxElement.querySelector('.reference-box-icon');
|
||||||
const nameElement = boxElement.querySelector('.reference-box-name');
|
const nameElement = boxElement.querySelector('.reference-box-name');
|
||||||
|
|
||||||
if (iconElement) {
|
if (iconElement) {
|
||||||
iconElement.innerHTML = '';
|
// ИЗМЕНЕНО: Используем символы вместо setIcon
|
||||||
setIcon(iconElement as HTMLElement, this.getIconName(reference));
|
iconElement.textContent = this.getIconSymbol(reference);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (nameElement) {
|
if (nameElement) {
|
||||||
|
|
@ -78,10 +90,11 @@ export class ReferenceBox {
|
||||||
* @returns информация о ссылке
|
* @returns информация о ссылке
|
||||||
*/
|
*/
|
||||||
static extractReference(boxElement: HTMLElement): FileReference | null {
|
static extractReference(boxElement: HTMLElement): FileReference | null {
|
||||||
const type = boxElement.getAttribute('data-reference-type') as 'file' | 'prompt';
|
const type = boxElement.getAttribute('data-reference-type') as 'file' | 'prompt' | 'external';
|
||||||
const name = boxElement.getAttribute('data-reference-name');
|
const name = boxElement.getAttribute('data-reference-name');
|
||||||
const path = boxElement.getAttribute('data-reference-path');
|
const path = boxElement.getAttribute('data-reference-path');
|
||||||
const permanent = boxElement.getAttribute('data-reference-permanent') === 'true';
|
const permanent = boxElement.getAttribute('data-reference-permanent') === 'true';
|
||||||
|
const uuid = boxElement.getAttribute('data-reference-uuid');
|
||||||
|
|
||||||
if (!type || !name || !path) {
|
if (!type || !name || !path) {
|
||||||
return null;
|
return null;
|
||||||
|
|
@ -91,7 +104,8 @@ export class ReferenceBox {
|
||||||
type,
|
type,
|
||||||
name,
|
name,
|
||||||
path,
|
path,
|
||||||
permanent
|
permanent,
|
||||||
|
uuid: uuid || undefined
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -109,32 +123,19 @@ export class ReferenceBox {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Возвращает имя иконки для ссылки
|
* Возвращает символ иконки для ссылки
|
||||||
|
* ИЗМЕНЕНО: Теперь возвращает символы вместо имен иконок Obsidian
|
||||||
*/
|
*/
|
||||||
private static getIconName(reference: FileReference): string {
|
private static getIconSymbol(reference: FileReference): string {
|
||||||
if (reference.type === 'prompt') {
|
if (reference.type === 'prompt') {
|
||||||
return 'zap';
|
return '⚡'; // Молния для промптов
|
||||||
}
|
}
|
||||||
|
|
||||||
// Определяем иконку по расширению файла
|
if (reference.type === 'external') {
|
||||||
const extension = reference.path.split('.').pop()?.toLowerCase();
|
return '📎'; // Знак доллара для внешних файлов
|
||||||
switch (extension) {
|
}
|
||||||
case 'md':
|
|
||||||
return 'file-text';
|
// Для файлов из vault используем @
|
||||||
case 'js':
|
return '📄';
|
||||||
case 'ts':
|
|
||||||
return 'code';
|
|
||||||
case 'json':
|
|
||||||
return 'brackets';
|
|
||||||
case 'css':
|
|
||||||
return 'palette';
|
|
||||||
case 'png':
|
|
||||||
case 'jpg':
|
|
||||||
case 'jpeg':
|
|
||||||
case 'gif':
|
|
||||||
return 'image';
|
|
||||||
default:
|
|
||||||
return 'file';
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -8,16 +8,19 @@ import LLMAgentPlugin from 'main';
|
||||||
import { FileReference, ReferenceParser } from './ReferenceParser';
|
import { FileReference, ReferenceParser } from './ReferenceParser';
|
||||||
import { TemplateEngine } from './TemplateEngine';
|
import { TemplateEngine } from './TemplateEngine';
|
||||||
import { Attachment, ChatHistoryItem } from 'src/components/models/ChatHistoryItem';
|
import { Attachment, ChatHistoryItem } from 'src/components/models/ChatHistoryItem';
|
||||||
|
import { CacheManager } from './CacheManager';
|
||||||
|
|
||||||
// ----------------------------------------------- Reference Expander ---------------------------------------------------------------
|
// ----------------------------------------------- Reference Expander ---------------------------------------------------------------
|
||||||
|
|
||||||
export class ReferenceExpander {
|
export class ReferenceExpander {
|
||||||
private plugin: LLMAgentPlugin;
|
private plugin: LLMAgentPlugin;
|
||||||
private templateEngine: TemplateEngine;
|
private templateEngine: TemplateEngine;
|
||||||
|
private cacheManager: CacheManager;
|
||||||
|
|
||||||
constructor(plugin: LLMAgentPlugin, templateEngine: TemplateEngine) {
|
constructor(plugin: LLMAgentPlugin, templateEngine: TemplateEngine) {
|
||||||
this.plugin = plugin;
|
this.plugin = plugin;
|
||||||
this.templateEngine = templateEngine;
|
this.templateEngine = templateEngine;
|
||||||
|
this.cacheManager = new CacheManager(plugin);
|
||||||
}
|
}
|
||||||
|
|
||||||
// ----------------------------------------------- Public Methods ---------------------------------------------------------------
|
// ----------------------------------------------- Public Methods ---------------------------------------------------------------
|
||||||
|
|
@ -75,8 +78,7 @@ export class ReferenceExpander {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Извлекает вложения из текста, НЕ раскрывая их в тексте.
|
* Извлекает вложения из текста и кеширует файлы
|
||||||
* Возвращает оригинальный текст и массив вложений.
|
|
||||||
*/
|
*/
|
||||||
async extractAttachments(
|
async extractAttachments(
|
||||||
text: string,
|
text: string,
|
||||||
|
|
@ -88,15 +90,60 @@ export class ReferenceExpander {
|
||||||
const attachments: Attachment[] = [];
|
const attachments: Attachment[] = [];
|
||||||
|
|
||||||
for (const reference of references) {
|
for (const reference of references) {
|
||||||
const content = await this.loadFileContent(reference);
|
try {
|
||||||
if (content !== null) {
|
let cachedPath: string;
|
||||||
|
let uuid: string;
|
||||||
|
let mimeType: string;
|
||||||
|
let sourcePath: string;
|
||||||
|
|
||||||
|
if (reference.type === 'external') {
|
||||||
|
// Кешируем внешний файл
|
||||||
|
const cacheResult = await this.cacheManager.cacheExternalFile(reference.path);
|
||||||
|
cachedPath = cacheResult.cachedPath;
|
||||||
|
uuid = cacheResult.uuid;
|
||||||
|
mimeType = cacheResult.mimeType;
|
||||||
|
sourcePath = reference.path; // Абсолютный
|
||||||
|
} else if (reference.type === 'file') {
|
||||||
|
// ИЗМЕНЕНО: Кешируем файл из vault
|
||||||
|
const file = this.plugin.app.vault.getAbstractFileByPath(reference.path);
|
||||||
|
if (file instanceof TFile) {
|
||||||
|
const cacheResult = await this.cacheManager.cacheVaultFile(file);
|
||||||
|
cachedPath = cacheResult.cachedPath;
|
||||||
|
uuid = cacheResult.uuid;
|
||||||
|
mimeType = this.getMimeType(file.name);
|
||||||
|
sourcePath = file.path; // Относительный
|
||||||
|
} else {
|
||||||
|
console.warn(`Файл не найден в vault: ${reference.path}`);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// prompt
|
||||||
|
// Кешируем промпт
|
||||||
|
const file = this.plugin.app.vault.getAbstractFileByPath(reference.path);
|
||||||
|
|
||||||
|
if (file instanceof TFile) {
|
||||||
|
const cacheResult = await this.cacheManager.cacheVaultFile(file);
|
||||||
|
cachedPath = cacheResult.cachedPath;
|
||||||
|
uuid = cacheResult.uuid;
|
||||||
|
mimeType = 'text/markdown';
|
||||||
|
sourcePath = file.path; // Относительный от promptsFolder
|
||||||
|
} else {
|
||||||
|
console.warn(`Промпт не найден: ${reference.path}`);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
attachments.push({
|
attachments.push({
|
||||||
type: reference.type,
|
type: reference.type,
|
||||||
name: reference.name,
|
name: reference.name,
|
||||||
path: reference.path,
|
sourcePath: sourcePath,
|
||||||
|
cachedPath: cachedPath,
|
||||||
|
uuid: uuid,
|
||||||
permanent: reference.permanent,
|
permanent: reference.permanent,
|
||||||
content: content
|
mimeType: mimeType
|
||||||
});
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`Не удалось закешировать ${reference.name}:`, error);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -106,6 +153,38 @@ export class ReferenceExpander {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private getMimeType(fileName: string): string {
|
||||||
|
// Та же логика, что в CacheManager
|
||||||
|
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';
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Подготавливает сообщения для LLM с учетом вложений.
|
* Подготавливает сообщения для LLM с учетом вложений.
|
||||||
* Раскрывает только постоянные (@@/##) ссылки и ссылки в последнем сообщении.
|
* Раскрывает только постоянные (@@/##) ссылки и ссылки в последнем сообщении.
|
||||||
|
|
|
||||||
|
|
@ -6,20 +6,21 @@
|
||||||
// ----------------------------------------------- Interfaces ---------------------------------------------------------------
|
// ----------------------------------------------- Interfaces ---------------------------------------------------------------
|
||||||
|
|
||||||
export interface ReferenceMatch {
|
export interface ReferenceMatch {
|
||||||
fullMatch: string; // "@filename" или "@@filename"
|
fullMatch: string;
|
||||||
prefix: string; // "@" или "@@"
|
prefix: string; // Теперь может быть @, !@, #, !#, $, !$
|
||||||
name: string; // "filename"
|
name: string;
|
||||||
startIndex: number;
|
startIndex: number;
|
||||||
endIndex: number;
|
endIndex: number;
|
||||||
type: 'file' | 'prompt';
|
type: 'file' | 'prompt' | 'external';
|
||||||
|
permanent: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface FileReference {
|
export interface FileReference {
|
||||||
type: 'file' | 'prompt';
|
type: 'file' | 'prompt' | 'external';
|
||||||
name: string;
|
name: string;
|
||||||
path: string;
|
path: string;
|
||||||
permanent: boolean; // true для @@ и ##
|
permanent: boolean;
|
||||||
// TODO: добавить snapshotId при реализации снапшотов
|
uuid?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
// ----------------------------------------------- Reference Parser ---------------------------------------------------------------
|
// ----------------------------------------------- Reference Parser ---------------------------------------------------------------
|
||||||
|
|
@ -33,9 +34,7 @@ export class ReferenceParser {
|
||||||
static parseReferences(text: string): ReferenceMatch[] {
|
static parseReferences(text: string): ReferenceMatch[] {
|
||||||
const matches: ReferenceMatch[] = [];
|
const matches: ReferenceMatch[] = [];
|
||||||
|
|
||||||
// Регулярное выражение для поиска @/@@ и #/## ссылок
|
const regex = /(![@#$]|[@#$])(?:"([^"]+)"|([^\s@#$"!]+))/g;
|
||||||
// Поддерживает имена файлов с пробелами, заключенные в кавычки или без пробелов
|
|
||||||
const regex = /(@{1,2}|#{1,2})(?:"([^"]+)"|([^\s@#"]+))/g;
|
|
||||||
|
|
||||||
let match;
|
let match;
|
||||||
while ((match = regex.exec(text)) !== null) {
|
while ((match = regex.exec(text)) !== null) {
|
||||||
|
|
@ -44,13 +43,22 @@ export class ReferenceParser {
|
||||||
const unquotedName = match[3];
|
const unquotedName = match[3];
|
||||||
const name = quotedName || unquotedName;
|
const name = quotedName || unquotedName;
|
||||||
|
|
||||||
|
// Определяем тип по первому символу префикса (после !)
|
||||||
|
const typeChar = prefix.replace('!', '');
|
||||||
|
let type: 'file' | 'prompt' | 'external';
|
||||||
|
|
||||||
|
if (typeChar === '@') type = 'file';
|
||||||
|
else if (typeChar === '#') type = 'prompt';
|
||||||
|
else type = 'external'; // $
|
||||||
|
|
||||||
matches.push({
|
matches.push({
|
||||||
fullMatch: match[0],
|
fullMatch: match[0],
|
||||||
prefix: prefix,
|
prefix: prefix,
|
||||||
name: name,
|
name: name,
|
||||||
startIndex: match.index,
|
startIndex: match.index,
|
||||||
endIndex: match.index + match[0].length,
|
endIndex: match.index + match[0].length,
|
||||||
type: prefix.startsWith('@') ? 'file' : 'prompt'
|
type: type,
|
||||||
|
permanent: prefix.startsWith('!')
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -64,11 +72,10 @@ export class ReferenceParser {
|
||||||
* @returns информация о текущей ссылке или null
|
* @returns информация о текущей ссылке или null
|
||||||
*/
|
*/
|
||||||
static getCurrentReference(text: string, cursorPosition: number): ReferenceMatch | null {
|
static getCurrentReference(text: string, cursorPosition: number): ReferenceMatch | null {
|
||||||
// Ищем незавершенные ссылки перед курсором
|
|
||||||
const beforeCursor = text.substring(0, cursorPosition);
|
const beforeCursor = text.substring(0, cursorPosition);
|
||||||
|
|
||||||
// Регулярное выражение для поиска начала ссылки без завершения
|
// ИЗМЕНЕНО: Поддержка !@, !#, !$
|
||||||
const incompleteRegex = /(@{1,2}|#{1,2})([^\s@#"]*?)$/;
|
const incompleteRegex = /(![@#$]|[@#$])([^\s@#$"!]*?)$/;
|
||||||
const match = beforeCursor.match(incompleteRegex);
|
const match = beforeCursor.match(incompleteRegex);
|
||||||
|
|
||||||
if (match) {
|
if (match) {
|
||||||
|
|
@ -76,13 +83,21 @@ export class ReferenceParser {
|
||||||
const partialName = match[2];
|
const partialName = match[2];
|
||||||
const startIndex = beforeCursor.length - match[0].length;
|
const startIndex = beforeCursor.length - match[0].length;
|
||||||
|
|
||||||
|
const typeChar = prefix.replace('!', '');
|
||||||
|
let type: 'file' | 'prompt' | 'external';
|
||||||
|
|
||||||
|
if (typeChar === '@') type = 'file';
|
||||||
|
else if (typeChar === '#') type = 'prompt';
|
||||||
|
else type = 'external';
|
||||||
|
|
||||||
return {
|
return {
|
||||||
fullMatch: match[0],
|
fullMatch: match[0],
|
||||||
prefix: prefix,
|
prefix: prefix,
|
||||||
name: partialName,
|
name: partialName,
|
||||||
startIndex: startIndex,
|
startIndex: startIndex,
|
||||||
endIndex: cursorPosition,
|
endIndex: cursorPosition,
|
||||||
type: prefix.startsWith('@') ? 'file' : 'prompt'
|
type: type,
|
||||||
|
permanent: prefix.startsWith('!')
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,7 @@
|
||||||
import { ItemView, Notice, WorkspaceLeaf } from 'obsidian';
|
import { ItemView, Notice, WorkspaceLeaf } from 'obsidian';
|
||||||
import { ChatPanel } from '../components/ChatPanel';
|
import { ChatPanel } from '../components/ChatPanel';
|
||||||
import LLMAgentPlugin from 'main';
|
import LLMAgentPlugin from 'main';
|
||||||
|
import { Attachment } from 'src/components/models/ChatHistoryItem';
|
||||||
|
|
||||||
export const CHAT_VIEW_TYPE = 'llm-agent-chat-view';
|
export const CHAT_VIEW_TYPE = 'llm-agent-chat-view';
|
||||||
|
|
||||||
|
|
@ -184,6 +185,8 @@ export class ChatView extends ItemView {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const cacheFolder = this.getCacheFolderPath();
|
||||||
|
|
||||||
// Шаг 1: Создаём узел пользователя с вложениями
|
// Шаг 1: Создаём узел пользователя с вложениями
|
||||||
const sendResponse = await fetch(`${this.plugin.settings.apiBaseUrl}/chat/send`, {
|
const sendResponse = await fetch(`${this.plugin.settings.apiBaseUrl}/chat/send`, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
|
|
@ -192,7 +195,8 @@ export class ChatView extends ItemView {
|
||||||
message: message,
|
message: message,
|
||||||
graph_id: this.selectedGraphId,
|
graph_id: this.selectedGraphId,
|
||||||
parent_node_id: this.currentNode,
|
parent_node_id: this.currentNode,
|
||||||
attachments: attachments || []
|
attachments: attachments || [],
|
||||||
|
cache_folder: cacheFolder
|
||||||
})
|
})
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -213,7 +217,7 @@ export class ChatView extends ItemView {
|
||||||
const systemPromptToUse =
|
const systemPromptToUse =
|
||||||
this.currentGraphCustomSystemPrompt !== null ? this.currentGraphCustomSystemPrompt : this.plugin.settings.systemPrompt;
|
this.currentGraphCustomSystemPrompt !== null ? this.currentGraphCustomSystemPrompt : this.plugin.settings.systemPrompt;
|
||||||
|
|
||||||
await this.streamLLMResponse(this.selectedGraphId, userNodeId, selectedModel, null, systemPromptToUse);
|
await this.streamLLMResponse(this.selectedGraphId, userNodeId, selectedModel, null, systemPromptToUse, cacheFolder);
|
||||||
} else {
|
} else {
|
||||||
throw new Error('Graph ID не определён после отправки сообщения');
|
throw new Error('Graph ID не определён после отправки сообщения');
|
||||||
}
|
}
|
||||||
|
|
@ -280,13 +284,15 @@ export class ChatView extends ItemView {
|
||||||
userNodeId: string,
|
userNodeId: string,
|
||||||
selectedModel?: string,
|
selectedModel?: string,
|
||||||
existingAssistantNodeId: string | null = null,
|
existingAssistantNodeId: string | null = null,
|
||||||
systemPrompt: string | null = null
|
systemPrompt: string | null = null,
|
||||||
|
cacheFolder?: string
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
const body = {
|
const body = {
|
||||||
graph_id: graphId,
|
graph_id: graphId,
|
||||||
user_node_id: userNodeId,
|
user_node_id: userNodeId,
|
||||||
system_prompt: systemPrompt,
|
system_prompt: systemPrompt,
|
||||||
model: selectedModel || this.plugin.settings.defaultModel
|
model: selectedModel || this.plugin.settings.defaultModel,
|
||||||
|
cache_folder: cacheFolder
|
||||||
};
|
};
|
||||||
|
|
||||||
if (existingAssistantNodeId) {
|
if (existingAssistantNodeId) {
|
||||||
|
|
@ -465,6 +471,8 @@ export class ChatView extends ItemView {
|
||||||
const systemPromptToUse =
|
const systemPromptToUse =
|
||||||
this.currentGraphCustomSystemPrompt !== null ? this.currentGraphCustomSystemPrompt : this.plugin.settings.systemPrompt;
|
this.currentGraphCustomSystemPrompt !== null ? this.currentGraphCustomSystemPrompt : this.plugin.settings.systemPrompt;
|
||||||
|
|
||||||
|
const cacheFolder = this.getCacheFolderPath();
|
||||||
|
|
||||||
const response = await fetch(`${this.plugin.settings.apiBaseUrl}/chat/regenerate`, {
|
const response = await fetch(`${this.plugin.settings.apiBaseUrl}/chat/regenerate`, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: { 'Content-Type': 'application/json' },
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
|
@ -472,7 +480,8 @@ export class ChatView extends ItemView {
|
||||||
graph_id: data.graphId,
|
graph_id: data.graphId,
|
||||||
node_id: data.nodeId,
|
node_id: data.nodeId,
|
||||||
model: data.model,
|
model: data.model,
|
||||||
system_prompt: systemPromptToUse
|
system_prompt: systemPromptToUse,
|
||||||
|
cache_folder: cacheFolder
|
||||||
})
|
})
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -566,4 +575,14 @@ export class ChatView extends ItemView {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ----------------------------------------------- Helpers ---------------------------------------------------------------
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Возвращает абсолютный путь к папке кеша
|
||||||
|
*/
|
||||||
|
private getCacheFolderPath(): string {
|
||||||
|
// TODO: Нужно обработать случай, когда cacheFolder пустой
|
||||||
|
return this.chatPanel?.cacheManager?.getCacheFolderPath() ?? '';
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
15
styles.css
15
styles.css
File diff suppressed because one or more lines are too long
Loading…
Reference in New Issue
Block a user