Add agency
This commit is contained in:
parent
768d93700e
commit
0acd67ee57
624
main.ts
624
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;';
|
||||
|
||||
new Setting(containerEl)
|
||||
const tabs = [
|
||||
{ id: 'basic', label: '⚙️ Основные' },
|
||||
{ id: 'agency', label: '🤖 Агент (ReAct Tools)' },
|
||||
{ id: 'voice', label: '🎤 Голос (Vosk)' }
|
||||
];
|
||||
|
||||
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)' });
|
||||
container.createEl('h3', { text: 'Управление ссылками и файлами' });
|
||||
|
||||
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)
|
||||
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);
|
||||
|
||||
|
|
|
|||
|
|
@ -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<void> {
|
||||
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 = `
|
||||
<div class="step-badge">
|
||||
🛠 Шаг ${step}: Подготовка и вызов <b>${toolName}</b>...
|
||||
</div>
|
||||
`;
|
||||
} else if (status === 'end') {
|
||||
indicator.innerHTML = `
|
||||
<div class="step-badge success">
|
||||
✅ Шаг ${step}: Инструмент <b>${toolName}</b> успешно завершен
|
||||
</div>
|
||||
`;
|
||||
// Прячем значок успеха через 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 {
|
|||
⚙️
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div class="agency-toggle-container" title="Включить Агентность (Вызов скриптов)">
|
||||
<label class="switch">
|
||||
<input type="checkbox" id="agency-toggle" ${this.props.plugin.settings.agencyModeEnabled ? 'checked' : ''}>
|
||||
<span class="slider"></span>
|
||||
</label>
|
||||
<span>🤖 Агент</span>
|
||||
</div>
|
||||
|
||||
<div class="param-select-container" title="Температура">
|
||||
<span class="param-icon">Temp: </span>
|
||||
<input type="number" id="temp-input" class="param-input" placeholder="${LLM_DEFAULTS.TEMPERATURE}" min="0" max="2" step="0.1">
|
||||
|
|
@ -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 = `<div class="step-badge">🛠 Шаг ${data.step}: Выполняю <b>${data.tool}</b>...</div>`;
|
||||
} else if (data.status === 'end') {
|
||||
indicator.innerHTML = `<div class="step-badge success">✅ Шаг ${data.step}: ${data.tool} завершен</div>`;
|
||||
// Через 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();
|
||||
}
|
||||
}
|
||||
|
|
@ -1261,12 +1346,15 @@ 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
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -105,13 +105,15 @@ const BaseNodeWrapper: React.FC<BaseNodeWrapperProps> = ({ 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
|
||||
});
|
||||
}
|
||||
};
|
||||
|
|
|
|||
|
|
@ -21,3 +21,12 @@ export interface Attachment {
|
|||
permanent: boolean; // true для !@, !#, !$
|
||||
mimeType?: string;
|
||||
}
|
||||
|
||||
export interface CustomTool {
|
||||
id: string;
|
||||
name: string;
|
||||
description: string;
|
||||
scriptPath: string;
|
||||
hiddenPrompt: string;
|
||||
hiddenPromptExpanded?: string;
|
||||
}
|
||||
|
|
@ -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; }
|
||||
}
|
||||
|
|
@ -43,20 +43,22 @@ export class AutocompleteManager {
|
|||
/**
|
||||
* Получает список файлов для автокомплита
|
||||
* @param query - поисковый запрос
|
||||
* @param filterExtension - опциональный фильтр по расширению (например, 'py')
|
||||
* @returns массив файлов для автокомплита
|
||||
*/
|
||||
async getFileOptions(query: string): Promise<AutocompleteItem[]> {
|
||||
async getFileOptions(query: string, filterExtension?: string): Promise<AutocompleteItem[]> {
|
||||
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<void> {
|
||||
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';
|
||||
|
|
|
|||
|
|
@ -44,8 +44,9 @@ export class ReferenceAutocomplete {
|
|||
* Показывает автокомплит с опциями.
|
||||
* @param reference - объект ReferenceMatch, определяющий текущий вводимый референс.
|
||||
* @param cursorRect - DOMRect объекта курсора, для определения позиции.
|
||||
* @param filterExtension - Опциональный фильтр расширений
|
||||
*/
|
||||
async showAutocomplete(reference: ReferenceMatch, cursorRect: DOMRect): Promise<void> {
|
||||
async showAutocomplete(reference: ReferenceMatch, cursorRect: DOMRect, filterExtension?: string): Promise<void> {
|
||||
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') {
|
||||
|
|
|
|||
|
|
@ -158,7 +158,7 @@ export class ChatView extends ItemView {
|
|||
|
||||
// ----------------------------------------------- Event Handlers ---------------------------------------------------------------
|
||||
|
||||
async handleSendMessage(message: string, selectedModel?: string, attachments?: Attachment[], customMaxTokens?: number, customTemperature?: number): Promise<void> {
|
||||
async handleSendMessage(message: string, selectedModel?: string, attachments?: Attachment[], customMaxTokens?: number, customTemperature?: number, agencyMode: boolean = false): Promise<void> {
|
||||
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<void> {
|
||||
// Разворачиваем системный промпт
|
||||
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<void> {
|
||||
async handleRegenerateMessage(data: { graphId: string; nodeId: string; model: string, maxTokens?: number, temperature?: number, agencyMode?: boolean }): Promise<void> {
|
||||
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);
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user