756 lines
28 KiB
TypeScript
756 lines
28 KiB
TypeScript
import { App, Editor, MarkdownView, Modal, Notice, Plugin, PluginSettingTab, Setting, ItemView, WorkspaceLeaf, TFile, FileSystemAdapter } from 'obsidian';
|
||
import { EventBus } from './src/utils/EventBus';
|
||
import { GraphView, GRAPH_VIEW_TYPE } from './src/views/GraphView';
|
||
import { ChatView, CHAT_VIEW_TYPE } from './src/views/ChatView';
|
||
import { WebSocketService } from './src/utils/WebSocketService';
|
||
import { TemplateEngine } from './src/references/TemplateEngine';
|
||
import { AutocompleteManager } from './src/references/AutocompleteManager';
|
||
import { ReferenceAutocomplete } from './src/references/ReferenceAutocomplete';
|
||
import { ReferenceParser } from './src/references/ReferenceParser';
|
||
|
||
import 'src/css/styles.css';
|
||
|
||
import { LLM_DEFAULTS } from 'src/constants';
|
||
|
||
// ----------------------------------------------- Settings ---------------------------------------------------------------
|
||
|
||
interface LLMAgentSettings {
|
||
apiBaseUrl: string;
|
||
systemPrompt: string;
|
||
defaultModel: string;
|
||
promptsFolder: string;
|
||
enableFileReferences: boolean;
|
||
enablePromptReferences: boolean;
|
||
excludedFolders: string[];
|
||
refCacheFolder: string;
|
||
|
||
temperatureChat: number;
|
||
maxTokensChatDefault: string;
|
||
|
||
summarizationModel: string;
|
||
voiceCommandModel: string;
|
||
logsFolder: string;
|
||
voiceInterval: number;
|
||
metricsInterval: number;
|
||
voiceDisplayLimit: number;
|
||
voiceContextLimit: number;
|
||
temperatureVoice: number;
|
||
maxTokensVoiceRegular: string;
|
||
maxTokensVoiceCommands: string;
|
||
promptRegular: string;
|
||
promptCommands: string;
|
||
promptMetrics: string;
|
||
markerStart: string;
|
||
markerStop: string;
|
||
|
||
voskModelPath: string;
|
||
showVoicePanel: boolean;
|
||
|
||
dictationStartAction: 'start' | 'continue';
|
||
|
||
lastActiveGraphId: string | null;
|
||
lastActiveNodeId: string | null;
|
||
}
|
||
|
||
const DEFAULT_SETTINGS: LLMAgentSettings = {
|
||
apiBaseUrl: 'http://localhost:5000/api',
|
||
systemPrompt: 'Ты полезный ИИ ассистент.',
|
||
defaultModel: LLM_DEFAULTS.MODEL,
|
||
promptsFolder: 'prompts',
|
||
enableFileReferences: true,
|
||
enablePromptReferences: true,
|
||
excludedFolders: ['.obsidian', 'llm-agent-backend', 'templates', 'ref-cache'],
|
||
refCacheFolder: 'ref-cache',
|
||
|
||
temperatureChat: LLM_DEFAULTS.TEMPERATURE,
|
||
maxTokensChatDefault: '',
|
||
|
||
summarizationModel: '',
|
||
voiceCommandModel: '',
|
||
logsFolder: 'audio-logs',
|
||
voiceInterval: 1,
|
||
metricsInterval: 15,
|
||
voiceDisplayLimit: 30000,
|
||
voiceContextLimit: 30000,
|
||
temperatureVoice: LLM_DEFAULTS.TEMPERATURE,
|
||
maxTokensVoiceRegular: '',
|
||
maxTokensVoiceCommands: '',
|
||
promptRegular: '',
|
||
promptCommands: '',
|
||
promptMetrics: '',
|
||
markerStart: 'старт',
|
||
markerStop: 'стоп',
|
||
|
||
voskModelPath: '',
|
||
showVoicePanel: false,
|
||
|
||
dictationStartAction: 'start',
|
||
|
||
lastActiveGraphId: null,
|
||
lastActiveNodeId: null,
|
||
};
|
||
|
||
// ----------------------------------------------- Main Plugin Class ---------------------------------------------------------------
|
||
|
||
export default class LLMAgentPlugin extends Plugin {
|
||
settings: LLMAgentSettings;
|
||
eventBus: EventBus;
|
||
webSocketService: WebSocketService;
|
||
activeStreamControllers: Map<string, AbortController>;
|
||
availableModels: string[];
|
||
|
||
async syncSettings() {
|
||
try {
|
||
const engine = new TemplateEngine(this);
|
||
|
||
// Клонируем настройки, чтобы не испортить исходные пути в конфиге плагина
|
||
const settingsToSync = JSON.parse(JSON.stringify(this.settings));
|
||
|
||
// Собираем промпты (раскрываем все цепочки ссылок) для работы с аудиологами на бекенде
|
||
settingsToSync.systemPrompt = await engine.processRawSettingValue(this.settings.systemPrompt);
|
||
settingsToSync.promptRegular = await engine.processRawSettingValue(this.settings.promptRegular);
|
||
settingsToSync.promptCommands = await engine.processRawSettingValue(this.settings.promptCommands);
|
||
settingsToSync.promptMetrics = await engine.processRawSettingValue(this.settings.promptMetrics);
|
||
|
||
const adapter = this.app.vault.adapter;
|
||
if (adapter instanceof FileSystemAdapter) {
|
||
// Соединяем базовый путь хранилища и относительный путь к логам
|
||
settingsToSync.absoluteLogsPath = adapter.getBasePath() +
|
||
(this.settings.logsFolder ? '/' + this.settings.logsFolder : '');
|
||
}
|
||
|
||
const response = await fetch(`${this.settings.apiBaseUrl}/settings/sync`, {
|
||
method: 'POST',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify(settingsToSync)
|
||
});
|
||
if (response.ok) {
|
||
console.log("LLM Agent: Settings synced with backend.");
|
||
}
|
||
} catch (e) {
|
||
console.error("LLM Agent: Failed to sync settings", e);
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Загружает список доступных моделей с бэкенда.
|
||
* @returns {Promise<void>}
|
||
*/
|
||
async fetchAvailableModels(): Promise<void> {
|
||
try {
|
||
const response = await fetch(`${this.settings.apiBaseUrl}/models`);
|
||
if (!response.ok) {
|
||
throw new Error(`Ошибка загрузки моделей: ${response.status}`);
|
||
}
|
||
this.availableModels = await response.json();
|
||
} catch (error) {
|
||
console.error('Ошибка загрузки доступных моделей:', error);
|
||
// Fallback к базовым моделям
|
||
this.availableModels = [LLM_DEFAULTS.MODEL, 'gpt-4o-mini', 'claude-3-5-sonnet-20241022'];
|
||
}
|
||
}
|
||
|
||
async onload() {
|
||
await this.loadSettings();
|
||
|
||
this.eventBus = new EventBus();
|
||
|
||
this.webSocketService = new WebSocketService(this.eventBus, this.settings.apiBaseUrl);
|
||
this.webSocketService.connect();
|
||
|
||
// Регистрируем кастомные views
|
||
this.registerView(GRAPH_VIEW_TYPE, (leaf) => new GraphView(leaf, this));
|
||
this.registerView(CHAT_VIEW_TYPE, (leaf) => new ChatView(leaf, this));
|
||
|
||
// Добавляем команды
|
||
this.addCommand({
|
||
id: 'open-llm-agent-graph',
|
||
name: 'Открыть граф диалогов LLM Agent',
|
||
callback: () => this.activateGraphView()
|
||
});
|
||
|
||
this.addCommand({
|
||
id: 'open-llm-agent-chat',
|
||
name: 'Открыть чат LLM Agent',
|
||
callback: () => this.activateChatView()
|
||
});
|
||
|
||
this.addCommand({
|
||
id: 'new-llm-agent-chat',
|
||
name: 'Новый чат LLM Agent',
|
||
callback: () => this.handleNewChat()
|
||
});
|
||
|
||
// Добавляем иконку в ribbon
|
||
this.addRibbonIcon('brain-circuit', 'LLM Agent', () => {
|
||
this.activateGraphView();
|
||
this.activateChatView();
|
||
});
|
||
|
||
console.log('LLM Agent plugin загружен');
|
||
|
||
this.activeStreamControllers = new Map<string, AbortController>();
|
||
|
||
this.eventBus.on('stream-started', this.handleStreamStarted.bind(this));
|
||
this.eventBus.on('stream-ended', this.handleStreamEnded.bind(this));
|
||
|
||
this.eventBus.on('stop-all-streams', () => {
|
||
this.activeStreamControllers.forEach((controller, nodeId) => {
|
||
controller.abort();
|
||
});
|
||
this.activeStreamControllers.clear();
|
||
new Notice('Отменены все генерации во всех графах', 2000);
|
||
this.eventBus.emit('global-stream-state-changed', false);
|
||
});
|
||
|
||
// This adds a settings tab so the user can configure various aspects of the plugin
|
||
this.addSettingTab(new SampleSettingTab(this.app, this));
|
||
|
||
await this.fetchAvailableModels();
|
||
|
||
// Ждем полной инициализации хранилища перед вызовом зависимых функций
|
||
// await this.syncSettings();
|
||
this.app.workspace.onLayoutReady(async () => {
|
||
await this.syncSettings();
|
||
});
|
||
|
||
this.registerEvent(
|
||
this.app.vault.on('modify', async (file) => {
|
||
// Если изменился любой md файл, перестраховываемся и синхроним настройки
|
||
// (Backend получит обновленный текст промптов, если на них есть ссылки)
|
||
if (file instanceof TFile && file.extension === 'md' && file.path.startsWith(this.settings.promptsFolder)) {
|
||
// TODO: Проверить, что срабатывает
|
||
await this.syncSettings();
|
||
}
|
||
})
|
||
);
|
||
}
|
||
|
||
onunload() {
|
||
if (this.webSocketService) {
|
||
this.webSocketService.disconnect();
|
||
}
|
||
|
||
this.activeStreamControllers.clear();
|
||
|
||
this.eventBus.off('stream-started', this.handleStreamStarted);
|
||
this.eventBus.off('stream-ended', this.handleStreamEnded);
|
||
|
||
console.log('LLM Agent plugin выгружен');
|
||
}
|
||
|
||
// ----------------------------------------------- Settings Management ---------------------------------------------------------------
|
||
|
||
async loadSettings() {
|
||
this.settings = Object.assign({}, DEFAULT_SETTINGS, await this.loadData());
|
||
}
|
||
|
||
async saveSettings() {
|
||
await this.saveData(this.settings);
|
||
this.eventBus.emit('settings-changed');
|
||
}
|
||
|
||
// ----------------------------------------------- View Management ---------------------------------------------------------------
|
||
|
||
async activateGraphView() {
|
||
const existingLeaf = this.app.workspace.getLeavesOfType(GRAPH_VIEW_TYPE)[0];
|
||
if (existingLeaf) {
|
||
this.app.workspace.revealLeaf(existingLeaf);
|
||
} else {
|
||
await this.app.workspace.getLeaf('tab').setViewState({
|
||
type: GRAPH_VIEW_TYPE,
|
||
active: true
|
||
});
|
||
}
|
||
}
|
||
|
||
async activateChatView() {
|
||
const existingLeaf = this.app.workspace.getLeavesOfType(CHAT_VIEW_TYPE)[0];
|
||
if (existingLeaf) {
|
||
this.app.workspace.revealLeaf(existingLeaf);
|
||
} else {
|
||
const rightLeaf = this.app.workspace.getRightLeaf(false);
|
||
if (rightLeaf) {
|
||
await rightLeaf.setViewState({
|
||
type: CHAT_VIEW_TYPE,
|
||
active: true
|
||
});
|
||
}
|
||
}
|
||
}
|
||
|
||
// ----------------------------------------------- Event Handlers ---------------------------------------------------------------
|
||
|
||
handleNewChat() {
|
||
this.eventBus.emit('new-chat');
|
||
}
|
||
|
||
handleStreamStarted(data: { nodeId: string; controller: AbortController }): void {
|
||
this.activeStreamControllers.set(data.nodeId, data.controller);
|
||
this.eventBus.emit('global-stream-state-changed', true);
|
||
}
|
||
|
||
handleStreamEnded(data: { nodeId: string }): void {
|
||
this.activeStreamControllers.delete(data.nodeId);
|
||
if (this.activeStreamControllers.size === 0) {
|
||
this.eventBus.emit('global-stream-state-changed', false);
|
||
}
|
||
}
|
||
}
|
||
|
||
// ----------------------------------------------- Sample Setting Tab ---------------------------------------------------------------
|
||
|
||
class SampleSettingTab extends PluginSettingTab {
|
||
plugin: LLMAgentPlugin;
|
||
|
||
constructor(app: App, plugin: LLMAgentPlugin) {
|
||
super(app, plugin);
|
||
this.plugin = plugin;
|
||
}
|
||
|
||
display(): void {
|
||
const { containerEl } = this;
|
||
containerEl.empty();
|
||
|
||
containerEl.createEl('h2', { text: 'Основные настройки' });
|
||
|
||
new Setting(containerEl)
|
||
.setName('Модель по умолчанию')
|
||
.setDesc('Выберите модель для основного чата.')
|
||
.addDropdown((dropdown) => {
|
||
this.plugin.availableModels.forEach(model => dropdown.addOption(model, model));
|
||
dropdown.setValue(this.plugin.settings.defaultModel)
|
||
.onChange(async (value) => {
|
||
this.plugin.settings.defaultModel = value;
|
||
await this.plugin.saveSettings();
|
||
await this.plugin.syncSettings();
|
||
});
|
||
});
|
||
|
||
new Setting(containerEl)
|
||
.setName('Модель суммаризации')
|
||
.setDesc('Выберите модель для суммаризации.')
|
||
.addDropdown((dropdown) => {
|
||
this.plugin.availableModels.forEach(model => dropdown.addOption(model, model));
|
||
dropdown.setValue(this.plugin.settings.summarizationModel)
|
||
.onChange(async (value) => {
|
||
this.plugin.settings.summarizationModel = value;
|
||
await this.plugin.saveSettings();
|
||
await this.plugin.syncSettings();
|
||
});
|
||
});
|
||
|
||
// Этот промпт будет отправляться LLM перед каждым диалогом
|
||
this.addPromptAutocomplete(new Setting(containerEl).setName('Системный промпт'), 'systemPrompt');
|
||
|
||
new Setting(containerEl)
|
||
.setName('Температура чата по умолчанию')
|
||
.setDesc(`Значение от 0.0 до 2.0 (по умолчанию ${LLM_DEFAULTS.TEMPERATURE}). Влияет на креативность.`)
|
||
.addText((text) =>
|
||
text
|
||
.setPlaceholder(String(LLM_DEFAULTS.TEMPERATURE))
|
||
.setValue(String(this.plugin.settings.temperatureChat))
|
||
.onChange(async (value) => {
|
||
const num = parseFloat(value);
|
||
this.plugin.settings.temperatureChat = isNaN(num) ? LLM_DEFAULTS.TEMPERATURE : num;
|
||
await this.plugin.saveSettings();
|
||
await this.plugin.syncSettings();
|
||
})
|
||
);
|
||
|
||
new Setting(containerEl)
|
||
.setName('Макс. токенов чата по умолчанию')
|
||
.setDesc('Оставьте пустым для отсутствия лимита.')
|
||
.addText((text) =>
|
||
text
|
||
.setPlaceholder('Лимит')
|
||
.setValue(this.plugin.settings.maxTokensChatDefault)
|
||
.onChange(async (value) => {
|
||
this.plugin.settings.maxTokensChatDefault = value;
|
||
await this.plugin.saveSettings();
|
||
await this.plugin.syncSettings();
|
||
})
|
||
);
|
||
|
||
new Setting(containerEl)
|
||
.setName('API Base URL')
|
||
.setDesc('Базовый URL для API LLM Agent')
|
||
.addText((text) =>
|
||
text
|
||
.setPlaceholder('http://localhost:5000/api')
|
||
.setValue(this.plugin.settings.apiBaseUrl)
|
||
.onChange(async (value) => {
|
||
this.plugin.settings.apiBaseUrl = value;
|
||
await this.plugin.saveSettings();
|
||
await this.plugin.syncSettings();
|
||
})
|
||
);
|
||
|
||
containerEl.createEl('h2', { text: 'Голосовое управление (Voice Service)' });
|
||
|
||
new Setting(containerEl)
|
||
.setName('Отображать голосовую панель')
|
||
.setDesc('Показывать нижнюю панель Voice Service в графе диалогов.')
|
||
.addToggle((toggle) =>
|
||
toggle.setValue(this.plugin.settings.showVoicePanel).onChange(async (value) => {
|
||
this.plugin.settings.showVoicePanel = value;
|
||
await this.plugin.saveSettings();
|
||
// Событие 'settings-changed' уже вызывается внутри saveSettings,
|
||
// поэтому панель будет скрываться/появляться мгновенно
|
||
})
|
||
);
|
||
|
||
new Setting(containerEl)
|
||
.setName('Модель команд')
|
||
.setDesc('LLM для обработки голосовых команд.')
|
||
.addDropdown((dropdown) => {
|
||
this.plugin.availableModels.forEach(model => dropdown.addOption(model, model));
|
||
dropdown.setValue(this.plugin.settings.voiceCommandModel)
|
||
.onChange(async (value) => {
|
||
this.plugin.settings.voiceCommandModel = value;
|
||
await this.plugin.saveSettings();
|
||
await this.plugin.syncSettings();
|
||
});
|
||
});
|
||
|
||
// Инструкции для Промпта №1.
|
||
this.addPromptAutocomplete(
|
||
new Setting(containerEl).setName('Регулярный промпт'),
|
||
'promptRegular'
|
||
);
|
||
|
||
// Инструкции для Промпта №2.
|
||
this.addPromptAutocomplete(
|
||
new Setting(containerEl).setName('Промпт команд'),
|
||
'promptCommands'
|
||
);
|
||
|
||
this.addPromptAutocomplete(
|
||
new Setting(containerEl).setName('Промпт качественной аналитики'),
|
||
'promptMetrics'
|
||
);
|
||
|
||
new Setting(containerEl)
|
||
.setName('Температура Voice')
|
||
.setDesc(`Температура для голосовых запросов (по умолчанию ${LLM_DEFAULTS.TEMPERATURE})`)
|
||
.addText((text) =>
|
||
text
|
||
.setPlaceholder(String(LLM_DEFAULTS.TEMPERATURE))
|
||
.setValue(String(this.plugin.settings.temperatureVoice))
|
||
.onChange(async (value) => {
|
||
const num = parseFloat(value);
|
||
this.plugin.settings.temperatureVoice = isNaN(num) ? LLM_DEFAULTS.TEMPERATURE : num;
|
||
await this.plugin.saveSettings();
|
||
await this.plugin.syncSettings();
|
||
})
|
||
);
|
||
|
||
new Setting(containerEl)
|
||
.setName('Макс. токенов Voice (Регулярный)')
|
||
.setDesc('Лимит токенов для регулярного анализа. Оставьте пустым для безлимита.')
|
||
.addText((text) =>
|
||
text
|
||
.setPlaceholder('Лимит')
|
||
.setValue(this.plugin.settings.maxTokensVoiceRegular)
|
||
.onChange(async (value) => {
|
||
this.plugin.settings.maxTokensVoiceRegular = value;
|
||
await this.plugin.saveSettings();
|
||
await this.plugin.syncSettings();
|
||
})
|
||
);
|
||
|
||
new Setting(containerEl)
|
||
.setName('Макс. токенов Voice (Команды)')
|
||
.setDesc('Лимит токенов для голосовых команд. Оставьте пустым для безлимита.')
|
||
.addText((text) =>
|
||
text
|
||
.setPlaceholder('Лимит')
|
||
.setValue(this.plugin.settings.maxTokensVoiceCommands)
|
||
.onChange(async (value) => {
|
||
this.plugin.settings.maxTokensVoiceCommands = value;
|
||
await this.plugin.saveSettings();
|
||
await this.plugin.syncSettings();
|
||
})
|
||
);
|
||
|
||
new Setting(containerEl)
|
||
.setName('Интервал регулярного анализа (мин)')
|
||
.setDesc('Как часто LLM анализирует последние фразы (Prompt 1).')
|
||
.addText((text) =>
|
||
text
|
||
.setValue(String(this.plugin.settings.voiceInterval))
|
||
.onChange(async (value) => {
|
||
this.plugin.settings.voiceInterval = Number(value);
|
||
await this.plugin.saveSettings();
|
||
await this.plugin.syncSettings();
|
||
})
|
||
);
|
||
|
||
new Setting(containerEl)
|
||
.setName('Интервал аналитики (мин)')
|
||
.setDesc('Как часто Python и LLM генерируют графики и качественный анализ (Таб 4).')
|
||
.addText((text) =>
|
||
text
|
||
.setValue(String(this.plugin.settings.metricsInterval))
|
||
.onChange(async (value) => {
|
||
this.plugin.settings.metricsInterval = Number(value) || 15;
|
||
await this.plugin.saveSettings();
|
||
await this.plugin.syncSettings();
|
||
})
|
||
);
|
||
|
||
new Setting(containerEl)
|
||
.setName('Действие диктовки')
|
||
.setDesc('При нажатии записи в чате, если голос не запущен, как его стартовать?')
|
||
.addDropdown((dropdown) => {
|
||
dropdown.addOption('start', 'Начать заново (Start New)');
|
||
dropdown.addOption('continue', 'Продолжить (Continue)');
|
||
dropdown.setValue(this.plugin.settings.dictationStartAction)
|
||
.onChange(async (value: 'start' | 'continue') => {
|
||
this.plugin.settings.dictationStartAction = value;
|
||
await this.plugin.saveSettings();
|
||
await this.plugin.syncSettings();
|
||
});
|
||
});
|
||
|
||
new Setting(containerEl)
|
||
.setName('Папка аудио-логов')
|
||
.setDesc('Папка внутри Obsidian, куда будут сохраняться текстовые расшифровки (в формате .md). Относительно корня хранилища.')
|
||
.addText((text) =>
|
||
text
|
||
.setPlaceholder('audio-logs')
|
||
.setValue(this.plugin.settings.logsFolder)
|
||
.onChange(async (value) => {
|
||
// Убираем лишние слэши для корректности пути
|
||
this.plugin.settings.logsFolder = value.replace(/^\/+|\/+$/g, '').trim();
|
||
await this.plugin.saveSettings();
|
||
await this.plugin.syncSettings();
|
||
})
|
||
);
|
||
|
||
new Setting(containerEl)
|
||
.setName('Объем контекста (символы)')
|
||
.setDesc('Сколько последних символов транскрибации отправлять в модель.')
|
||
.addText((text) =>
|
||
text
|
||
.setValue(String(this.plugin.settings.voiceContextLimit))
|
||
.onChange(async (value) => {
|
||
this.plugin.settings.voiceContextLimit = Number(value);
|
||
await this.plugin.saveSettings();
|
||
await this.plugin.syncSettings();
|
||
})
|
||
);
|
||
|
||
new Setting(containerEl)
|
||
.setName('Маркер начала команды')
|
||
.setDesc('Слово, после которого текст считается командой (Response 2).')
|
||
.addText((text) =>
|
||
text
|
||
.setPlaceholder('старт')
|
||
.setValue(this.plugin.settings.markerStart)
|
||
.onChange(async (value) => {
|
||
this.plugin.settings.markerStart = value;
|
||
await this.plugin.saveSettings();
|
||
await this.plugin.syncSettings();
|
||
})
|
||
);
|
||
|
||
new Setting(containerEl)
|
||
.setName('Маркер конца команды')
|
||
.setDesc('Слово, завершающее захват голосовой команды.')
|
||
.addText((text) =>
|
||
text
|
||
.setPlaceholder('стоп')
|
||
.setValue(this.plugin.settings.markerStop)
|
||
.onChange(async (value) => {
|
||
this.plugin.settings.markerStop = value;
|
||
await this.plugin.saveSettings();
|
||
await this.plugin.syncSettings();
|
||
})
|
||
);
|
||
|
||
new Setting(containerEl)
|
||
.setName('Путь к модели Vosk')
|
||
.setDesc('Абсолютный путь к папке с распакованной языковой моделью (например: C:\\vosk-models\\vosk-model-small-ru)')
|
||
.addText((text) =>
|
||
text
|
||
.setPlaceholder('Путь к модели на диске')
|
||
.setValue(this.plugin.settings.voskModelPath)
|
||
.onChange(async (value) => {
|
||
this.plugin.settings.voskModelPath = value.replace(/"/g, '').trim(); // Убираем кавычки, если скопировали путь как путь
|
||
await this.plugin.saveSettings();
|
||
await this.plugin.syncSettings();
|
||
})
|
||
);
|
||
|
||
containerEl.createEl('h2', { text: 'Управление ссылками и файлами' });
|
||
|
||
new Setting(containerEl)
|
||
.setName('Папка с промптами')
|
||
.setDesc('Путь к папке, содержащей промпты для команд #/##')
|
||
.addText((text) =>
|
||
text
|
||
.setPlaceholder('prompts')
|
||
.setValue(this.plugin.settings.promptsFolder)
|
||
.onChange(async (value) => {
|
||
this.plugin.settings.promptsFolder = value;
|
||
await this.plugin.saveSettings();
|
||
await this.plugin.syncSettings();
|
||
})
|
||
);
|
||
|
||
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();
|
||
await this.plugin.syncSettings();
|
||
})
|
||
);
|
||
|
||
// Настройки ссылок на файлы
|
||
new Setting(containerEl)
|
||
.setName('Включить ссылки на файлы')
|
||
.setDesc('Разрешить использование @/@@ ссылок на файлы в хранилище')
|
||
.addToggle((toggle) =>
|
||
toggle.setValue(this.plugin.settings.enableFileReferences).onChange(async (value) => {
|
||
this.plugin.settings.enableFileReferences = value;
|
||
await this.plugin.saveSettings();
|
||
await this.plugin.syncSettings();
|
||
})
|
||
);
|
||
|
||
// Настройки ссылок на промпты
|
||
new Setting(containerEl)
|
||
.setName('Включить ссылки на промпты')
|
||
.setDesc('Разрешить использование #/## ссылок на промпты')
|
||
.addToggle((toggle) =>
|
||
toggle.setValue(this.plugin.settings.enablePromptReferences).onChange(async (value) => {
|
||
this.plugin.settings.enablePromptReferences = value;
|
||
await this.plugin.saveSettings();
|
||
await this.plugin.syncSettings();
|
||
})
|
||
);
|
||
|
||
// Новое поле для настройки исключенных папок
|
||
new Setting(containerEl)
|
||
.setName('Исключенные папки для ссылок')
|
||
.setDesc('Список относительных путей к папкам, файлы из которых не будут предлагаться в автодополнении ссылок (@/@@).')
|
||
.addToggle((toggle) =>
|
||
toggle.setValue(this.plugin.settings.enableFileReferences).onChange(async (value) => {
|
||
this.plugin.settings.enableFileReferences = value;
|
||
await this.plugin.saveSettings();
|
||
await this.plugin.syncSettings();
|
||
})
|
||
);
|
||
|
||
// Динамический список исключенных папок
|
||
containerEl.createEl('h3', { text: 'Исключенные папки' });
|
||
this.plugin.settings.excludedFolders.forEach((folder, index) => {
|
||
new Setting(containerEl)
|
||
.setClass('llm-agent-excluded-folder-item') // Добавляем класс для стилизации
|
||
.addText((text) => {
|
||
text.setPlaceholder('Путь к папке (например, templates/)' + index)
|
||
.setValue(folder)
|
||
.onChange(async (value) => {
|
||
this.plugin.settings.excludedFolders[index] = value;
|
||
await this.plugin.saveSettings();
|
||
await this.plugin.syncSettings();
|
||
// Очищаем кэш автокомплита при изменении настроек
|
||
this.plugin.eventBus.emit('settings-changed');
|
||
});
|
||
})
|
||
.addExtraButton((button) => {
|
||
button
|
||
.setIcon('x')
|
||
.setTooltip('Удалить папку')
|
||
.onClick(async () => {
|
||
this.plugin.settings.excludedFolders.splice(index, 1);
|
||
await this.plugin.saveSettings();
|
||
await this.plugin.syncSettings();
|
||
this.display(); // Перерисовать настройки для обновления списка
|
||
// Очищаем кэш автокомплита при изменении настроек
|
||
this.plugin.eventBus.emit('settings-changed');
|
||
});
|
||
});
|
||
});
|
||
|
||
new Setting(containerEl).addButton((button) => {
|
||
button
|
||
.setButtonText('Добавить папку')
|
||
.setIcon('plus')
|
||
.onClick(async () => {
|
||
this.plugin.settings.excludedFolders.push(''); // Добавляем пустое поле
|
||
await this.plugin.saveSettings();
|
||
await this.plugin.syncSettings();
|
||
this.display(); // Перерисовать настройки для обновления списка
|
||
});
|
||
});
|
||
}
|
||
|
||
private addPromptAutocomplete(setting: Setting, settingKey: keyof LLMAgentSettings) {
|
||
const manager = new AutocompleteManager(this.plugin);
|
||
|
||
setting.addTextArea((text) => {
|
||
const el = text.inputEl;
|
||
const autocomplete = new ReferenceAutocomplete(el, manager);
|
||
|
||
autocomplete.onItemSelected(async (item, match) => {
|
||
const val = el.value;
|
||
const before = val.substring(0, match.startIndex);
|
||
const after = val.substring(match.endIndex);
|
||
|
||
// Если в имени есть пробел — оборачиваем в кавычки
|
||
const nameToInsert = item.name.includes(' ') ? `"${item.name}"` : item.name;
|
||
const newValue = before + "#" + nameToInsert + after;
|
||
|
||
// Обновляем визуально в поле
|
||
el.value = newValue;
|
||
|
||
// Обновляем в конфиге (используем приведение типа, чтобы TS не ругался)
|
||
(this.plugin.settings as any)[settingKey] = newValue;
|
||
|
||
await this.plugin.saveSettings();
|
||
await this.plugin.syncSettings();
|
||
|
||
autocomplete.hideAutocomplete();
|
||
});
|
||
|
||
text.setPlaceholder('Впишите текст или начните с # для выбора промпта')
|
||
.setValue(String(this.plugin.settings[settingKey]))
|
||
.onChange(async (value) => {
|
||
// Прямое присваивание через приведение к any для обхода строгой проверки типов
|
||
(this.plugin.settings as any)[settingKey] = value;
|
||
|
||
await this.plugin.saveSettings();
|
||
await this.plugin.syncSettings();
|
||
|
||
const cursor = el.selectionStart;
|
||
const textValue = el.value;
|
||
const ref = ReferenceParser.getCurrentReference(textValue, cursor);
|
||
|
||
if (ref && ref.type === 'prompt') {
|
||
// Используем getBoundingClientRect для позиционирования меню под полем ввода
|
||
const rect = el.getBoundingClientRect();
|
||
autocomplete.showAutocomplete(ref, rect);
|
||
} else {
|
||
autocomplete.hideAutocomplete();
|
||
}
|
||
});
|
||
|
||
el.addEventListener('keydown', (e) => {
|
||
if (autocomplete.isVisible()) {
|
||
if (autocomplete.handleKeydown(e)) {
|
||
e.stopPropagation();
|
||
}
|
||
}
|
||
});
|
||
});
|
||
}
|
||
}
|