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 { AutocompleteManager } from './src/references/AutocompleteManager';
|
||||||
import { ReferenceAutocomplete } from './src/references/ReferenceAutocomplete';
|
import { ReferenceAutocomplete } from './src/references/ReferenceAutocomplete';
|
||||||
import { ReferenceParser } from './src/references/ReferenceParser';
|
import { ReferenceParser } from './src/references/ReferenceParser';
|
||||||
|
import { CustomTool } from './src/components/models/ChatHistoryItem';
|
||||||
|
import { v4 as uuidv4 } from 'uuid';
|
||||||
|
|
||||||
import 'src/css/styles.css';
|
import 'src/css/styles.css';
|
||||||
|
|
||||||
|
|
@ -19,6 +21,7 @@ interface LLMAgentSettings {
|
||||||
systemPrompt: string;
|
systemPrompt: string;
|
||||||
defaultModel: string;
|
defaultModel: string;
|
||||||
promptsFolder: string;
|
promptsFolder: string;
|
||||||
|
scriptsFolder: string; // Новое: Папка со скриптами питона
|
||||||
enableFileReferences: boolean;
|
enableFileReferences: boolean;
|
||||||
enablePromptReferences: boolean;
|
enablePromptReferences: boolean;
|
||||||
excludedFolders: string[];
|
excludedFolders: string[];
|
||||||
|
|
@ -26,6 +29,8 @@ interface LLMAgentSettings {
|
||||||
|
|
||||||
temperatureChat: number;
|
temperatureChat: number;
|
||||||
maxTokensChatDefault: string;
|
maxTokensChatDefault: string;
|
||||||
|
agencyModeEnabled: boolean; // Новое: По умолчанию включен ли агент
|
||||||
|
customTools: CustomTool[]; // Новое: Список созданных инструментов
|
||||||
|
|
||||||
summarizationModel: string;
|
summarizationModel: string;
|
||||||
voiceCommandModel: string;
|
voiceCommandModel: string;
|
||||||
|
|
@ -57,6 +62,7 @@ const DEFAULT_SETTINGS: LLMAgentSettings = {
|
||||||
systemPrompt: 'Ты полезный ИИ ассистент.',
|
systemPrompt: 'Ты полезный ИИ ассистент.',
|
||||||
defaultModel: LLM_DEFAULTS.MODEL,
|
defaultModel: LLM_DEFAULTS.MODEL,
|
||||||
promptsFolder: 'prompts',
|
promptsFolder: 'prompts',
|
||||||
|
scriptsFolder: 'scripts',
|
||||||
enableFileReferences: true,
|
enableFileReferences: true,
|
||||||
enablePromptReferences: true,
|
enablePromptReferences: true,
|
||||||
excludedFolders: ['.obsidian', 'llm-agent-backend', 'templates', 'ref-cache'],
|
excludedFolders: ['.obsidian', 'llm-agent-backend', 'templates', 'ref-cache'],
|
||||||
|
|
@ -64,6 +70,8 @@ const DEFAULT_SETTINGS: LLMAgentSettings = {
|
||||||
|
|
||||||
temperatureChat: LLM_DEFAULTS.TEMPERATURE,
|
temperatureChat: LLM_DEFAULTS.TEMPERATURE,
|
||||||
maxTokensChatDefault: '',
|
maxTokensChatDefault: '',
|
||||||
|
agencyModeEnabled: true,
|
||||||
|
customTools: [],
|
||||||
|
|
||||||
summarizationModel: '',
|
summarizationModel: '',
|
||||||
voiceCommandModel: '',
|
voiceCommandModel: '',
|
||||||
|
|
@ -112,6 +120,23 @@ export default class LLMAgentPlugin extends Plugin {
|
||||||
settingsToSync.promptCommands = await engine.processRawSettingValue(this.settings.promptCommands);
|
settingsToSync.promptCommands = await engine.processRawSettingValue(this.settings.promptCommands);
|
||||||
settingsToSync.promptMetrics = await engine.processRawSettingValue(this.settings.promptMetrics);
|
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;
|
const adapter = this.app.vault.adapter;
|
||||||
if (adapter instanceof FileSystemAdapter) {
|
if (adapter instanceof FileSystemAdapter) {
|
||||||
// Соединяем базовый путь хранилища и относительный путь к логам
|
// Соединяем базовый путь хранилища и относительный путь к логам
|
||||||
|
|
@ -125,7 +150,7 @@ export default class LLMAgentPlugin extends Plugin {
|
||||||
body: JSON.stringify(settingsToSync)
|
body: JSON.stringify(settingsToSync)
|
||||||
});
|
});
|
||||||
if (response.ok) {
|
if (response.ok) {
|
||||||
console.log("LLM Agent: Settings synced with backend.");
|
console.log("LLM Agent: Settings and Tools synced with backend.");
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error("LLM Agent: Failed to sync settings", e);
|
console.error("LLM Agent: Failed to sync settings", e);
|
||||||
|
|
@ -275,9 +300,9 @@ export default class LLMAgentPlugin extends Plugin {
|
||||||
type: CHAT_VIEW_TYPE,
|
type: CHAT_VIEW_TYPE,
|
||||||
active: true
|
active: true
|
||||||
});
|
});
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// ----------------------------------------------- Event Handlers ---------------------------------------------------------------
|
// ----------------------------------------------- Event Handlers ---------------------------------------------------------------
|
||||||
|
|
||||||
|
|
@ -302,6 +327,7 @@ export default class LLMAgentPlugin extends Plugin {
|
||||||
|
|
||||||
class SampleSettingTab extends PluginSettingTab {
|
class SampleSettingTab extends PluginSettingTab {
|
||||||
plugin: LLMAgentPlugin;
|
plugin: LLMAgentPlugin;
|
||||||
|
activeTabId: string = 'basic';
|
||||||
|
|
||||||
constructor(app: App, plugin: LLMAgentPlugin) {
|
constructor(app: App, plugin: LLMAgentPlugin) {
|
||||||
super(app, plugin);
|
super(app, plugin);
|
||||||
|
|
@ -312,9 +338,68 @@ class SampleSettingTab extends PluginSettingTab {
|
||||||
const { containerEl } = this;
|
const { containerEl } = this;
|
||||||
containerEl.empty();
|
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('Модель по умолчанию')
|
.setName('Модель по умолчанию')
|
||||||
.setDesc('Выберите модель для основного чата.')
|
.setDesc('Выберите модель для основного чата.')
|
||||||
.addDropdown((dropdown) => {
|
.addDropdown((dropdown) => {
|
||||||
|
|
@ -327,7 +412,7 @@ class SampleSettingTab extends PluginSettingTab {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
new Setting(containerEl)
|
new Setting(container)
|
||||||
.setName('Модель суммаризации')
|
.setName('Модель суммаризации')
|
||||||
.setDesc('Выберите модель для суммаризации.')
|
.setDesc('Выберите модель для суммаризации.')
|
||||||
.addDropdown((dropdown) => {
|
.addDropdown((dropdown) => {
|
||||||
|
|
@ -340,10 +425,7 @@ class SampleSettingTab extends PluginSettingTab {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
// Этот промпт будет отправляться LLM перед каждым диалогом
|
new Setting(container)
|
||||||
this.addPromptAutocomplete(new Setting(containerEl).setName('Системный промпт'), 'systemPrompt');
|
|
||||||
|
|
||||||
new Setting(containerEl)
|
|
||||||
.setName('Температура чата по умолчанию')
|
.setName('Температура чата по умолчанию')
|
||||||
.setDesc(`Значение от 0.0 до 2.0 (по умолчанию ${LLM_DEFAULTS.TEMPERATURE}). Влияет на креативность.`)
|
.setDesc(`Значение от 0.0 до 2.0 (по умолчанию ${LLM_DEFAULTS.TEMPERATURE}). Влияет на креативность.`)
|
||||||
.addText((text) =>
|
.addText((text) =>
|
||||||
|
|
@ -358,7 +440,7 @@ class SampleSettingTab extends PluginSettingTab {
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
new Setting(containerEl)
|
new Setting(container)
|
||||||
.setName('Макс. токенов чата по умолчанию')
|
.setName('Макс. токенов чата по умолчанию')
|
||||||
.setDesc('Оставьте пустым для отсутствия лимита.')
|
.setDesc('Оставьте пустым для отсутствия лимита.')
|
||||||
.addText((text) =>
|
.addText((text) =>
|
||||||
|
|
@ -372,220 +454,12 @@ class SampleSettingTab extends PluginSettingTab {
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
new Setting(containerEl)
|
// Этот промпт будет отправляться LLM перед каждым диалогом
|
||||||
.setName('API Base URL')
|
this.addPromptAutocomplete(new Setting(container).setName('Глобальный системный промпт'), 'systemPrompt');
|
||||||
.setDesc('Базовый URL для API LLM Agent')
|
|
||||||
.addText((text) =>
|
|
||||||
text
|
|
||||||
.setPlaceholder('http://localhost:5000/api')
|
|
||||||
.setValue(this.plugin.settings.apiBaseUrl)
|
|
||||||
.onChange(async (value) => {
|
|
||||||
this.plugin.settings.apiBaseUrl = value;
|
|
||||||
await this.plugin.saveSettings();
|
|
||||||
await this.plugin.syncSettings();
|
|
||||||
})
|
|
||||||
);
|
|
||||||
|
|
||||||
containerEl.createEl('h2', { text: 'Голосовое управление (Voice Service)' });
|
container.createEl('h3', { text: 'Управление ссылками и файлами' });
|
||||||
|
|
||||||
new Setting(containerEl)
|
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(containerEl)
|
|
||||||
.setName('Модель команд')
|
|
||||||
.setDesc('LLM для обработки голосовых команд.')
|
|
||||||
.addDropdown((dropdown) => {
|
|
||||||
this.plugin.availableModels.forEach(model => dropdown.addOption(model, model));
|
|
||||||
dropdown.setValue(this.plugin.settings.voiceCommandModel)
|
|
||||||
.onChange(async (value) => {
|
|
||||||
this.plugin.settings.voiceCommandModel = value;
|
|
||||||
await this.plugin.saveSettings();
|
|
||||||
await this.plugin.syncSettings();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
// Инструкции для Промпта №1.
|
|
||||||
this.addPromptAutocomplete(
|
|
||||||
new Setting(containerEl).setName('Регулярный промпт'),
|
|
||||||
'promptRegular'
|
|
||||||
);
|
|
||||||
|
|
||||||
// Инструкции для Промпта №2.
|
|
||||||
this.addPromptAutocomplete(
|
|
||||||
new Setting(containerEl).setName('Промпт команд'),
|
|
||||||
'promptCommands'
|
|
||||||
);
|
|
||||||
|
|
||||||
this.addPromptAutocomplete(
|
|
||||||
new Setting(containerEl).setName('Промпт качественной аналитики'),
|
|
||||||
'promptMetrics'
|
|
||||||
);
|
|
||||||
|
|
||||||
new Setting(containerEl)
|
|
||||||
.setName('Температура Voice')
|
|
||||||
.setDesc(`Температура для голосовых запросов (по умолчанию ${LLM_DEFAULTS.TEMPERATURE})`)
|
|
||||||
.addText((text) =>
|
|
||||||
text
|
|
||||||
.setPlaceholder(String(LLM_DEFAULTS.TEMPERATURE))
|
|
||||||
.setValue(String(this.plugin.settings.temperatureVoice))
|
|
||||||
.onChange(async (value) => {
|
|
||||||
const num = parseFloat(value);
|
|
||||||
this.plugin.settings.temperatureVoice = isNaN(num) ? LLM_DEFAULTS.TEMPERATURE : num;
|
|
||||||
await this.plugin.saveSettings();
|
|
||||||
await this.plugin.syncSettings();
|
|
||||||
})
|
|
||||||
);
|
|
||||||
|
|
||||||
new Setting(containerEl)
|
|
||||||
.setName('Макс. токенов Voice (Регулярный)')
|
|
||||||
.setDesc('Лимит токенов для регулярного анализа. Оставьте пустым для безлимита.')
|
|
||||||
.addText((text) =>
|
|
||||||
text
|
|
||||||
.setPlaceholder('Лимит')
|
|
||||||
.setValue(this.plugin.settings.maxTokensVoiceRegular)
|
|
||||||
.onChange(async (value) => {
|
|
||||||
this.plugin.settings.maxTokensVoiceRegular = value;
|
|
||||||
await this.plugin.saveSettings();
|
|
||||||
await this.plugin.syncSettings();
|
|
||||||
})
|
|
||||||
);
|
|
||||||
|
|
||||||
new Setting(containerEl)
|
|
||||||
.setName('Макс. токенов Voice (Команды)')
|
|
||||||
.setDesc('Лимит токенов для голосовых команд. Оставьте пустым для безлимита.')
|
|
||||||
.addText((text) =>
|
|
||||||
text
|
|
||||||
.setPlaceholder('Лимит')
|
|
||||||
.setValue(this.plugin.settings.maxTokensVoiceCommands)
|
|
||||||
.onChange(async (value) => {
|
|
||||||
this.plugin.settings.maxTokensVoiceCommands = value;
|
|
||||||
await this.plugin.saveSettings();
|
|
||||||
await this.plugin.syncSettings();
|
|
||||||
})
|
|
||||||
);
|
|
||||||
|
|
||||||
new Setting(containerEl)
|
|
||||||
.setName('Интервал регулярного анализа (мин)')
|
|
||||||
.setDesc('Как часто LLM анализирует последние фразы (Prompt 1).')
|
|
||||||
.addText((text) =>
|
|
||||||
text
|
|
||||||
.setValue(String(this.plugin.settings.voiceInterval))
|
|
||||||
.onChange(async (value) => {
|
|
||||||
this.plugin.settings.voiceInterval = Number(value);
|
|
||||||
await this.plugin.saveSettings();
|
|
||||||
await this.plugin.syncSettings();
|
|
||||||
})
|
|
||||||
);
|
|
||||||
|
|
||||||
new Setting(containerEl)
|
|
||||||
.setName('Интервал аналитики (мин)')
|
|
||||||
.setDesc('Как часто Python и LLM генерируют графики и качественный анализ (Таб 4).')
|
|
||||||
.addText((text) =>
|
|
||||||
text
|
|
||||||
.setValue(String(this.plugin.settings.metricsInterval))
|
|
||||||
.onChange(async (value) => {
|
|
||||||
this.plugin.settings.metricsInterval = Number(value) || 15;
|
|
||||||
await this.plugin.saveSettings();
|
|
||||||
await this.plugin.syncSettings();
|
|
||||||
})
|
|
||||||
);
|
|
||||||
|
|
||||||
new Setting(containerEl)
|
|
||||||
.setName('Действие диктовки')
|
|
||||||
.setDesc('При нажатии записи в чате, если голос не запущен, как его стартовать?')
|
|
||||||
.addDropdown((dropdown) => {
|
|
||||||
dropdown.addOption('start', 'Начать заново (Start New)');
|
|
||||||
dropdown.addOption('continue', 'Продолжить (Continue)');
|
|
||||||
dropdown.setValue(this.plugin.settings.dictationStartAction)
|
|
||||||
.onChange(async (value: 'start' | 'continue') => {
|
|
||||||
this.plugin.settings.dictationStartAction = value;
|
|
||||||
await this.plugin.saveSettings();
|
|
||||||
await this.plugin.syncSettings();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
new Setting(containerEl)
|
|
||||||
.setName('Папка аудио-логов')
|
|
||||||
.setDesc('Папка внутри Obsidian, куда будут сохраняться текстовые расшифровки (в формате .md). Относительно корня хранилища.')
|
|
||||||
.addText((text) =>
|
|
||||||
text
|
|
||||||
.setPlaceholder('audio-logs')
|
|
||||||
.setValue(this.plugin.settings.logsFolder)
|
|
||||||
.onChange(async (value) => {
|
|
||||||
// Убираем лишние слэши для корректности пути
|
|
||||||
this.plugin.settings.logsFolder = value.replace(/^\/+|\/+$/g, '').trim();
|
|
||||||
await this.plugin.saveSettings();
|
|
||||||
await this.plugin.syncSettings();
|
|
||||||
})
|
|
||||||
);
|
|
||||||
|
|
||||||
new Setting(containerEl)
|
|
||||||
.setName('Объем контекста (символы)')
|
|
||||||
.setDesc('Сколько последних символов транскрибации отправлять в модель.')
|
|
||||||
.addText((text) =>
|
|
||||||
text
|
|
||||||
.setValue(String(this.plugin.settings.voiceContextLimit))
|
|
||||||
.onChange(async (value) => {
|
|
||||||
this.plugin.settings.voiceContextLimit = Number(value);
|
|
||||||
await this.plugin.saveSettings();
|
|
||||||
await this.plugin.syncSettings();
|
|
||||||
})
|
|
||||||
);
|
|
||||||
|
|
||||||
new Setting(containerEl)
|
|
||||||
.setName('Маркер начала команды')
|
|
||||||
.setDesc('Слово, после которого текст считается командой (Response 2).')
|
|
||||||
.addText((text) =>
|
|
||||||
text
|
|
||||||
.setPlaceholder('старт')
|
|
||||||
.setValue(this.plugin.settings.markerStart)
|
|
||||||
.onChange(async (value) => {
|
|
||||||
this.plugin.settings.markerStart = value;
|
|
||||||
await this.plugin.saveSettings();
|
|
||||||
await this.plugin.syncSettings();
|
|
||||||
})
|
|
||||||
);
|
|
||||||
|
|
||||||
new Setting(containerEl)
|
|
||||||
.setName('Маркер конца команды')
|
|
||||||
.setDesc('Слово, завершающее захват голосовой команды.')
|
|
||||||
.addText((text) =>
|
|
||||||
text
|
|
||||||
.setPlaceholder('стоп')
|
|
||||||
.setValue(this.plugin.settings.markerStop)
|
|
||||||
.onChange(async (value) => {
|
|
||||||
this.plugin.settings.markerStop = value;
|
|
||||||
await this.plugin.saveSettings();
|
|
||||||
await this.plugin.syncSettings();
|
|
||||||
})
|
|
||||||
);
|
|
||||||
|
|
||||||
new Setting(containerEl)
|
|
||||||
.setName('Путь к модели Vosk')
|
|
||||||
.setDesc('Абсолютный путь к папке с распакованной языковой моделью (например: C:\\vosk-models\\vosk-model-small-ru)')
|
|
||||||
.addText((text) =>
|
|
||||||
text
|
|
||||||
.setPlaceholder('Путь к модели на диске')
|
|
||||||
.setValue(this.plugin.settings.voskModelPath)
|
|
||||||
.onChange(async (value) => {
|
|
||||||
this.plugin.settings.voskModelPath = value.replace(/"/g, '').trim(); // Убираем кавычки, если скопировали путь как путь
|
|
||||||
await this.plugin.saveSettings();
|
|
||||||
await this.plugin.syncSettings();
|
|
||||||
})
|
|
||||||
);
|
|
||||||
|
|
||||||
containerEl.createEl('h2', { text: 'Управление ссылками и файлами' });
|
|
||||||
|
|
||||||
new Setting(containerEl)
|
|
||||||
.setName('Папка с промптами')
|
.setName('Папка с промптами')
|
||||||
.setDesc('Путь к папке, содержащей промпты для команд #/##')
|
.setDesc('Путь к папке, содержащей промпты для команд #/##')
|
||||||
.addText((text) =>
|
.addText((text) =>
|
||||||
|
|
@ -599,7 +473,7 @@ class SampleSettingTab extends PluginSettingTab {
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
new Setting(containerEl)
|
new Setting(container)
|
||||||
.setName('Папка кеша для вложений')
|
.setName('Папка кеша для вложений')
|
||||||
.setDesc('Абсолютный или относительный путь к папке для кеширования файлов')
|
.setDesc('Абсолютный или относительный путь к папке для кеширования файлов')
|
||||||
.addText((text) =>
|
.addText((text) =>
|
||||||
|
|
@ -614,7 +488,7 @@ class SampleSettingTab extends PluginSettingTab {
|
||||||
);
|
);
|
||||||
|
|
||||||
// Настройки ссылок на файлы
|
// Настройки ссылок на файлы
|
||||||
new Setting(containerEl)
|
new Setting(container)
|
||||||
.setName('Включить ссылки на файлы')
|
.setName('Включить ссылки на файлы')
|
||||||
.setDesc('Разрешить использование @/@@ ссылок на файлы в хранилище')
|
.setDesc('Разрешить использование @/@@ ссылок на файлы в хранилище')
|
||||||
.addToggle((toggle) =>
|
.addToggle((toggle) =>
|
||||||
|
|
@ -626,7 +500,7 @@ class SampleSettingTab extends PluginSettingTab {
|
||||||
);
|
);
|
||||||
|
|
||||||
// Настройки ссылок на промпты
|
// Настройки ссылок на промпты
|
||||||
new Setting(containerEl)
|
new Setting(container)
|
||||||
.setName('Включить ссылки на промпты')
|
.setName('Включить ссылки на промпты')
|
||||||
.setDesc('Разрешить использование #/## ссылок на промпты')
|
.setDesc('Разрешить использование #/## ссылок на промпты')
|
||||||
.addToggle((toggle) =>
|
.addToggle((toggle) =>
|
||||||
|
|
@ -638,7 +512,7 @@ class SampleSettingTab extends PluginSettingTab {
|
||||||
);
|
);
|
||||||
|
|
||||||
// Новое поле для настройки исключенных папок
|
// Новое поле для настройки исключенных папок
|
||||||
new Setting(containerEl)
|
new Setting(container)
|
||||||
.setName('Исключенные папки для ссылок')
|
.setName('Исключенные папки для ссылок')
|
||||||
.setDesc('Список относительных путей к папкам, файлы из которых не будут предлагаться в автодополнении ссылок (@/@@).')
|
.setDesc('Список относительных путей к папкам, файлы из которых не будут предлагаться в автодополнении ссылок (@/@@).')
|
||||||
.addToggle((toggle) =>
|
.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) => {
|
this.plugin.settings.excludedFolders.forEach((folder, index) => {
|
||||||
new Setting(containerEl)
|
new Setting(container)
|
||||||
.setClass('llm-agent-excluded-folder-item') // Добавляем класс для стилизации
|
.setClass('llm-agent-excluded-folder-item') // Добавляем класс для стилизации
|
||||||
.addText((text) => {
|
.addText((text) => {
|
||||||
text.setPlaceholder('Путь к папке (например, templates/)' + index)
|
text.setPlaceholder('Путь к папке (например, templates/)' + index)
|
||||||
|
|
@ -680,7 +554,7 @@ class SampleSettingTab extends PluginSettingTab {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
new Setting(containerEl).addButton((button) => {
|
new Setting(container).addButton((button) => {
|
||||||
button
|
button
|
||||||
.setButtonText('Добавить папку')
|
.setButtonText('Добавить папку')
|
||||||
.setIcon('plus')
|
.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) {
|
private addPromptAutocomplete(setting: Setting, settingKey: keyof LLMAgentSettings) {
|
||||||
const manager = new AutocompleteManager(this.plugin);
|
const manager = new AutocompleteManager(this.plugin);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -20,7 +20,7 @@ import { LLM_DEFAULTS } from 'src/constants';
|
||||||
|
|
||||||
interface ChatPanelProps {
|
interface ChatPanelProps {
|
||||||
chatHistory: ChatHistoryItem[];
|
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;
|
selectedModel?: string;
|
||||||
plugin: LLMAgentPlugin;
|
plugin: LLMAgentPlugin;
|
||||||
currentGraphCustomSystemPrompt: string | null;
|
currentGraphCustomSystemPrompt: string | null;
|
||||||
|
|
@ -64,6 +64,8 @@ export class ChatPanel {
|
||||||
dictationLastLen: number = 0;
|
dictationLastLen: number = 0;
|
||||||
hasGlobalStreams: boolean = false;
|
hasGlobalStreams: boolean = false;
|
||||||
|
|
||||||
|
agencyToggle: HTMLInputElement;
|
||||||
|
|
||||||
// ----------------------------------------------- Reference System Fields ---------------------------------------------------------------
|
// ----------------------------------------------- Reference System Fields ---------------------------------------------------------------
|
||||||
cacheManager: CacheManager;
|
cacheManager: CacheManager;
|
||||||
|
|
||||||
|
|
@ -211,15 +213,13 @@ export class ChatPanel {
|
||||||
this.modelDropdown.style.display = 'none';
|
this.modelDropdown.style.display = 'none';
|
||||||
}
|
}
|
||||||
|
|
||||||
handleInput(): void {
|
async setAgencyToggle(): Promise<void> {
|
||||||
const value = this.messageInput.textContent || '';
|
if (this.props.plugin.settings) {
|
||||||
const commands = ['/help', '/imagine', '/analyze', '/subtitles_meet', '/subtitles_teams', '/summarize', '/chat'];
|
this.props.plugin.settings.agencyModeEnabled = this.agencyToggle.checked;
|
||||||
|
if (typeof this.props.plugin.saveSettings === 'function') {
|
||||||
if (value.startsWith('/')) {
|
await this.props.plugin.saveSettings(); // TODO: Подумать, насколько это должно быть здесь
|
||||||
const filteredCommands = commands.filter((cmd) => cmd.startsWith(value.toLowerCase()));
|
await this.props.plugin.syncSettings();
|
||||||
this.showSuggestions(filteredCommands);
|
}
|
||||||
} else {
|
|
||||||
this.hideSuggestions();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -467,6 +467,45 @@ export class ChatPanel {
|
||||||
this.processStreamUpdates();
|
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мс
|
* Обрабатывает очередь так, чтобы рендер происходил не чаще чем раз в 300мс
|
||||||
* и дожидался окончания предыдущей отрисовки.
|
* и дожидался окончания предыдущей отрисовки.
|
||||||
|
|
@ -614,6 +653,15 @@ export class ChatPanel {
|
||||||
⚙️
|
⚙️
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</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="Температура">
|
<div class="param-select-container" title="Температура">
|
||||||
<span class="param-icon">Temp: </span>
|
<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">
|
<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.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.tempInput = this.container.querySelector('#temp-input') as HTMLInputElement;
|
||||||
this.tempResetBtn = this.container.querySelector('#temp-reset') as HTMLButtonElement;
|
this.tempResetBtn = this.container.querySelector('#temp-reset') as HTMLButtonElement;
|
||||||
this.maxTokensInput = this.container.querySelector('#max-tokens-input') as HTMLInputElement;
|
this.maxTokensInput = this.container.querySelector('#max-tokens-input') as HTMLInputElement;
|
||||||
|
|
@ -723,7 +773,6 @@ export class ChatPanel {
|
||||||
});
|
});
|
||||||
|
|
||||||
this.messageInput.addEventListener('input', () => {
|
this.messageInput.addEventListener('input', () => {
|
||||||
this.handleInput();
|
|
||||||
this.handleReferenceInput();
|
this.handleReferenceInput();
|
||||||
this.updatePlaceholder();
|
this.updatePlaceholder();
|
||||||
});
|
});
|
||||||
|
|
@ -741,6 +790,10 @@ export class ChatPanel {
|
||||||
this.toggleModelDropdown();
|
this.toggleModelDropdown();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
this.stopButton.addEventListener('click', () => this.handleStopGeneration());
|
this.stopButton.addEventListener('click', () => this.handleStopGeneration());
|
||||||
|
|
||||||
this.maxTokensResetBtn.addEventListener('click', () => {
|
this.maxTokensResetBtn.addEventListener('click', () => {
|
||||||
|
|
@ -766,6 +819,34 @@ export class ChatPanel {
|
||||||
this.updateScrollButtonsVisibility();
|
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
|
// Close dropdown when clicking outside
|
||||||
document.addEventListener('click', (e) => {
|
document.addEventListener('click', (e) => {
|
||||||
if (!this.modelSelectButton.contains(e.target as Node) && !this.modelDropdown.contains(e.target as Node)) {
|
if (!this.modelSelectButton.contains(e.target as Node) && !this.modelDropdown.contains(e.target as Node)) {
|
||||||
|
|
@ -1136,7 +1217,9 @@ export class ChatPanel {
|
||||||
const valTemp = this.tempInput?.value;
|
const valTemp = this.tempInput?.value;
|
||||||
const parsedTemp = valTemp ? parseFloat(valTemp) : undefined;
|
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();
|
this.clearInput();
|
||||||
|
|
@ -1174,7 +1257,9 @@ export class ChatPanel {
|
||||||
const valTemp = this.tempInput?.value;
|
const valTemp = this.tempInput?.value;
|
||||||
const parsedTemp = valTemp ? parseFloat(valTemp) : undefined;
|
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();
|
this.clearInput();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -1261,12 +1346,15 @@ export class ChatPanel {
|
||||||
const valTemp = this.tempInput?.value;
|
const valTemp = this.tempInput?.value;
|
||||||
const parsedTemp = valTemp ? parseFloat(valTemp) : undefined;
|
const parsedTemp = valTemp ? parseFloat(valTemp) : undefined;
|
||||||
|
|
||||||
|
const isAgencyActive = this.agencyToggle.checked;
|
||||||
|
|
||||||
this.props.plugin.eventBus.emit('regenerate-message', {
|
this.props.plugin.eventBus.emit('regenerate-message', {
|
||||||
graphId,
|
graphId,
|
||||||
nodeId,
|
nodeId,
|
||||||
model: this.selectedModel,
|
model: this.selectedModel,
|
||||||
maxTokens: parsedMaxTokens,
|
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 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 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', {
|
eventBus.emit('regenerate-message', {
|
||||||
graphId: data.graphId,
|
graphId: data.graphId,
|
||||||
nodeId: data.nodeId,
|
nodeId: data.nodeId,
|
||||||
model: model, // НОВОЕ: Добавляем модель
|
model: model, // НОВОЕ: Добавляем модель
|
||||||
maxTokens: maxTokens,
|
maxTokens: maxTokens,
|
||||||
temperature: temperature
|
temperature: temperature,
|
||||||
|
agencyMode: agencyMode
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -21,3 +21,12 @@ export interface Attachment {
|
||||||
permanent: boolean; // true для !@, !#, !$
|
permanent: boolean; // true для !@, !#, !$
|
||||||
mimeType?: string;
|
mimeType?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface CustomTool {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
description: string;
|
||||||
|
scriptPath: string;
|
||||||
|
hiddenPrompt: string;
|
||||||
|
hiddenPromptExpanded?: string;
|
||||||
|
}
|
||||||
|
|
@ -308,23 +308,18 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
.chat-controls {
|
.chat-controls {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: space-between;
|
flex-wrap: wrap; /* Элементы переносятся, если панель узкая */
|
||||||
align-items: center;
|
gap: 6px;
|
||||||
font-size: 12px;
|
justify-content: space-between;
|
||||||
color: var(--text-muted);
|
align-items: center;
|
||||||
|
padding-top: 4px;
|
||||||
}
|
}
|
||||||
|
.control-buttons-left, .control-buttons-right {
|
||||||
.control-buttons-left {
|
display: flex;
|
||||||
display: flex;
|
flex-wrap: wrap;
|
||||||
align-items: center;
|
gap: 4px;
|
||||||
gap: 8px;
|
align-items: center;
|
||||||
}
|
|
||||||
|
|
||||||
.control-buttons-right {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 8px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.model-select-container {
|
.model-select-container {
|
||||||
|
|
@ -1660,3 +1655,98 @@
|
||||||
100% { transform: scale(1); opacity: 1; }
|
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 query - поисковый запрос
|
||||||
|
* @param filterExtension - опциональный фильтр по расширению (например, 'py')
|
||||||
* @returns массив файлов для автокомплита
|
* @returns массив файлов для автокомплита
|
||||||
*/
|
*/
|
||||||
async getFileOptions(query: string): Promise<AutocompleteItem[]> {
|
async getFileOptions(query: string, filterExtension?: string): Promise<AutocompleteItem[]> {
|
||||||
if (!this.plugin.settings.enableFileReferences) return [];
|
if (!this.plugin.settings.enableFileReferences) return [];
|
||||||
|
|
||||||
const lowercaseQuery = query.toLowerCase();
|
const lowercaseQuery = query.toLowerCase();
|
||||||
const virtualItems: AutocompleteItem[] = [];
|
const virtualItems: AutocompleteItem[] = [];
|
||||||
|
|
||||||
// Пункт 1: Если в запросе "cur", добавляем логи в начало
|
// Пункт 1: Если в запросе "cur", добавляем логи в начало
|
||||||
if ("current-log".includes(lowercaseQuery)) {
|
// (Только если мы не ищем скрипты конкретного расширения)
|
||||||
|
if (!filterExtension && "current-log".includes(lowercaseQuery)) {
|
||||||
virtualItems.push({
|
virtualItems.push({
|
||||||
name: "current-log",
|
name: "current-log",
|
||||||
path: "virtual:current-log",
|
path: "virtual:current-log",
|
||||||
type: "file", // Для применения иконки @ по умолчанию
|
type: "file",
|
||||||
icon: "file-text"
|
icon: "file-text"
|
||||||
});
|
});
|
||||||
virtualItems.push({
|
virtualItems.push({
|
||||||
|
|
@ -70,10 +72,18 @@ export class AutocompleteManager {
|
||||||
await this.updateCacheIfNeeded();
|
await this.updateCacheIfNeeded();
|
||||||
const allFiles = this.fileCache.get('all') || [];
|
const allFiles = this.fileCache.get('all') || [];
|
||||||
|
|
||||||
// Объединяем: виртуальные вначале, потом поиск по файлам
|
let filteredFiles = allFiles;
|
||||||
const filteredFiles = !query.trim()
|
|
||||||
? allFiles
|
// Применяем строгий фильтр расширения (если передан, например для '.py' скриптов)
|
||||||
: allFiles.filter((item) => item.name.toLowerCase().includes(lowercaseQuery) || item.path.toLowerCase().includes(lowercaseQuery));
|
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);
|
return [...virtualItems, ...filteredFiles].slice(0, AutocompleteManager.MAX_TOTAL_ITEMS);
|
||||||
}
|
}
|
||||||
|
|
@ -173,26 +183,28 @@ export class AutocompleteManager {
|
||||||
* Обновляет кеш файлов и промптов
|
* Обновляет кеш файлов и промптов
|
||||||
*/
|
*/
|
||||||
private async updateCache(): Promise<void> {
|
private async updateCache(): Promise<void> {
|
||||||
const files = this.plugin.app.vault.getMarkdownFiles();
|
// Получаем ВСЕ файлы хранилища, чтобы поддерживать скрипты (.py), картинки и др.
|
||||||
|
const files = this.plugin.app.vault.getFiles();
|
||||||
const fileItems: AutocompleteItem[] = [];
|
const fileItems: AutocompleteItem[] = [];
|
||||||
const promptItems: AutocompleteItem[] = [];
|
const promptItems: AutocompleteItem[] = [];
|
||||||
|
|
||||||
// Нормализуем пути исключенных папок
|
|
||||||
const normalizedExcludedFolders = this.plugin.settings.excludedFolders
|
const normalizedExcludedFolders = this.plugin.settings.excludedFolders
|
||||||
.filter((folder) => folder.trim() !== '')
|
.filter((folder) => folder.trim() !== '')
|
||||||
.map((folder) => (folder.endsWith('/') ? folder : folder + '/')) // Убедимся, что заканчивается на слэш
|
.map((folder) => (folder.endsWith('/') ? folder : folder + '/'))
|
||||||
.map((folder) => (folder.startsWith('/') ? folder.substring(1) : folder)); // Удаляем начальный слэш
|
.map((folder) => (folder.startsWith('/') ? folder.substring(1) : folder));
|
||||||
|
|
||||||
for (const file of files) {
|
for (const file of files) {
|
||||||
// Пропускаем файлы из исключенных папок
|
|
||||||
if (this.isFileInExcludedFolder(file, normalizedExcludedFolders)) {
|
if (this.isFileInExcludedFolder(file, normalizedExcludedFolders)) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const isPrompt = this.isPromptFile(file);
|
||||||
const item: AutocompleteItem = {
|
const item: AutocompleteItem = {
|
||||||
name: file.basename,
|
// Для промптов оставляем basename (без расширения),
|
||||||
|
// а для обычных файлов используем name (с расширением) для точной идентификации
|
||||||
|
name: isPrompt ? file.basename : file.name,
|
||||||
path: file.path,
|
path: file.path,
|
||||||
type: this.isPromptFile(file) ? 'prompt' : 'file',
|
type: isPrompt ? 'prompt' : 'file',
|
||||||
icon: this.getFileIcon(file),
|
icon: this.getFileIcon(file),
|
||||||
file: file
|
file: file
|
||||||
};
|
};
|
||||||
|
|
@ -204,7 +216,6 @@ export class AutocompleteManager {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Сортируем по имени
|
|
||||||
fileItems.sort((a, b) => a.name.localeCompare(b.name));
|
fileItems.sort((a, b) => a.name.localeCompare(b.name));
|
||||||
promptItems.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) {
|
switch (extension) {
|
||||||
case 'md':
|
case 'md':
|
||||||
return 'file-text';
|
return 'file-text';
|
||||||
|
case 'py': return 'file-terminal';
|
||||||
case 'js':
|
case 'js':
|
||||||
case 'ts':
|
case 'ts':
|
||||||
return 'code';
|
return 'code';
|
||||||
|
|
|
||||||
|
|
@ -44,8 +44,9 @@ export class ReferenceAutocomplete {
|
||||||
* Показывает автокомплит с опциями.
|
* Показывает автокомплит с опциями.
|
||||||
* @param reference - объект ReferenceMatch, определяющий текущий вводимый референс.
|
* @param reference - объект ReferenceMatch, определяющий текущий вводимый референс.
|
||||||
* @param cursorRect - DOMRect объекта курсора, для определения позиции.
|
* @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;
|
this.currentReference = reference;
|
||||||
|
|
||||||
// Для внешних файлов ($, !$) сразу открываем диалог
|
// Для внешних файлов ($, !$) сразу открываем диалог
|
||||||
|
|
@ -57,10 +58,10 @@ export class ReferenceAutocomplete {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Для файлов и промптов работаем как раньше
|
|
||||||
let options: AutocompleteItem[] = [];
|
let options: AutocompleteItem[] = [];
|
||||||
if (reference.type === 'file') {
|
if (reference.type === 'file') {
|
||||||
options = await this.autocompleteManager.getFileOptions(reference.name);
|
// ИЗМЕНЕНО: Передаем фильтр в менеджер
|
||||||
|
options = await this.autocompleteManager.getFileOptions(reference.name, filterExtension);
|
||||||
} else if (reference.type === 'prompt') {
|
} else if (reference.type === 'prompt') {
|
||||||
options = await this.autocompleteManager.getPromptOptions(reference.name);
|
options = await this.autocompleteManager.getPromptOptions(reference.name);
|
||||||
} else if (reference.type === 'url') {
|
} else if (reference.type === 'url') {
|
||||||
|
|
|
||||||
|
|
@ -158,7 +158,7 @@ export class ChatView extends ItemView {
|
||||||
|
|
||||||
// ----------------------------------------------- Event Handlers ---------------------------------------------------------------
|
// ----------------------------------------------- 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 {
|
try {
|
||||||
// ЗАЩИТА: Если граф не выбран, создаем его на лету, чтобы не потерять текст
|
// ЗАЩИТА: Если граф не выбран, создаем его на лету, чтобы не потерять текст
|
||||||
if (!this.selectedGraphId) {
|
if (!this.selectedGraphId) {
|
||||||
|
|
@ -213,7 +213,7 @@ export class ChatView extends ItemView {
|
||||||
const systemPromptToUse =
|
const systemPromptToUse =
|
||||||
this.currentGraphCustomSystemPrompt !== null ? this.currentGraphCustomSystemPrompt : this.plugin.settings.systemPrompt;
|
this.currentGraphCustomSystemPrompt !== null ? this.currentGraphCustomSystemPrompt : this.plugin.settings.systemPrompt;
|
||||||
|
|
||||||
await this.streamLLMResponse(this.selectedGraphId, userNodeId, selectedModel, null, systemPromptToUse, cacheFolder, customMaxTokens, customTemperature);
|
await this.streamLLMResponse(this.selectedGraphId!, userNodeId, selectedModel, null, systemPromptToUse, cacheFolder, customMaxTokens, customTemperature, agencyMode);
|
||||||
} else {
|
} else {
|
||||||
throw new Error('Graph ID не определён после отправки сообщения');
|
throw new Error('Graph ID не определён после отправки сообщения');
|
||||||
}
|
}
|
||||||
|
|
@ -226,10 +226,15 @@ export class ChatView extends ItemView {
|
||||||
handleSettingsChanged(): void {
|
handleSettingsChanged(): void {
|
||||||
if (this.chatPanel)
|
if (this.chatPanel)
|
||||||
{
|
{
|
||||||
// 1. Обновляем модель (это у вас уже было)
|
// 1. Обновляем модель
|
||||||
this.chatPanel.setSelectedModel(this.plugin.settings.defaultModel);
|
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);
|
this.chatPanel.updateSettings(this.plugin);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -294,7 +299,8 @@ export class ChatView extends ItemView {
|
||||||
systemPrompt: string | null = null,
|
systemPrompt: string | null = null,
|
||||||
cacheFolder?: string,
|
cacheFolder?: string,
|
||||||
customMaxTokens?: number,
|
customMaxTokens?: number,
|
||||||
customTemperature?: number
|
customTemperature?: number,
|
||||||
|
agencyMode: boolean = false
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
// Разворачиваем системный промпт
|
// Разворачиваем системный промпт
|
||||||
let finalSystemPrompt = systemPrompt;
|
let finalSystemPrompt = systemPrompt;
|
||||||
|
|
@ -314,7 +320,8 @@ export class ChatView extends ItemView {
|
||||||
model: selectedModel || this.plugin.settings.defaultModel,
|
model: selectedModel || this.plugin.settings.defaultModel,
|
||||||
cache_folder: cacheFolder,
|
cache_folder: cacheFolder,
|
||||||
temperature: temperature,
|
temperature: temperature,
|
||||||
max_tokens: customMaxTokens
|
max_tokens: customMaxTokens,
|
||||||
|
agency_mode: agencyMode
|
||||||
};
|
};
|
||||||
|
|
||||||
if (existingAssistantNodeId) {
|
if (existingAssistantNodeId) {
|
||||||
|
|
@ -376,6 +383,11 @@ export class ChatView extends ItemView {
|
||||||
graphId: graphId,
|
graphId: graphId,
|
||||||
currentNode: assistantNodeId
|
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) {
|
} else if (data.type === 'chunk' && assistantNodeId) {
|
||||||
accumulatedContent += data.content;
|
accumulatedContent += data.content;
|
||||||
this.updateStreamingMessage(assistantNodeId, accumulatedContent);
|
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 {
|
try {
|
||||||
const systemPromptToUse =
|
const systemPromptToUse =
|
||||||
this.currentGraphCustomSystemPrompt !== null ? this.currentGraphCustomSystemPrompt : this.plugin.settings.systemPrompt;
|
this.currentGraphCustomSystemPrompt !== null ? this.currentGraphCustomSystemPrompt : this.plugin.settings.systemPrompt;
|
||||||
|
|
@ -533,7 +545,8 @@ export class ChatView extends ItemView {
|
||||||
const temperatureFromBackend = result.temperature;
|
const temperatureFromBackend = result.temperature;
|
||||||
|
|
||||||
if (data.graphId) {
|
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) {
|
} catch (error: any) {
|
||||||
console.error('Ошибка регенерации:', error);
|
console.error('Ошибка регенерации:', error);
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user