diff --git a/main.ts b/main.ts index 453940f..257095e 100644 --- a/main.ts +++ b/main.ts @@ -7,6 +7,8 @@ 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 { CustomTool } from './src/components/models/ChatHistoryItem'; +import { v4 as uuidv4 } from 'uuid'; import 'src/css/styles.css'; @@ -19,6 +21,7 @@ interface LLMAgentSettings { systemPrompt: string; defaultModel: string; promptsFolder: string; + scriptsFolder: string; // Новое: Папка со скриптами питона enableFileReferences: boolean; enablePromptReferences: boolean; excludedFolders: string[]; @@ -26,6 +29,8 @@ interface LLMAgentSettings { temperatureChat: number; maxTokensChatDefault: string; + agencyModeEnabled: boolean; // Новое: По умолчанию включен ли агент + customTools: CustomTool[]; // Новое: Список созданных инструментов summarizationModel: string; voiceCommandModel: string; @@ -57,6 +62,7 @@ const DEFAULT_SETTINGS: LLMAgentSettings = { systemPrompt: 'Ты полезный ИИ ассистент.', defaultModel: LLM_DEFAULTS.MODEL, promptsFolder: 'prompts', + scriptsFolder: 'scripts', enableFileReferences: true, enablePromptReferences: true, excludedFolders: ['.obsidian', 'llm-agent-backend', 'templates', 'ref-cache'], @@ -64,6 +70,8 @@ const DEFAULT_SETTINGS: LLMAgentSettings = { temperatureChat: LLM_DEFAULTS.TEMPERATURE, maxTokensChatDefault: '', + agencyModeEnabled: true, + customTools: [], summarizationModel: '', voiceCommandModel: '', @@ -112,6 +120,23 @@ export default class LLMAgentPlugin extends Plugin { settingsToSync.promptCommands = await engine.processRawSettingValue(this.settings.promptCommands); settingsToSync.promptMetrics = await engine.processRawSettingValue(this.settings.promptMetrics); + // Раскрываем скрытые промпты у всех тулов + for (let i = 0; i < settingsToSync.customTools.length; i++) { + if (settingsToSync.customTools[i].hiddenPrompt) { + settingsToSync.customTools[i].hiddenPromptExpanded = await engine.processRawSettingValue(settingsToSync.customTools[i].hiddenPrompt); + } else { + settingsToSync.customTools[i].hiddenPromptExpanded = ""; + } + + // Конвертируем относительный путь скрипта в абсолютный для бэкенда + const adapter = this.app.vault.adapter; + if (adapter instanceof FileSystemAdapter && settingsToSync.customTools[i].scriptPath) { + const vaultPath = adapter.getBasePath(); + const cleanScriptPath = settingsToSync.customTools[i].scriptPath.replace('@', '').trim(); + settingsToSync.customTools[i].scriptPath = `${vaultPath}/${cleanScriptPath}`; + } + } + const adapter = this.app.vault.adapter; if (adapter instanceof FileSystemAdapter) { // Соединяем базовый путь хранилища и относительный путь к логам @@ -125,7 +150,7 @@ export default class LLMAgentPlugin extends Plugin { body: JSON.stringify(settingsToSync) }); if (response.ok) { - console.log("LLM Agent: Settings synced with backend."); + console.log("LLM Agent: Settings and Tools synced with backend."); } } catch (e) { console.error("LLM Agent: Failed to sync settings", e); @@ -275,9 +300,9 @@ export default class LLMAgentPlugin extends Plugin { type: CHAT_VIEW_TYPE, active: true }); - } } } + } // ----------------------------------------------- Event Handlers --------------------------------------------------------------- @@ -302,6 +327,7 @@ export default class LLMAgentPlugin extends Plugin { class SampleSettingTab extends PluginSettingTab { plugin: LLMAgentPlugin; + activeTabId: string = 'basic'; constructor(app: App, plugin: LLMAgentPlugin) { super(app, plugin); @@ -312,9 +338,68 @@ class SampleSettingTab extends PluginSettingTab { const { containerEl } = this; containerEl.empty(); - containerEl.createEl('h2', { text: 'Основные настройки' }); + // Навигация (Табы) + const tabHeader = containerEl.createDiv({ cls: 'settings-tab-header' }); + tabHeader.style.cssText = 'display: flex; gap: 10px; margin-bottom: 20px; border-bottom: 1px solid var(--background-modifier-border); padding-bottom: 10px;'; + + const tabs = [ + { id: 'basic', label: '⚙️ Основные' }, + { id: 'agency', label: '🤖 Агент (ReAct Tools)' }, + { id: 'voice', label: '🎤 Голос (Vosk)' } + ]; - new Setting(containerEl) + const contentContainer = containerEl.createDiv({ cls: 'settings-tab-content' }); + + const renderTab = (id: string) => { + contentContainer.empty(); + + // Подсветка активной кнопки + Array.from(tabHeader.children).forEach((child: HTMLElement) => { + if (child.dataset.tabId === id) { + child.style.backgroundColor = 'var(--interactive-accent)'; + child.style.color = 'var(--text-on-accent)'; + } else { + child.style.backgroundColor = 'var(--background-secondary)'; + child.style.color = 'var(--text-normal)'; + } + }); + + if (id === 'basic') this.renderBasicSettings(contentContainer); + if (id === 'agency') this.renderAgencySettings(contentContainer); + if (id === 'voice') this.renderVoiceSettings(contentContainer); + }; + + tabs.forEach(tab => { + const btn = tabHeader.createEl('button', { text: tab.label }); + btn.dataset.tabId = tab.id; + btn.style.cssText = 'border: none; padding: 6px 12px; border-radius: 6px; cursor: pointer; transition: 0.2s;'; + btn.onclick = () => { + this.activeTabId = tab.id; + renderTab(this.activeTabId); + }; + }); + + renderTab(this.activeTabId); + } + + renderBasicSettings(container: HTMLElement) { + container.createEl('h3', { text: 'Глобальные параметры' }); + + new Setting(container) + .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(); + }) + ); + + new Setting(container) .setName('Модель по умолчанию') .setDesc('Выберите модель для основного чата.') .addDropdown((dropdown) => { @@ -327,7 +412,7 @@ class SampleSettingTab extends PluginSettingTab { }); }); - new Setting(containerEl) + new Setting(container) .setName('Модель суммаризации') .setDesc('Выберите модель для суммаризации.') .addDropdown((dropdown) => { @@ -340,10 +425,7 @@ class SampleSettingTab extends PluginSettingTab { }); }); - // Этот промпт будет отправляться LLM перед каждым диалогом - this.addPromptAutocomplete(new Setting(containerEl).setName('Системный промпт'), 'systemPrompt'); - - new Setting(containerEl) + new Setting(container) .setName('Температура чата по умолчанию') .setDesc(`Значение от 0.0 до 2.0 (по умолчанию ${LLM_DEFAULTS.TEMPERATURE}). Влияет на креативность.`) .addText((text) => @@ -358,7 +440,7 @@ class SampleSettingTab extends PluginSettingTab { }) ); - new Setting(containerEl) + new Setting(container) .setName('Макс. токенов чата по умолчанию') .setDesc('Оставьте пустым для отсутствия лимита.') .addText((text) => @@ -372,220 +454,12 @@ class SampleSettingTab extends PluginSettingTab { }) ); - 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(); - }) - ); + // Этот промпт будет отправляться LLM перед каждым диалогом + this.addPromptAutocomplete(new Setting(container).setName('Глобальный системный промпт'), 'systemPrompt'); - 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) + container.createEl('h3', { text: 'Управление ссылками и файлами' }); + + new Setting(container) .setName('Папка с промптами') .setDesc('Путь к папке, содержащей промпты для команд #/##') .addText((text) => @@ -599,7 +473,7 @@ class SampleSettingTab extends PluginSettingTab { }) ); - new Setting(containerEl) + new Setting(container) .setName('Папка кеша для вложений') .setDesc('Абсолютный или относительный путь к папке для кеширования файлов') .addText((text) => @@ -614,7 +488,7 @@ class SampleSettingTab extends PluginSettingTab { ); // Настройки ссылок на файлы - new Setting(containerEl) + new Setting(container) .setName('Включить ссылки на файлы') .setDesc('Разрешить использование @/@@ ссылок на файлы в хранилище') .addToggle((toggle) => @@ -626,7 +500,7 @@ class SampleSettingTab extends PluginSettingTab { ); // Настройки ссылок на промпты - new Setting(containerEl) + new Setting(container) .setName('Включить ссылки на промпты') .setDesc('Разрешить использование #/## ссылок на промпты') .addToggle((toggle) => @@ -638,7 +512,7 @@ class SampleSettingTab extends PluginSettingTab { ); // Новое поле для настройки исключенных папок - new Setting(containerEl) + new Setting(container) .setName('Исключенные папки для ссылок') .setDesc('Список относительных путей к папкам, файлы из которых не будут предлагаться в автодополнении ссылок (@/@@).') .addToggle((toggle) => @@ -650,9 +524,9 @@ class SampleSettingTab extends PluginSettingTab { ); // Динамический список исключенных папок - containerEl.createEl('h3', { text: 'Исключенные папки' }); + container.createEl('h3', { text: 'Исключенные папки' }); this.plugin.settings.excludedFolders.forEach((folder, index) => { - new Setting(containerEl) + new Setting(container) .setClass('llm-agent-excluded-folder-item') // Добавляем класс для стилизации .addText((text) => { text.setPlaceholder('Путь к папке (например, templates/)' + index) @@ -680,7 +554,7 @@ class SampleSettingTab extends PluginSettingTab { }); }); - new Setting(containerEl).addButton((button) => { + new Setting(container).addButton((button) => { button .setButtonText('Добавить папку') .setIcon('plus') @@ -693,6 +567,298 @@ class SampleSettingTab extends PluginSettingTab { }); } + renderAgencySettings(container: HTMLElement) { + container.createEl('h3', { text: 'Фреймворк Инструментов (ReAct Engine)' }); + + new Setting(container) + .setName('Агентность включена по умолчанию') + .setDesc('Разрешает ИИ вызывать ваши питоновские скрипты по мере необходимости') + .addToggle((t) => t.setValue(this.plugin.settings.agencyModeEnabled).onChange(async (v) => { + this.plugin.settings.agencyModeEnabled = v; + await this.plugin.saveSettings(); + })); + + container.createEl('h4', { text: 'Кастомные инструменты (Mini-Agents)', cls: 'agency-tools-header' }); + + if (!this.plugin.settings.customTools) this.plugin.settings.customTools = []; + + this.plugin.settings.customTools.forEach((tool, index) => { + const toolDiv = container.createDiv({ + attr: { style: 'border: 1px solid var(--interactive-accent); padding: 10px; margin-bottom: 15px; border-radius: 8px; background-color: var(--background-secondary-alt);' } + }); + + new Setting(toolDiv) + .setName(`Название (только англ.)`) + .addText(t => t.setValue(tool.name).onChange(async v => { + tool.name = v.replace(/[^a-zA-Z0-9_]/g, ''); + await this.plugin.saveSettings(); + })) + .addExtraButton(b => b.setIcon('trash').setTooltip('Удалить этот инструмент').onClick(async () => { + this.plugin.settings.customTools.splice(index, 1); + await this.plugin.saveSettings(); + this.display(); // Полностью перерисовать + })); + + new Setting(toolDiv) + .setName('Описание для ИИ (Когда применять?)') + .addTextArea(t => { + t.inputEl.style.width = '100%'; + t.setValue(tool.description).onChange(async v => { + tool.description = v; + await this.plugin.saveSettings(); + }); + }); + + const scriptSetting = new Setting(toolDiv) + .setName('Скрипт и Метод') + .setDesc('путь/скрипт.py:название_метода (Используйте @)'); + this.addPromptAutocompleteStrict(scriptSetting, tool, 'scriptPath', 'py'); + + const formatSetting = new Setting(toolDiv) + .setName('Форматирующий промпт (Пост-процессинг)') + .setDesc('Стиль ответа, прикладываемый к результату работы скрипта (Используйте #)'); + this.addPromptAutocompleteStrict(formatSetting, tool, 'hiddenPrompt'); + }); + + new Setting(container).addButton(b => b.setButtonText('➕ Добавить новый инструмент').setCta().onClick(async () => { + this.plugin.settings.customTools.push({ id: uuidv4(), name: 'New_Tool', description: '', scriptPath: '', hiddenPrompt: '' }); + await this.plugin.saveSettings(); + this.display(); + })); + } + + renderVoiceSettings(container: HTMLElement) { + container.createEl('h3', { text: 'Транскрибация Vosk' }); + + new Setting(container) + .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(container) + .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(); + }) + ); + + new Setting(container) + .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(); + }); + }); + + new Setting(container) + .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(container) + .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(container) + .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(container) + .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(container) + .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(container) + .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(container) + .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(container) + .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(); + }) + ); + + + container.createEl('h3', { text: 'Интервалы и Аналитика' }); + + new Setting(container) + .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(container) + .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(); + }) + ); + + container.createEl('h3', { text: 'Промпты голоса' }); + this.addPromptAutocomplete(new Setting(container).setName('Регулярный промпт'), 'promptRegular'); + this.addPromptAutocomplete(new Setting(container).setName('Промпт команд (Marker)'), 'promptCommands'); + this.addPromptAutocomplete(new Setting(container).setName('Промпт Качественной Аналитики'), 'promptMetrics'); + } + + private addPromptAutocompleteStrict(setting: Setting, obj: any, key: string, filterExtension?: string) { + const manager = new AutocompleteManager(this.plugin); + setting.addText((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); + + // ИЗМЕНЕНО: Если это файл (скрипт), берем ВЕСЬ ПУТЬ к нему, а не просто Имя. + // Это позволяет бэкенду выполнять скрипты из подпапок (напр. scripts/my_script.py) + const textToUse = item.type === 'file' ? item.path : item.name; + const nameToInsert = textToUse.includes(' ') ? `"${textToUse}"` : textToUse; + + const prefix = item.type === 'file' ? '@' : '#'; + const newValue = before + prefix + nameToInsert + after; + + el.value = newValue; + obj[key] = newValue; + await this.plugin.saveSettings(); + await this.plugin.syncSettings(); + autocomplete.hideAutocomplete(); + }); + text.setValue(String(obj[key])).onChange(async (value) => { + obj[key] = value; + await this.plugin.saveSettings(); + await this.plugin.syncSettings(); + const cursor = el.selectionStart; + const ref = ReferenceParser.getCurrentReference(el.value, cursor); + if (ref) { + const rect = el.getBoundingClientRect(); + // ИЗМЕНЕНО: Отправляем фильтр расширения (если он был задан) + autocomplete.showAutocomplete(ref, rect, filterExtension); + } else { + autocomplete.hideAutocomplete(); + } + }); + el.addEventListener('keydown', (e) => { if (autocomplete.isVisible() && autocomplete.handleKeydown(e)) { e.stopPropagation(); } }); + }); + } + private addPromptAutocomplete(setting: Setting, settingKey: keyof LLMAgentSettings) { const manager = new AutocompleteManager(this.plugin); diff --git a/src/components/ChatPanel.ts b/src/components/ChatPanel.ts index 9738824..3f5383b 100644 --- a/src/components/ChatPanel.ts +++ b/src/components/ChatPanel.ts @@ -20,7 +20,7 @@ import { LLM_DEFAULTS } from 'src/constants'; interface ChatPanelProps { chatHistory: ChatHistoryItem[]; - onSendMessage: (message: string, selectedModel?: string, attachments?: Attachment[], customMaxTokens?: number, customTemperature?: number) => void; + onSendMessage: (message: string, selectedModel?: string, attachments?: Attachment[], customMaxTokens?: number, customTemperature?: number, agencyMode?: boolean) => void; selectedModel?: string; plugin: LLMAgentPlugin; currentGraphCustomSystemPrompt: string | null; @@ -64,6 +64,8 @@ export class ChatPanel { dictationLastLen: number = 0; hasGlobalStreams: boolean = false; + agencyToggle: HTMLInputElement; + // ----------------------------------------------- Reference System Fields --------------------------------------------------------------- cacheManager: CacheManager; @@ -211,15 +213,13 @@ export class ChatPanel { this.modelDropdown.style.display = 'none'; } - handleInput(): void { - const value = this.messageInput.textContent || ''; - const commands = ['/help', '/imagine', '/analyze', '/subtitles_meet', '/subtitles_teams', '/summarize', '/chat']; - - if (value.startsWith('/')) { - const filteredCommands = commands.filter((cmd) => cmd.startsWith(value.toLowerCase())); - this.showSuggestions(filteredCommands); - } else { - this.hideSuggestions(); + async setAgencyToggle(): Promise { + if (this.props.plugin.settings) { + this.props.plugin.settings.agencyModeEnabled = this.agencyToggle.checked; + if (typeof this.props.plugin.saveSettings === 'function') { + await this.props.plugin.saveSettings(); // TODO: Подумать, насколько это должно быть здесь + await this.props.plugin.syncSettings(); + } } } @@ -467,6 +467,45 @@ export class ChatPanel { this.processStreamUpdates(); } + toolExecutionUpdate(nodeId: string, toolName: string, status: 'start' | 'end', step: number = 1): void { + const messageEl = this.container.querySelector(`.message[data-node-id="${nodeId}"]`); + if (!messageEl) return; + + const contentDiv = messageEl.querySelector('.chat-message-content') as HTMLElement; + + let indicator = contentDiv.querySelector('.agent-steps-info'); + if (!indicator) { + indicator = document.createElement('div'); + indicator.className = 'agent-steps-info'; + // Вставляем В НАЧАЛО контента + contentDiv.insertBefore(indicator, contentDiv.firstChild); + } + + if (status === 'start') { + indicator.innerHTML = ` +
+ 🛠 Шаг ${step}: Подготовка и вызов ${toolName}... +
+ `; + } else if (status === 'end') { + indicator.innerHTML = ` +
+ ✅ Шаг ${step}: Инструмент ${toolName} успешно завершен +
+ `; + // Прячем значок успеха через 3.5 секунды, чтобы не мозолил глаза + setTimeout(() => { + if (indicator && indicator.parentNode) { + indicator.remove(); + } + }, 3500); + } + + // Автоскролл + const scrollContainer = this.container.closest('.view-content') as HTMLElement; + if (scrollContainer) scrollContainer.scrollTop = scrollContainer.scrollHeight; + } + /** * Обрабатывает очередь так, чтобы рендер происходил не чаще чем раз в 300мс * и дожидался окончания предыдущей отрисовки. @@ -614,6 +653,15 @@ export class ChatPanel { ⚙️ + +
+ + 🤖 Агент +
+
Temp: @@ -657,6 +705,8 @@ export class ChatPanel { this.attachedFilesContainer = this.container.querySelector('#attached-files-container') as HTMLDivElement; + this.agencyToggle = this.container.querySelector('#agency-toggle') as HTMLInputElement; + this.tempInput = this.container.querySelector('#temp-input') as HTMLInputElement; this.tempResetBtn = this.container.querySelector('#temp-reset') as HTMLButtonElement; this.maxTokensInput = this.container.querySelector('#max-tokens-input') as HTMLInputElement; @@ -723,7 +773,6 @@ export class ChatPanel { }); this.messageInput.addEventListener('input', () => { - this.handleInput(); this.handleReferenceInput(); this.updatePlaceholder(); }); @@ -741,6 +790,10 @@ export class ChatPanel { this.toggleModelDropdown(); }); + + + + this.stopButton.addEventListener('click', () => this.handleStopGeneration()); this.maxTokensResetBtn.addEventListener('click', () => { @@ -766,6 +819,34 @@ export class ChatPanel { this.updateScrollButtonsVisibility(); }); + this.agencyToggle.addEventListener('change', async () => { + this.setAgencyToggle(); + }); + + // ??? + this.props.plugin.eventBus.on('agent-step', (data: { nodeId: string, step: number, tool: string, status: string }) => { + // Ищем контейнер сообщения по его ID (тот, что сейчас стримится) + const messageEl = this.container.querySelector(`.message[data-node-id="${data.nodeId}"]`); + if (!messageEl) return; + + let indicator = messageEl.querySelector('.agent-steps-info') as HTMLElement; + if (!indicator) { + indicator = document.createElement('div'); + indicator.className = 'agent-steps-info'; + // Вставляем В НАЧАЛО контента сообщения + const content = messageEl.querySelector('.chat-message-content'); + content?.prepend(indicator); + } + + if (data.status === 'start') { + indicator.innerHTML = `
🛠 Шаг ${data.step}: Выполняю ${data.tool}...
`; + } else if (data.status === 'end') { + indicator.innerHTML = `
✅ Шаг ${data.step}: ${data.tool} завершен
`; + // Через 3 секунды можно скрыть индикатор шага, чтобы не мешался + setTimeout(() => indicator.remove(), 3000); + } + }); + // Close dropdown when clicking outside document.addEventListener('click', (e) => { if (!this.modelSelectButton.contains(e.target as Node) && !this.modelDropdown.contains(e.target as Node)) { @@ -1136,7 +1217,9 @@ export class ChatPanel { const valTemp = this.tempInput?.value; const parsedTemp = valTemp ? parseFloat(valTemp) : undefined; - this.props.onSendMessage(originalText || '', this.selectedModel, allAttachments, parsedMaxTokens, parsedTemp); + const isAgencyActive = this.agencyToggle.checked; + + this.props.onSendMessage(originalText || '', this.selectedModel, allAttachments, parsedMaxTokens, parsedTemp, isAgencyActive); } this.clearInput(); @@ -1174,7 +1257,9 @@ export class ChatPanel { const valTemp = this.tempInput?.value; const parsedTemp = valTemp ? parseFloat(valTemp) : undefined; - this.props.onSendMessage(message, this.selectedModel, this.currentAttachments, parsedMaxTokens, parsedTemp); + const isAgencyActive = this.agencyToggle.checked; + + this.props.onSendMessage(message, this.selectedModel, this.currentAttachments, parsedMaxTokens, parsedTemp, isAgencyActive); this.clearInput(); } } @@ -1260,13 +1345,16 @@ export class ChatPanel { const valTemp = this.tempInput?.value; const parsedTemp = valTemp ? parseFloat(valTemp) : undefined; + + const isAgencyActive = this.agencyToggle.checked; this.props.plugin.eventBus.emit('regenerate-message', { graphId, nodeId, model: this.selectedModel, maxTokens: parsedMaxTokens, - temperature: parsedTemp + temperature: parsedTemp, + agencyMode: isAgencyActive }); }); diff --git a/src/components/GraphPanel.ts b/src/components/GraphPanel.ts index be6c146..6afbef0 100644 --- a/src/components/GraphPanel.ts +++ b/src/components/GraphPanel.ts @@ -105,13 +105,15 @@ const BaseNodeWrapper: React.FC = ({ data, isConnectable, // или перелопачивать код и хранить в узлах и бд настройки, использованные при генерации, как будто, ещё хуже let maxTokens = (data.app as any).plugins?.plugins?.['llm-agent-plugin']?.settings?.maxTokensChatDefault || ''; let temperature = (data.app as any).plugins?.plugins?.['llm-agent-plugin']?.settings?.temperatureChat || LLM_DEFAULTS.TEMPERATURE; + let agencyMode = (data.app as any).plugins?.plugins?.['llm-agent-plugin']?.settings?.agencyModeEnabled || false; eventBus.emit('regenerate-message', { graphId: data.graphId, nodeId: data.nodeId, model: model, // НОВОЕ: Добавляем модель maxTokens: maxTokens, - temperature: temperature + temperature: temperature, + agencyMode: agencyMode }); } }; diff --git a/src/components/models/ChatHistoryItem.ts b/src/components/models/ChatHistoryItem.ts index 49e604c..4d74a29 100644 --- a/src/components/models/ChatHistoryItem.ts +++ b/src/components/models/ChatHistoryItem.ts @@ -20,4 +20,13 @@ export interface Attachment { uuid: string; // UUID для идентификации кеша permanent: boolean; // true для !@, !#, !$ mimeType?: string; +} + +export interface CustomTool { + id: string; + name: string; + description: string; + scriptPath: string; + hiddenPrompt: string; + hiddenPromptExpanded?: string; } \ No newline at end of file diff --git a/src/css/styles.css b/src/css/styles.css index 2c3c39d..73c73f9 100644 --- a/src/css/styles.css +++ b/src/css/styles.css @@ -308,23 +308,18 @@ } .chat-controls { - display: flex; - justify-content: space-between; - align-items: center; - font-size: 12px; - color: var(--text-muted); + display: flex; + flex-wrap: wrap; /* Элементы переносятся, если панель узкая */ + gap: 6px; + justify-content: space-between; + align-items: center; + padding-top: 4px; } - -.control-buttons-left { - display: flex; - align-items: center; - gap: 8px; -} - -.control-buttons-right { - display: flex; - align-items: center; - gap: 8px; +.control-buttons-left, .control-buttons-right { + display: flex; + flex-wrap: wrap; + gap: 4px; + align-items: center; } .model-select-container { @@ -1660,3 +1655,98 @@ 100% { transform: scale(1); opacity: 1; } } +/* ----------------------------------------------- Agency toggle --------------------------------------------------------------- */ + +.agency-toggle-container { + display: flex; + align-items: center; + gap: 6px; + margin-right: 4px; + font-size: 11px; + color: var(--text-muted); +} + +.switch { + position: relative; + display: inline-block; + width: 28px; + height: 16px; +} +.switch input { opacity: 0; width: 0; height: 0; } +.slider { + position: absolute; + cursor: pointer; + top: 0; left: 0; right: 0; bottom: 0; + background-color: var(--background-modifier-border); + transition: .2s; + border-radius: 16px; +} +.slider:before { + position: absolute; + content: ""; + height: 12px; width: 12px; + left: 2px; bottom: 2px; + background-color: white; + transition: .2s; + border-radius: 50%; +} +input:checked + .slider { background-color: var(--interactive-accent); } +input:checked + .slider:before { transform: translateX(12px); } + +/* Визуальный блок работы инструмента (Scratchpad-нода) */ +.tool-execution-block { + margin: 8px 0; + padding: 8px; + border-radius: 6px; + background-color: var(--background-secondary-alt); + border-left: 3px solid var(--interactive-accent); + font-family: var(--font-monospace); + font-size: 0.85em; + color: var(--text-muted); + animation: pulse-bg 1.5s infinite; +} +.tool-execution-block.finished { + animation: none; + border-left: 3px solid var(--text-success); + opacity: 0.8; +} +@keyframes pulse-bg { + 0% { opacity: 0.6; } + 50% { opacity: 1; } + 100% { opacity: 0.6; } +} + + + + +/* --- Индикаторы работы Агента (ReAct Steps) --- */ +.agent-steps-info { + margin-bottom: 8px; + display: flex; + flex-direction: column; + gap: 4px; +} + +.step-badge { + display: inline-flex; + padding: 6px 10px; + background-color: var(--background-secondary-alt); + border-left: 3px solid var(--interactive-accent); + border-radius: 4px; + font-family: var(--font-monospace); + font-size: 0.85em; + color: var(--text-muted); + animation: pulse-bg 1.5s infinite; +} + +.step-badge.success { + animation: none; + border-left-color: var(--text-success); + opacity: 0.8; +} + +@keyframes pulse-bg { + 0% { opacity: 0.6; } + 50% { opacity: 1; } + 100% { opacity: 0.6; } +} \ No newline at end of file diff --git a/src/references/AutocompleteManager.ts b/src/references/AutocompleteManager.ts index 3042d59..6518016 100644 --- a/src/references/AutocompleteManager.ts +++ b/src/references/AutocompleteManager.ts @@ -43,20 +43,22 @@ export class AutocompleteManager { /** * Получает список файлов для автокомплита * @param query - поисковый запрос + * @param filterExtension - опциональный фильтр по расширению (например, 'py') * @returns массив файлов для автокомплита */ - async getFileOptions(query: string): Promise { + async getFileOptions(query: string, filterExtension?: string): Promise { if (!this.plugin.settings.enableFileReferences) return []; const lowercaseQuery = query.toLowerCase(); const virtualItems: AutocompleteItem[] = []; // Пункт 1: Если в запросе "cur", добавляем логи в начало - if ("current-log".includes(lowercaseQuery)) { + // (Только если мы не ищем скрипты конкретного расширения) + if (!filterExtension && "current-log".includes(lowercaseQuery)) { virtualItems.push({ name: "current-log", path: "virtual:current-log", - type: "file", // Для применения иконки @ по умолчанию + type: "file", icon: "file-text" }); virtualItems.push({ @@ -70,10 +72,18 @@ export class AutocompleteManager { await this.updateCacheIfNeeded(); const allFiles = this.fileCache.get('all') || []; - // Объединяем: виртуальные вначале, потом поиск по файлам - const filteredFiles = !query.trim() - ? allFiles - : allFiles.filter((item) => item.name.toLowerCase().includes(lowercaseQuery) || item.path.toLowerCase().includes(lowercaseQuery)); + let filteredFiles = allFiles; + + // Применяем строгий фильтр расширения (если передан, например для '.py' скриптов) + if (filterExtension) { + const ext = `.${filterExtension.toLowerCase()}`; + filteredFiles = filteredFiles.filter(item => item.path.toLowerCase().endsWith(ext)); + } + + // Фильтруем по введенному запросу пользователя + filteredFiles = !query.trim() + ? filteredFiles + : filteredFiles.filter((item) => item.name.toLowerCase().includes(lowercaseQuery) || item.path.toLowerCase().includes(lowercaseQuery)); return [...virtualItems, ...filteredFiles].slice(0, AutocompleteManager.MAX_TOTAL_ITEMS); } @@ -173,26 +183,28 @@ export class AutocompleteManager { * Обновляет кеш файлов и промптов */ private async updateCache(): Promise { - const files = this.plugin.app.vault.getMarkdownFiles(); + // Получаем ВСЕ файлы хранилища, чтобы поддерживать скрипты (.py), картинки и др. + const files = this.plugin.app.vault.getFiles(); const fileItems: AutocompleteItem[] = []; const promptItems: AutocompleteItem[] = []; - // Нормализуем пути исключенных папок const normalizedExcludedFolders = this.plugin.settings.excludedFolders .filter((folder) => folder.trim() !== '') - .map((folder) => (folder.endsWith('/') ? folder : folder + '/')) // Убедимся, что заканчивается на слэш - .map((folder) => (folder.startsWith('/') ? folder.substring(1) : folder)); // Удаляем начальный слэш + .map((folder) => (folder.endsWith('/') ? folder : folder + '/')) + .map((folder) => (folder.startsWith('/') ? folder.substring(1) : folder)); for (const file of files) { - // Пропускаем файлы из исключенных папок if (this.isFileInExcludedFolder(file, normalizedExcludedFolders)) { continue; } + const isPrompt = this.isPromptFile(file); const item: AutocompleteItem = { - name: file.basename, + // Для промптов оставляем basename (без расширения), + // а для обычных файлов используем name (с расширением) для точной идентификации + name: isPrompt ? file.basename : file.name, path: file.path, - type: this.isPromptFile(file) ? 'prompt' : 'file', + type: isPrompt ? 'prompt' : 'file', icon: this.getFileIcon(file), file: file }; @@ -204,7 +216,6 @@ export class AutocompleteManager { } } - // Сортируем по имени fileItems.sort((a, b) => a.name.localeCompare(b.name)); promptItems.sort((a, b) => a.name.localeCompare(b.name)); @@ -238,6 +249,7 @@ export class AutocompleteManager { switch (extension) { case 'md': return 'file-text'; + case 'py': return 'file-terminal'; case 'js': case 'ts': return 'code'; diff --git a/src/references/ReferenceAutocomplete.ts b/src/references/ReferenceAutocomplete.ts index 10d56cc..14632ac 100644 --- a/src/references/ReferenceAutocomplete.ts +++ b/src/references/ReferenceAutocomplete.ts @@ -44,8 +44,9 @@ export class ReferenceAutocomplete { * Показывает автокомплит с опциями. * @param reference - объект ReferenceMatch, определяющий текущий вводимый референс. * @param cursorRect - DOMRect объекта курсора, для определения позиции. + * @param filterExtension - Опциональный фильтр расширений */ - async showAutocomplete(reference: ReferenceMatch, cursorRect: DOMRect): Promise { + async showAutocomplete(reference: ReferenceMatch, cursorRect: DOMRect, filterExtension?: string): Promise { this.currentReference = reference; // Для внешних файлов ($, !$) сразу открываем диалог @@ -57,10 +58,10 @@ export class ReferenceAutocomplete { return; } - // Для файлов и промптов работаем как раньше let options: AutocompleteItem[] = []; if (reference.type === 'file') { - options = await this.autocompleteManager.getFileOptions(reference.name); + // ИЗМЕНЕНО: Передаем фильтр в менеджер + options = await this.autocompleteManager.getFileOptions(reference.name, filterExtension); } else if (reference.type === 'prompt') { options = await this.autocompleteManager.getPromptOptions(reference.name); } else if (reference.type === 'url') { diff --git a/src/views/ChatView.ts b/src/views/ChatView.ts index 1a060e2..63d657b 100644 --- a/src/views/ChatView.ts +++ b/src/views/ChatView.ts @@ -158,7 +158,7 @@ export class ChatView extends ItemView { // ----------------------------------------------- Event Handlers --------------------------------------------------------------- - async handleSendMessage(message: string, selectedModel?: string, attachments?: Attachment[], customMaxTokens?: number, customTemperature?: number): Promise { + async handleSendMessage(message: string, selectedModel?: string, attachments?: Attachment[], customMaxTokens?: number, customTemperature?: number, agencyMode: boolean = false): Promise { try { // ЗАЩИТА: Если граф не выбран, создаем его на лету, чтобы не потерять текст if (!this.selectedGraphId) { @@ -213,7 +213,7 @@ export class ChatView extends ItemView { const systemPromptToUse = this.currentGraphCustomSystemPrompt !== null ? this.currentGraphCustomSystemPrompt : this.plugin.settings.systemPrompt; - await this.streamLLMResponse(this.selectedGraphId, userNodeId, selectedModel, null, systemPromptToUse, cacheFolder, customMaxTokens, customTemperature); + await this.streamLLMResponse(this.selectedGraphId!, userNodeId, selectedModel, null, systemPromptToUse, cacheFolder, customMaxTokens, customTemperature, agencyMode); } else { throw new Error('Graph ID не определён после отправки сообщения'); } @@ -226,10 +226,15 @@ export class ChatView extends ItemView { handleSettingsChanged(): void { if (this.chatPanel) { - // 1. Обновляем модель (это у вас уже было) + // 1. Обновляем модель this.chatPanel.setSelectedModel(this.plugin.settings.defaultModel); - // 2. Уведомляем панель об обновлении всего объекта плагина (и настроек внутри) + // 2. Обновляем состояние тумблера из настроек плагина + if (this.chatPanel.agencyToggle) { + this.chatPanel.agencyToggle.checked = !!this.plugin.settings.agencyModeEnabled; + } + + // 3. Уведомляем панель об обновлении всего объекта плагина (и настроек внутри) this.chatPanel.updateSettings(this.plugin); } } @@ -294,7 +299,8 @@ export class ChatView extends ItemView { systemPrompt: string | null = null, cacheFolder?: string, customMaxTokens?: number, - customTemperature?: number + customTemperature?: number, + agencyMode: boolean = false ): Promise { // Разворачиваем системный промпт let finalSystemPrompt = systemPrompt; @@ -314,7 +320,8 @@ export class ChatView extends ItemView { model: selectedModel || this.plugin.settings.defaultModel, cache_folder: cacheFolder, temperature: temperature, - max_tokens: customMaxTokens + max_tokens: customMaxTokens, + agency_mode: agencyMode }; if (existingAssistantNodeId) { @@ -376,6 +383,11 @@ export class ChatView extends ItemView { graphId: graphId, currentNode: assistantNodeId }); + } + else if (data.type === 'tool_start' && assistantNodeId) { + if (this.chatPanel) this.chatPanel.toolExecutionUpdate(assistantNodeId, data.name, 'start', data.step); + } else if (data.type === 'tool_end' && assistantNodeId) { + if (this.chatPanel) this.chatPanel.toolExecutionUpdate(assistantNodeId, data.name, 'end', data.step); } else if (data.type === 'chunk' && assistantNodeId) { accumulatedContent += data.content; this.updateStreamingMessage(assistantNodeId, accumulatedContent); @@ -496,7 +508,7 @@ export class ChatView extends ItemView { } } - async handleRegenerateMessage(data: { graphId: string; nodeId: string; model: string, maxTokens?: number, temperature?: number }): Promise { + async handleRegenerateMessage(data: { graphId: string; nodeId: string; model: string, maxTokens?: number, temperature?: number, agencyMode?: boolean }): Promise { try { const systemPromptToUse = this.currentGraphCustomSystemPrompt !== null ? this.currentGraphCustomSystemPrompt : this.plugin.settings.systemPrompt; @@ -533,7 +545,8 @@ export class ChatView extends ItemView { const temperatureFromBackend = result.temperature; if (data.graphId) { - await this.streamLLMResponse(data.graphId, userNodeId, modelToUse, existingAssistantNodeId, systemPromptFromBackend, cacheFolder, maxTokensFromBackend, temperatureFromBackend); + await this.streamLLMResponse(data.graphId, userNodeId, modelToUse, existingAssistantNodeId, systemPromptFromBackend, + cacheFolder, maxTokensFromBackend, temperatureFromBackend, data.agencyMode || false); } } catch (error: any) { console.error('Ошибка регенерации:', error);