1200 lines
47 KiB
TypeScript
1200 lines
47 KiB
TypeScript
import { App, Editor, MarkdownView, Modal, Notice, Plugin, PluginSettingTab, Setting, ItemView, WorkspaceLeaf, TFile, FileSystemAdapter } from 'obsidian';
|
||
import { EventBus } from './src/utils/EventBus';
|
||
import { GraphView, GRAPH_VIEW_TYPE } from './src/views/GraphView';
|
||
import { ChatView, CHAT_VIEW_TYPE } from './src/views/ChatView';
|
||
import { WebSocketService } from './src/utils/WebSocketService';
|
||
import { TemplateEngine } from './src/references/TemplateEngine';
|
||
import { AutocompleteManager } from './src/references/AutocompleteManager';
|
||
import { ReferenceAutocomplete } from './src/references/ReferenceAutocomplete';
|
||
import { ReferenceParser } from './src/references/ReferenceParser';
|
||
import { CustomTool } from './src/components/models/ChatHistoryItem';
|
||
import { VaultQueryHandler } from 'src/utils/VaultQueryHandler';
|
||
import { v4 as uuidv4 } from 'uuid';
|
||
|
||
import 'src/css/styles.css';
|
||
|
||
import { LLM_DEFAULTS } from 'src/constants';
|
||
|
||
export interface MCPToolState {
|
||
name: string;
|
||
originalDescription: string;
|
||
customDescription: string;
|
||
isEnabled?: boolean;
|
||
}
|
||
|
||
export interface MCPServerConfig {
|
||
id: string;
|
||
name: string;
|
||
command: string;
|
||
args: string;
|
||
envString: string;
|
||
serverInstruction?: string;
|
||
fetchedTools?: MCPToolState[];
|
||
isEnabled?: boolean;
|
||
}
|
||
|
||
// ----------------------------------------------- Settings ---------------------------------------------------------------
|
||
|
||
interface LLMAgentSettings {
|
||
apiBaseUrl: string;
|
||
systemPrompt: string;
|
||
defaultModel: string;
|
||
promptsFolder: string;
|
||
scriptsFolder: string; // Новое: Папка со скриптами питона
|
||
enableFileReferences: boolean;
|
||
enablePromptReferences: boolean;
|
||
excludedFolders: string[];
|
||
refCacheFolder: string;
|
||
|
||
temperatureChat: number;
|
||
maxTokensChatDefault: string;
|
||
agencyModeEnabled: boolean; // Новое: По умолчанию включен ли агент
|
||
customTools: CustomTool[]; // Новое: Список созданных инструментов
|
||
mcpServers: MCPServerConfig[];
|
||
summarizationModel: string;
|
||
showDebugInfo: boolean;
|
||
|
||
voiceCommandModel: string;
|
||
logsFolder: string;
|
||
voiceInterval: number;
|
||
metricsInterval: number;
|
||
voiceDisplayLimit: number;
|
||
voiceContextLimit: number;
|
||
temperatureVoice: number;
|
||
maxTokensVoiceRegular: string;
|
||
maxTokensVoiceCommands: string;
|
||
promptRegular: string;
|
||
promptCommands: string;
|
||
promptMetrics: string;
|
||
markerStart: string;
|
||
markerStop: string;
|
||
|
||
voskModelPath: string;
|
||
showVoicePanel: boolean;
|
||
|
||
dictationStartAction: 'start' | 'continue';
|
||
|
||
lastActiveGraphId: string | null;
|
||
lastActiveNodeId: string | null;
|
||
}
|
||
|
||
const DEFAULT_SETTINGS: LLMAgentSettings = {
|
||
apiBaseUrl: 'http://localhost:5000/api',
|
||
systemPrompt: 'Ты полезный ИИ ассистент.',
|
||
defaultModel: LLM_DEFAULTS.MODEL,
|
||
promptsFolder: 'prompts',
|
||
scriptsFolder: 'scripts',
|
||
enableFileReferences: true,
|
||
enablePromptReferences: true,
|
||
excludedFolders: ['.obsidian', 'llm-agent-backend', 'templates', 'ref-cache'],
|
||
refCacheFolder: 'ref-cache',
|
||
|
||
temperatureChat: LLM_DEFAULTS.TEMPERATURE,
|
||
maxTokensChatDefault: '',
|
||
agencyModeEnabled: true,
|
||
customTools: [],
|
||
mcpServers: [],
|
||
summarizationModel: '',
|
||
showDebugInfo: false,
|
||
|
||
voiceCommandModel: '',
|
||
logsFolder: 'audio-logs',
|
||
voiceInterval: 1,
|
||
metricsInterval: 15,
|
||
voiceDisplayLimit: 30000,
|
||
voiceContextLimit: 30000,
|
||
temperatureVoice: LLM_DEFAULTS.TEMPERATURE,
|
||
maxTokensVoiceRegular: '',
|
||
maxTokensVoiceCommands: '',
|
||
promptRegular: '',
|
||
promptCommands: '',
|
||
promptMetrics: '',
|
||
markerStart: 'старт',
|
||
markerStop: 'стоп',
|
||
|
||
voskModelPath: '',
|
||
showVoicePanel: false,
|
||
|
||
dictationStartAction: 'start',
|
||
|
||
lastActiveGraphId: null,
|
||
lastActiveNodeId: null,
|
||
};
|
||
|
||
// ----------------------------------------------- Main Plugin Class ---------------------------------------------------------------
|
||
|
||
export default class LLMAgentPlugin extends Plugin {
|
||
settings: LLMAgentSettings;
|
||
eventBus: EventBus;
|
||
webSocketService: WebSocketService;
|
||
activeStreamControllers: Map<string, AbortController>;
|
||
availableModels: string[];
|
||
|
||
vaultQueryHandler: VaultQueryHandler | undefined;
|
||
|
||
async syncSettings() {
|
||
try {
|
||
const engine = new TemplateEngine(this);
|
||
|
||
// Клонируем настройки, чтобы не испортить исходные пути в конфиге плагина
|
||
const settingsToSync = JSON.parse(JSON.stringify(this.settings));
|
||
|
||
// Собираем промпты (раскрываем все цепочки ссылок) для работы с аудиологами на бекенде
|
||
settingsToSync.systemPrompt = await engine.processRawSettingValue(this.settings.systemPrompt);
|
||
settingsToSync.promptRegular = await engine.processRawSettingValue(this.settings.promptRegular);
|
||
settingsToSync.promptCommands = await engine.processRawSettingValue(this.settings.promptCommands);
|
||
settingsToSync.promptMetrics = await engine.processRawSettingValue(this.settings.promptMetrics);
|
||
|
||
// Раскрываем скрытые промпты у всех тулов
|
||
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) {
|
||
const vaultPath = adapter.getBasePath();
|
||
settingsToSync.vaultAbsolutePath = vaultPath;
|
||
|
||
// Соединяем базовый путь хранилища и относительный путь к логам
|
||
settingsToSync.absoluteLogsPath = adapter.getBasePath() +
|
||
(this.settings.logsFolder ? '/' + this.settings.logsFolder : '');
|
||
}
|
||
|
||
const response = await fetch(`${this.settings.apiBaseUrl}/settings/sync`, {
|
||
method: 'POST',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify(settingsToSync)
|
||
});
|
||
if (response.ok) {
|
||
console.log("LLM Agent: Settings and Tools synced with backend.");
|
||
}
|
||
} catch (e) {
|
||
console.error("LLM Agent: Failed to sync settings", e);
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Загружает список доступных моделей с бэкенда.
|
||
* @returns {Promise<void>}
|
||
*/
|
||
async fetchAvailableModels(): Promise<void> {
|
||
try {
|
||
const response = await fetch(`${this.settings.apiBaseUrl}/models`);
|
||
if (!response.ok) {
|
||
throw new Error(`Ошибка загрузки моделей: ${response.status}`);
|
||
}
|
||
this.availableModels = await response.json();
|
||
} catch (error) {
|
||
console.error('Ошибка загрузки доступных моделей:', error);
|
||
// Fallback к базовым моделям
|
||
this.availableModels = [LLM_DEFAULTS.MODEL, 'gpt-4o-mini', 'claude-3-5-sonnet-20241022'];
|
||
}
|
||
}
|
||
|
||
async onload() {
|
||
await this.loadSettings();
|
||
|
||
this.eventBus = new EventBus();
|
||
|
||
this.webSocketService = new WebSocketService(this.eventBus, this.settings.apiBaseUrl);
|
||
this.webSocketService.connect();
|
||
|
||
// Инициализируем обработчик vault-запросов от бэкенда
|
||
this.vaultQueryHandler = new VaultQueryHandler(this.app);
|
||
|
||
// Регистрируем хук: бэкенд просит Obsidian выполнить операцию с vault
|
||
this.webSocketService.onServerEvent(
|
||
'vault_query_request',
|
||
async (data: { request_id: string; type: string; payload: Record<string, any> }) => {
|
||
const { request_id, type, payload } = data;
|
||
console.log(`🔍 vault_query_request: type=${type}, id=${request_id}`);
|
||
|
||
try {
|
||
const result = await this.vaultQueryHandler.handle({
|
||
request_id,
|
||
type,
|
||
payload
|
||
});
|
||
|
||
this.webSocketService.emit('vault_query_response', {
|
||
request_id,
|
||
result
|
||
});
|
||
|
||
console.log(`✅ vault_query_response отправлен: id=${request_id}`);
|
||
} catch (e: any) {
|
||
const errorMsg = e?.message ?? String(e);
|
||
console.error(`❌ vault_query_request ошибка: ${errorMsg}`);
|
||
|
||
this.webSocketService.emit('vault_query_response', {
|
||
request_id,
|
||
error: errorMsg
|
||
});
|
||
}
|
||
}
|
||
);
|
||
|
||
// Регистрируем кастомные views
|
||
this.registerView(GRAPH_VIEW_TYPE, (leaf) => new GraphView(leaf, this));
|
||
this.registerView(CHAT_VIEW_TYPE, (leaf) => new ChatView(leaf, this));
|
||
|
||
// Добавляем команды
|
||
this.addCommand({
|
||
id: 'open-llm-agent-graph',
|
||
name: 'Открыть граф диалогов LLM Agent',
|
||
callback: () => this.activateGraphView()
|
||
});
|
||
|
||
this.addCommand({
|
||
id: 'open-llm-agent-chat',
|
||
name: 'Открыть чат LLM Agent',
|
||
callback: () => this.activateChatView()
|
||
});
|
||
|
||
this.addCommand({
|
||
id: 'new-llm-agent-chat',
|
||
name: 'Новый чат LLM Agent',
|
||
callback: () => this.handleNewChat()
|
||
});
|
||
|
||
// Добавляем иконку в ribbon
|
||
this.addRibbonIcon('brain-circuit', 'LLM Agent', () => {
|
||
this.activateGraphView();
|
||
this.activateChatView();
|
||
});
|
||
|
||
console.log('LLM Agent plugin загружен');
|
||
|
||
this.activeStreamControllers = new Map<string, AbortController>();
|
||
|
||
this.eventBus.on('stream-started', this.handleStreamStarted.bind(this));
|
||
this.eventBus.on('stream-ended', this.handleStreamEnded.bind(this));
|
||
|
||
this.eventBus.on('stop-all-streams', () => {
|
||
this.activeStreamControllers.forEach((controller, nodeId) => {
|
||
controller.abort();
|
||
});
|
||
this.activeStreamControllers.clear();
|
||
new Notice('Отменены все генерации во всех графах', 2000);
|
||
this.eventBus.emit('global-stream-state-changed', false);
|
||
});
|
||
|
||
// This adds a settings tab so the user can configure various aspects of the plugin
|
||
this.addSettingTab(new SampleSettingTab(this.app, this));
|
||
|
||
await this.fetchAvailableModels();
|
||
|
||
// Ждем полной инициализации хранилища перед вызовом зависимых функций
|
||
// await this.syncSettings();
|
||
this.app.workspace.onLayoutReady(async () => {
|
||
await this.syncSettings();
|
||
});
|
||
|
||
this.registerEvent(
|
||
this.app.vault.on('modify', async (file) => {
|
||
// Если изменился любой md файл, перестраховываемся и синхроним настройки
|
||
// (Backend получит обновленный текст промптов, если на них есть ссылки)
|
||
if (file instanceof TFile && file.extension === 'md' && file.path.startsWith(this.settings.promptsFolder)) {
|
||
// TODO: Проверить, что срабатывает
|
||
await this.syncSettings();
|
||
}
|
||
})
|
||
);
|
||
}
|
||
|
||
onunload() {
|
||
if (this.webSocketService) {
|
||
this.webSocketService.disconnect();
|
||
}
|
||
|
||
this.activeStreamControllers.clear();
|
||
|
||
this.eventBus.off('stream-started', this.handleStreamStarted);
|
||
this.eventBus.off('stream-ended', this.handleStreamEnded);
|
||
|
||
console.log('LLM Agent plugin выгружен');
|
||
}
|
||
|
||
// ----------------------------------------------- Settings Management ---------------------------------------------------------------
|
||
|
||
async loadSettings() {
|
||
this.settings = Object.assign({}, DEFAULT_SETTINGS, await this.loadData());
|
||
}
|
||
|
||
async saveSettings() {
|
||
await this.saveData(this.settings);
|
||
this.eventBus.emit('settings-changed');
|
||
}
|
||
|
||
// ----------------------------------------------- View Management ---------------------------------------------------------------
|
||
|
||
async activateGraphView() {
|
||
const existingLeaf = this.app.workspace.getLeavesOfType(GRAPH_VIEW_TYPE)[0];
|
||
if (existingLeaf) {
|
||
this.app.workspace.revealLeaf(existingLeaf);
|
||
} else {
|
||
await this.app.workspace.getLeaf('tab').setViewState({
|
||
type: GRAPH_VIEW_TYPE,
|
||
active: true
|
||
});
|
||
}
|
||
}
|
||
|
||
async activateChatView() {
|
||
const existingLeaf = this.app.workspace.getLeavesOfType(CHAT_VIEW_TYPE)[0];
|
||
if (existingLeaf) {
|
||
this.app.workspace.revealLeaf(existingLeaf);
|
||
} else {
|
||
const rightLeaf = this.app.workspace.getRightLeaf(false);
|
||
if (rightLeaf) {
|
||
await rightLeaf.setViewState({
|
||
type: CHAT_VIEW_TYPE,
|
||
active: true
|
||
});
|
||
}
|
||
}
|
||
}
|
||
|
||
// ----------------------------------------------- Event Handlers ---------------------------------------------------------------
|
||
|
||
handleNewChat() {
|
||
this.eventBus.emit('new-chat');
|
||
}
|
||
|
||
handleStreamStarted(data: { nodeId: string; controller: AbortController }): void {
|
||
this.activeStreamControllers.set(data.nodeId, data.controller);
|
||
this.eventBus.emit('global-stream-state-changed', true);
|
||
}
|
||
|
||
handleStreamEnded(data: { nodeId: string }): void {
|
||
this.activeStreamControllers.delete(data.nodeId);
|
||
if (this.activeStreamControllers.size === 0) {
|
||
this.eventBus.emit('global-stream-state-changed', false);
|
||
}
|
||
}
|
||
}
|
||
|
||
// ----------------------------------------------- Sample Setting Tab ---------------------------------------------------------------
|
||
|
||
class SampleSettingTab extends PluginSettingTab {
|
||
plugin: LLMAgentPlugin;
|
||
activeTabId: string = 'basic';
|
||
|
||
constructor(app: App, plugin: LLMAgentPlugin) {
|
||
super(app, plugin);
|
||
this.plugin = plugin;
|
||
}
|
||
|
||
display(): void {
|
||
const { containerEl } = this;
|
||
containerEl.empty();
|
||
|
||
// Навигация (Табы)
|
||
const tabHeader = containerEl.createDiv({ cls: 'settings-tab-header' });
|
||
tabHeader.style.cssText = 'display: flex; gap: 10px; margin-bottom: 20px; border-bottom: 1px solid var(--background-modifier-border); padding-bottom: 10px;';
|
||
|
||
const tabs = [
|
||
{ id: 'basic', label: '⚙️ Основные' },
|
||
{ id: 'agency', label: '🤖 Агент (ReAct Tools)' },
|
||
{ id: 'mcp', label: '🔌 MCP Серверы' },
|
||
{ 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 === 'mcp') this.renderMCPSettings(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) => {
|
||
this.plugin.availableModels.forEach(model => dropdown.addOption(model, model));
|
||
dropdown.setValue(this.plugin.settings.defaultModel)
|
||
.onChange(async (value) => {
|
||
this.plugin.settings.defaultModel = value;
|
||
await this.plugin.saveSettings();
|
||
await this.plugin.syncSettings();
|
||
});
|
||
});
|
||
|
||
new Setting(container)
|
||
.setName('Модель суммаризации')
|
||
.setDesc('Выберите модель для суммаризации.')
|
||
.addDropdown((dropdown) => {
|
||
this.plugin.availableModels.forEach(model => dropdown.addOption(model, model));
|
||
dropdown.setValue(this.plugin.settings.summarizationModel)
|
||
.onChange(async (value) => {
|
||
this.plugin.settings.summarizationModel = value;
|
||
await this.plugin.saveSettings();
|
||
await this.plugin.syncSettings();
|
||
});
|
||
});
|
||
|
||
new Setting(container)
|
||
.setName('Температура чата по умолчанию')
|
||
.setDesc(`Значение от 0.0 до 2.0 (по умолчанию ${LLM_DEFAULTS.TEMPERATURE}). Влияет на креативность.`)
|
||
.addText((text) =>
|
||
text
|
||
.setPlaceholder(String(LLM_DEFAULTS.TEMPERATURE))
|
||
.setValue(String(this.plugin.settings.temperatureChat))
|
||
.onChange(async (value) => {
|
||
const num = parseFloat(value);
|
||
this.plugin.settings.temperatureChat = isNaN(num) ? LLM_DEFAULTS.TEMPERATURE : num;
|
||
await this.plugin.saveSettings();
|
||
await this.plugin.syncSettings();
|
||
})
|
||
);
|
||
|
||
new Setting(container)
|
||
.setName('Макс. токенов чата по умолчанию')
|
||
.setDesc('Оставьте пустым для отсутствия лимита.')
|
||
.addText((text) =>
|
||
text
|
||
.setPlaceholder('Лимит')
|
||
.setValue(this.plugin.settings.maxTokensChatDefault)
|
||
.onChange(async (value) => {
|
||
this.plugin.settings.maxTokensChatDefault = value;
|
||
await this.plugin.saveSettings();
|
||
await this.plugin.syncSettings();
|
||
})
|
||
);
|
||
|
||
// Этот промпт будет отправляться LLM перед каждым диалогом
|
||
this.addPromptAutocomplete(new Setting(container).setName('Глобальный системный промпт'), 'systemPrompt');
|
||
|
||
container.createEl('h3', { text: 'Управление ссылками и файлами' });
|
||
|
||
new Setting(container)
|
||
.setName('Папка с промптами')
|
||
.setDesc('Путь к папке, содержащей промпты для команд #/##')
|
||
.addText((text) =>
|
||
text
|
||
.setPlaceholder('prompts')
|
||
.setValue(this.plugin.settings.promptsFolder)
|
||
.onChange(async (value) => {
|
||
this.plugin.settings.promptsFolder = value;
|
||
await this.plugin.saveSettings();
|
||
await this.plugin.syncSettings();
|
||
})
|
||
);
|
||
|
||
new Setting(container)
|
||
.setName('Папка кеша для вложений')
|
||
.setDesc('Абсолютный или относительный путь к папке для кеширования файлов')
|
||
.addText((text) =>
|
||
text
|
||
.setPlaceholder('ref-cache')
|
||
.setValue(this.plugin.settings.refCacheFolder)
|
||
.onChange(async (value) => {
|
||
this.plugin.settings.refCacheFolder = value;
|
||
await this.plugin.saveSettings();
|
||
await this.plugin.syncSettings();
|
||
})
|
||
);
|
||
|
||
// Настройки ссылок на файлы
|
||
new Setting(container)
|
||
.setName('Включить ссылки на файлы')
|
||
.setDesc('Разрешить использование @/@@ ссылок на файлы в хранилище')
|
||
.addToggle((toggle) =>
|
||
toggle.setValue(this.plugin.settings.enableFileReferences).onChange(async (value) => {
|
||
this.plugin.settings.enableFileReferences = value;
|
||
await this.plugin.saveSettings();
|
||
await this.plugin.syncSettings();
|
||
})
|
||
);
|
||
|
||
// Настройки ссылок на промпты
|
||
new Setting(container)
|
||
.setName('Включить ссылки на промпты')
|
||
.setDesc('Разрешить использование #/## ссылок на промпты')
|
||
.addToggle((toggle) =>
|
||
toggle.setValue(this.plugin.settings.enablePromptReferences).onChange(async (value) => {
|
||
this.plugin.settings.enablePromptReferences = value;
|
||
await this.plugin.saveSettings();
|
||
await this.plugin.syncSettings();
|
||
})
|
||
);
|
||
|
||
// Новое поле для настройки исключенных папок
|
||
new Setting(container)
|
||
.setName('Исключенные папки для ссылок')
|
||
.setDesc('Список относительных путей к папкам, файлы из которых не будут предлагаться в автодополнении ссылок (@/@@).')
|
||
.addToggle((toggle) =>
|
||
toggle.setValue(this.plugin.settings.enableFileReferences).onChange(async (value) => {
|
||
this.plugin.settings.enableFileReferences = value;
|
||
await this.plugin.saveSettings();
|
||
await this.plugin.syncSettings();
|
||
})
|
||
);
|
||
|
||
// Динамический список исключенных папок
|
||
container.createEl('h3', { text: 'Исключенные папки' });
|
||
this.plugin.settings.excludedFolders.forEach((folder, index) => {
|
||
new Setting(container)
|
||
.setClass('llm-agent-excluded-folder-item') // Добавляем класс для стилизации
|
||
.addText((text) => {
|
||
text.setPlaceholder('Путь к папке (например, templates/)' + index)
|
||
.setValue(folder)
|
||
.onChange(async (value) => {
|
||
this.plugin.settings.excludedFolders[index] = value;
|
||
await this.plugin.saveSettings();
|
||
await this.plugin.syncSettings();
|
||
// Очищаем кэш автокомплита при изменении настроек
|
||
this.plugin.eventBus.emit('settings-changed');
|
||
});
|
||
})
|
||
.addExtraButton((button) => {
|
||
button
|
||
.setIcon('x')
|
||
.setTooltip('Удалить папку')
|
||
.onClick(async () => {
|
||
this.plugin.settings.excludedFolders.splice(index, 1);
|
||
await this.plugin.saveSettings();
|
||
await this.plugin.syncSettings();
|
||
this.display(); // Перерисовать настройки для обновления списка
|
||
// Очищаем кэш автокомплита при изменении настроек
|
||
this.plugin.eventBus.emit('settings-changed');
|
||
});
|
||
});
|
||
});
|
||
|
||
new Setting(container).addButton((button) => {
|
||
button
|
||
.setButtonText('Добавить папку')
|
||
.setIcon('plus')
|
||
.onClick(async () => {
|
||
this.plugin.settings.excludedFolders.push(''); // Добавляем пустое поле
|
||
await this.plugin.saveSettings();
|
||
await this.plugin.syncSettings();
|
||
this.display(); // Перерисовать настройки для обновления списка
|
||
});
|
||
});
|
||
}
|
||
|
||
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();
|
||
}));
|
||
|
||
new Setting(container)
|
||
.setName('Вывод отладочной информации')
|
||
.setDesc('Выводить JSON-запросы и результаты работы инструментов после ответа агента.')
|
||
.addToggle((toggle) =>
|
||
toggle.setValue(this.plugin.settings.showDebugInfo).onChange(async (value) => {
|
||
this.plugin.settings.showDebugInfo = value;
|
||
await this.plugin.saveSettings();
|
||
await this.plugin.syncSettings();
|
||
})
|
||
);
|
||
|
||
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 scriptRow = toolDiv.createDiv({ attr: { style: 'display: flex; gap: 10px; align-items: flex-end;' } });
|
||
|
||
const scriptSetting = new Setting(scriptRow)
|
||
.setName('Скрипт')
|
||
.setDesc('Путь к .py (используйте @)');
|
||
scriptSetting.settingEl.style.flex = '3';
|
||
scriptSetting.settingEl.style.border = 'none';
|
||
this.addPromptAutocompleteStrict(scriptSetting, tool, 'scriptPath', 'py');
|
||
|
||
const methodSetting = new Setting(scriptRow)
|
||
.setName('Метод')
|
||
.setDesc('Имя функции')
|
||
.addText(t => t
|
||
.setPlaceholder('') // 'main'
|
||
.setValue(tool.methodName || '') // 'main'
|
||
.onChange(async v => {
|
||
tool.methodName = v.trim();
|
||
await this.plugin.saveSettings();
|
||
})
|
||
);
|
||
methodSetting.settingEl.style.flex = '1';
|
||
methodSetting.settingEl.style.border = 'none';
|
||
methodSetting.infoEl.style.display = 'none'; // Скроем описание метода для компактности
|
||
|
||
|
||
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: '', methodName: '', hiddenPrompt: '' }); // methodName: 'main'
|
||
await this.plugin.saveSettings();
|
||
this.display();
|
||
}));
|
||
}
|
||
|
||
renderMCPSettings(container: HTMLElement) {
|
||
container.createEl('h3', { text: 'MCP (Model Context Protocol) Серверы' });
|
||
container.createEl('p', { text: 'Стандартизированные инструменты от Anthropic (через stdio). Используйте docker, npx или uvx.' });
|
||
|
||
if (!this.plugin.settings.mcpServers) this.plugin.settings.mcpServers = [];
|
||
|
||
this.plugin.settings.mcpServers.forEach((server, index) => {
|
||
if (server.isEnabled === undefined) server.isEnabled = true;
|
||
|
||
const serverDiv = container.createDiv({
|
||
attr: { style: `border: 1px solid var(--interactive-accent); padding: 10px; margin-bottom: 15px; border-radius: 8px; background-color: var(--background-secondary-alt); transition: opacity 0.3s; opacity: ${server.isEnabled ? '1' : '0.5'}` }
|
||
});
|
||
|
||
new Setting(serverDiv)
|
||
.setName(`Имя сервера`)
|
||
.addToggle(tg => tg
|
||
.setTooltip(server.isEnabled ? 'Выключить сервер' : 'Включить сервер')
|
||
.setValue(server.isEnabled!)
|
||
.onChange(async v => {
|
||
server.isEnabled = v;
|
||
serverDiv.style.opacity = v ? '1' : '0.5';
|
||
tg.setTooltip(v ? 'Выключить сервер' : 'Включить сервер');
|
||
await this.plugin.saveSettings();
|
||
await this.plugin.syncSettings(); // Мгновенно синхроним с бэкендом
|
||
})
|
||
)
|
||
.addText(t => t.setValue(server.name).onChange(async v => {
|
||
server.name = v.replace(/[^a-zA-Z0-9_\-]/g, '');
|
||
await this.plugin.saveSettings();
|
||
}))
|
||
.addExtraButton(b => b.setIcon('trash').setTooltip('Удалить этот сервер').onClick(async () => {
|
||
this.plugin.settings.mcpServers.splice(index, 1);
|
||
await this.plugin.saveSettings();
|
||
this.display();
|
||
}));
|
||
|
||
new Setting(serverDiv)
|
||
.setName('Команда (Command)')
|
||
.setDesc('Пример: docker, npx, uvx')
|
||
.addText(t => {
|
||
t.inputEl.style.width = '100%';
|
||
t.setValue(server.command).onChange(async v => {
|
||
server.command = v;
|
||
await this.plugin.saveSettings();
|
||
});
|
||
});
|
||
|
||
new Setting(serverDiv)
|
||
.setName('Аргументы (Args)')
|
||
.setDesc('Через пробел. Пример: run -i --rm -e DDG_SAFE_SEARCH=MODERATE ddg-mcp-server')
|
||
.addTextArea(t => {
|
||
t.inputEl.style.width = '100%';
|
||
t.setValue(server.args).onChange(async v => {
|
||
server.args = v;
|
||
await this.plugin.saveSettings();
|
||
});
|
||
});
|
||
|
||
new Setting(serverDiv)
|
||
.setName('Переменные окружения (Env)')
|
||
.setDesc('Опционально, через запятую. Формат (KEY=VAL,KEY2=VAL2)')
|
||
.addText(t => {
|
||
t.inputEl.style.width = '100%';
|
||
t.setValue(server.envString || '').onChange(async v => {
|
||
server.envString = v;
|
||
await this.plugin.saveSettings();
|
||
});
|
||
});
|
||
|
||
new Setting(serverDiv)
|
||
.setName('Общая инструкция для сервера (Над-инструкция)')
|
||
.setDesc('Эти инструкции будут добавлены в системный промпт при использовании графа.')
|
||
.addTextArea(t => {
|
||
t.inputEl.style.width = '100%';
|
||
t.inputEl.style.height = '60px';
|
||
t.setValue(server.serverInstruction || '').onChange(async v => {
|
||
server.serverInstruction = v;
|
||
await this.plugin.saveSettings();
|
||
});
|
||
});
|
||
|
||
this.createFetchedTools(server, serverDiv);
|
||
});
|
||
|
||
new Setting(container).addButton(b => b.setButtonText('➕ Добавить MCP сервер').setCta().onClick(async () => {
|
||
this.plugin.settings.mcpServers.push({ id: uuidv4(), name: 'duckduckgo', command: 'docker', args: 'run -i --rm ddg-mcp-server', envString: '', isEnabled: true });
|
||
await this.plugin.saveSettings();
|
||
this.display();
|
||
}));
|
||
}
|
||
|
||
createFetchedTools(server: MCPServerConfig, serverDiv: HTMLDivElement) {
|
||
const toolsContainer = serverDiv.createDiv({ cls: 'mcp-tools-container', attr: { style: 'margin-top: 15px; border-top: 1px solid var(--background-modifier-border); padding-top: 10px;' } });
|
||
|
||
const fetchBtn = toolsContainer.createEl('button', { text: '🔄 Подгрузить/Обновить инструменты' });
|
||
fetchBtn.style.marginBottom = '10px';
|
||
|
||
fetchBtn.onclick = async () => {
|
||
fetchBtn.textContent = '⏳ Подгрузка...';
|
||
fetchBtn.disabled = true;
|
||
try {
|
||
const response = await fetch(`${this.plugin.settings.apiBaseUrl}/mcp/fetch`, {
|
||
method: 'POST',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify(server)
|
||
});
|
||
|
||
if (!response.ok) throw new Error("Ошибка HTTP: " + response.status);
|
||
|
||
const remoteTools: {name: string, description: string}[] = await response.json();
|
||
|
||
// Логика синхронизации: оставляем кастомные описания, удаляем пропавшие, добавляем новые
|
||
if (!server.fetchedTools) server.fetchedTools = [];
|
||
const existingMap = new Map(server.fetchedTools.map(t => [t.name, t]));
|
||
|
||
server.fetchedTools = remoteTools.map(rt => {
|
||
if (existingMap.has(rt.name)) {
|
||
const existing = existingMap.get(rt.name)!;
|
||
return { ...existing, originalDescription: rt.description }; // обновляем оригинальное описание на всякий случай
|
||
}
|
||
return { name: rt.name, originalDescription: rt.description, customDescription: '', isEnabled: true };
|
||
});
|
||
|
||
await this.plugin.saveSettings();
|
||
new Notice(`Загружено инструментов: ${remoteTools.length}`);
|
||
this.display(); // Перерисовываем UI
|
||
} catch (e) {
|
||
console.error(e);
|
||
new Notice('Ошибка подгрузки инструментов: ' + e);
|
||
} finally {
|
||
fetchBtn.textContent = '🔄 Подгрузить/Обновить инструменты';
|
||
fetchBtn.disabled = false;
|
||
}
|
||
};
|
||
|
||
// Отрисовка списка тулов
|
||
if (server.fetchedTools && server.fetchedTools.length > 0) {
|
||
server.fetchedTools.forEach((tool, tIndex) => {
|
||
if (tool.isEnabled === undefined) tool.isEnabled = true;
|
||
|
||
const toolDiv = toolsContainer.createDiv({
|
||
attr: { style: `margin-bottom: 10px; padding: 10px; background: var(--background-primary); border-radius: 6px; transition: opacity 0.3s; opacity: ${tool.isEnabled ? '1' : '0.5'}` }
|
||
});
|
||
|
||
// Заголовок тула с переключателем
|
||
const headerDiv = toolDiv.createDiv({ attr: { style: 'display: flex; justify-content: space-between; align-items: flex-start; margin-bottom: 5px;' } });
|
||
headerDiv.createEl('div', { text: `🛠 ${tool.name}`, attr: { style: 'font-weight: bold; margin-top: 5px;' } });
|
||
|
||
const toggleSetting = new Setting(headerDiv)
|
||
.addToggle(tg => tg
|
||
.setTooltip(tool.isEnabled ? 'Выключить инструмент' : 'Включить инструмент')
|
||
.setValue(tool.isEnabled!)
|
||
.onChange(async v => {
|
||
tool.isEnabled = v;
|
||
toolDiv.style.opacity = v ? '1' : '0.5';
|
||
tg.setTooltip(v ? 'Выключить инструмент' : 'Включить инструмент');
|
||
await this.plugin.saveSettings();
|
||
await this.plugin.syncSettings(); // Мгновенно синхроним с бэкендом
|
||
})
|
||
);
|
||
// Убираем лишние рамки и отступы у элемента Setting для компактности в шапке
|
||
toggleSetting.settingEl.style.border = 'none';
|
||
toggleSetting.settingEl.style.padding = '0';
|
||
toggleSetting.infoEl.remove();
|
||
|
||
// Описание
|
||
toolDiv.createEl('div', { text: tool.originalDescription, attr: { style: 'font-size: 0.85em; color: var(--text-muted); margin-bottom: 8px;' } });
|
||
|
||
new Setting(toolDiv)
|
||
.setName('Дополнительные инструкции')
|
||
.setDesc('Этот текст будет добавлен к оригинальному описанию инструмента. Оставьте пустым, если не требуется.')
|
||
.addTextArea(t => {
|
||
t.inputEl.style.width = '100%';
|
||
t.setValue(tool.customDescription).onChange(async v => {
|
||
tool.customDescription = v;
|
||
await this.plugin.saveSettings();
|
||
});
|
||
});
|
||
});
|
||
}
|
||
}
|
||
|
||
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);
|
||
|
||
setting.addTextArea((text) => {
|
||
const el = text.inputEl;
|
||
const autocomplete = new ReferenceAutocomplete(el, manager);
|
||
|
||
autocomplete.onItemSelected(async (item, match) => {
|
||
const val = el.value;
|
||
const before = val.substring(0, match.startIndex);
|
||
const after = val.substring(match.endIndex);
|
||
|
||
// Если в имени есть пробел — оборачиваем в кавычки
|
||
const nameToInsert = item.name.includes(' ') ? `"${item.name}"` : item.name;
|
||
const newValue = before + "#" + nameToInsert + after;
|
||
|
||
// Обновляем визуально в поле
|
||
el.value = newValue;
|
||
|
||
// Обновляем в конфиге (используем приведение типа, чтобы TS не ругался)
|
||
(this.plugin.settings as any)[settingKey] = newValue;
|
||
|
||
await this.plugin.saveSettings();
|
||
await this.plugin.syncSettings();
|
||
|
||
autocomplete.hideAutocomplete();
|
||
});
|
||
|
||
text.setPlaceholder('Впишите текст или начните с # для выбора промпта')
|
||
.setValue(String(this.plugin.settings[settingKey]))
|
||
.onChange(async (value) => {
|
||
// Прямое присваивание через приведение к any для обхода строгой проверки типов
|
||
(this.plugin.settings as any)[settingKey] = value;
|
||
|
||
await this.plugin.saveSettings();
|
||
await this.plugin.syncSettings();
|
||
|
||
const cursor = el.selectionStart;
|
||
const textValue = el.value;
|
||
const ref = ReferenceParser.getCurrentReference(textValue, cursor);
|
||
|
||
if (ref && ref.type === 'prompt') {
|
||
// Используем getBoundingClientRect для позиционирования меню под полем ввода
|
||
const rect = el.getBoundingClientRect();
|
||
autocomplete.showAutocomplete(ref, rect);
|
||
} else {
|
||
autocomplete.hideAutocomplete();
|
||
}
|
||
});
|
||
|
||
el.addEventListener('keydown', (e) => {
|
||
if (autocomplete.isVisible()) {
|
||
if (autocomplete.handleKeydown(e)) {
|
||
e.stopPropagation();
|
||
}
|
||
}
|
||
});
|
||
});
|
||
}
|
||
}
|