Make prompts parameters not simple text but names of prompt files
This commit is contained in:
parent
f2afaa4836
commit
06949fc13e
136
main.ts
136
main.ts
|
|
@ -1,8 +1,12 @@
|
||||||
import { App, Editor, MarkdownView, Modal, Notice, Plugin, PluginSettingTab, Setting, ItemView, WorkspaceLeaf } from 'obsidian';
|
import { App, Editor, MarkdownView, Modal, Notice, Plugin, PluginSettingTab, Setting, ItemView, WorkspaceLeaf, TFile } from 'obsidian';
|
||||||
import { EventBus } from './src/utils/EventBus';
|
import { EventBus } from './src/utils/EventBus';
|
||||||
import { GraphView, GRAPH_VIEW_TYPE } from './src/views/GraphView';
|
import { GraphView, GRAPH_VIEW_TYPE } from './src/views/GraphView';
|
||||||
import { ChatView, CHAT_VIEW_TYPE } from './src/views/ChatView';
|
import { ChatView, CHAT_VIEW_TYPE } from './src/views/ChatView';
|
||||||
import { WebSocketService } from './src/utils/WebSocketService';
|
import { WebSocketService } from './src/utils/WebSocketService';
|
||||||
|
import { TemplateEngine } from './src/references/TemplateEngine';
|
||||||
|
import { AutocompleteManager } from './src/references/AutocompleteManager';
|
||||||
|
import { ReferenceAutocomplete } from './src/references/ReferenceAutocomplete';
|
||||||
|
import { ReferenceParser } from './src/references/ReferenceParser';
|
||||||
|
|
||||||
import 'src/css/styles.css';
|
import 'src/css/styles.css';
|
||||||
|
|
||||||
|
|
@ -60,10 +64,20 @@ export default class LLMAgentPlugin extends Plugin {
|
||||||
|
|
||||||
async syncSettings() {
|
async syncSettings() {
|
||||||
try {
|
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);
|
||||||
|
|
||||||
const response = await fetch(`${this.settings.apiBaseUrl}/settings/sync`, {
|
const response = await fetch(`${this.settings.apiBaseUrl}/settings/sync`, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: { 'Content-Type': 'application/json' },
|
headers: { 'Content-Type': 'application/json' },
|
||||||
body: JSON.stringify(this.settings)
|
body: JSON.stringify(settingsToSync)
|
||||||
});
|
});
|
||||||
if (response.ok) {
|
if (response.ok) {
|
||||||
console.log("LLM Agent: Settings synced with backend.");
|
console.log("LLM Agent: Settings synced with backend.");
|
||||||
|
|
@ -141,6 +155,17 @@ export default class LLMAgentPlugin extends Plugin {
|
||||||
await this.fetchAvailableModels();
|
await this.fetchAvailableModels();
|
||||||
// Вызываем один раз при запуске
|
// Вызываем один раз при запуске
|
||||||
await this.syncSettings();
|
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() {
|
onunload() {
|
||||||
|
|
@ -267,19 +292,8 @@ class SampleSettingTab extends PluginSettingTab {
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
new Setting(containerEl)
|
// Этот промпт будет отправляться LLM перед каждым диалогом
|
||||||
.setName('Системный промпт')
|
this.addPromptAutocomplete(new Setting(containerEl).setName('Системный промпт'), 'systemPrompt');
|
||||||
.setDesc('Этот промпт будет отправляться LLM перед каждым диалогом.')
|
|
||||||
.addTextArea((text) =>
|
|
||||||
text
|
|
||||||
.setPlaceholder('Ты полезный ИИ ассистент.')
|
|
||||||
.setValue(this.plugin.settings.systemPrompt)
|
|
||||||
.onChange(async (value) => {
|
|
||||||
this.plugin.settings.systemPrompt = value;
|
|
||||||
await this.plugin.saveSettings();
|
|
||||||
await this.plugin.syncSettings();
|
|
||||||
})
|
|
||||||
);
|
|
||||||
|
|
||||||
containerEl.createEl('h2', { text: 'Голосовое управление (Voice Service)' });
|
containerEl.createEl('h2', { text: 'Голосовое управление (Voice Service)' });
|
||||||
|
|
||||||
|
|
@ -350,32 +364,16 @@ class SampleSettingTab extends PluginSettingTab {
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
new Setting(containerEl)
|
// Инструкции для Промпта №1.
|
||||||
.setName('Регулярный промпт')
|
this.addPromptAutocomplete(
|
||||||
.setDesc('Инструкции для Промпта №1.')
|
new Setting(containerEl).setName('Регулярный промпт'),
|
||||||
.addTextArea((text) =>
|
'promptRegular'
|
||||||
text
|
|
||||||
// .setPlaceholder('prompts/regular_analysis.md')
|
|
||||||
.setValue(this.plugin.settings.promptRegular)
|
|
||||||
.onChange(async (value) => {
|
|
||||||
this.plugin.settings.promptRegular = value;
|
|
||||||
await this.plugin.saveSettings();
|
|
||||||
await this.plugin.syncSettings();
|
|
||||||
})
|
|
||||||
);
|
);
|
||||||
|
|
||||||
new Setting(containerEl)
|
// Инструкции для Промпта №2.
|
||||||
.setName('Промпт команд')
|
this.addPromptAutocomplete(
|
||||||
.setDesc('Инструкции для Промпта №2.')
|
new Setting(containerEl).setName('Промпт команд'),
|
||||||
.addTextArea((text) =>
|
'promptCommands'
|
||||||
text
|
|
||||||
// .setPlaceholder('prompts/voice_commands.md')
|
|
||||||
.setValue(this.plugin.settings.promptCommands)
|
|
||||||
.onChange(async (value) => {
|
|
||||||
this.plugin.settings.promptCommands = value;
|
|
||||||
await this.plugin.saveSettings();
|
|
||||||
await this.plugin.syncSettings();
|
|
||||||
})
|
|
||||||
);
|
);
|
||||||
|
|
||||||
containerEl.createEl('h2', { text: 'Управление ссылками и файлами' });
|
containerEl.createEl('h2', { text: 'Управление ссылками и файлами' });
|
||||||
|
|
@ -487,4 +485,64 @@ class SampleSettingTab extends PluginSettingTab {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -9,6 +9,9 @@ import { setIcon } from 'obsidian';
|
||||||
|
|
||||||
// ----------------------------------------------- Reference Autocomplete Component ---------------------------------------------------------------
|
// ----------------------------------------------- Reference Autocomplete Component ---------------------------------------------------------------
|
||||||
|
|
||||||
|
const CHAT_CONTAINER_SELECTOR = 'chat-input-container';
|
||||||
|
const SETTINGS_CONTAINER_SELECTOR = 'vertical-tab-content-container';
|
||||||
|
|
||||||
export class ReferenceAutocomplete {
|
export class ReferenceAutocomplete {
|
||||||
private container: HTMLElement;
|
private container: HTMLElement;
|
||||||
private dropdown: HTMLElement;
|
private dropdown: HTMLElement;
|
||||||
|
|
@ -23,7 +26,13 @@ export class ReferenceAutocomplete {
|
||||||
constructor(inputElement: HTMLElement, autocompleteManager: AutocompleteManager) {
|
constructor(inputElement: HTMLElement, autocompleteManager: AutocompleteManager) {
|
||||||
this.inputElement = inputElement;
|
this.inputElement = inputElement;
|
||||||
this.autocompleteManager = autocompleteManager;
|
this.autocompleteManager = autocompleteManager;
|
||||||
this.container = this.inputElement.closest('.chat-input-container') as HTMLElement;
|
|
||||||
|
// Ищем контейнер чата, а если не находим (мы в настройках),
|
||||||
|
// используем родителя элемента или body
|
||||||
|
const chatContainer = this.inputElement.closest(`.${CHAT_CONTAINER_SELECTOR}`);
|
||||||
|
const settingsContainer = this.inputElement.closest(`.${SETTINGS_CONTAINER_SELECTOR}`);
|
||||||
|
|
||||||
|
this.container = (chatContainer || settingsContainer || document.body) as HTMLElement;
|
||||||
|
|
||||||
this.createDropdown();
|
this.createDropdown();
|
||||||
this.setupEventListeners();
|
this.setupEventListeners();
|
||||||
|
|
@ -143,6 +152,11 @@ export class ReferenceAutocomplete {
|
||||||
this.dropdown.className = 'reference-autocomplete';
|
this.dropdown.className = 'reference-autocomplete';
|
||||||
this.dropdown.style.display = 'none';
|
this.dropdown.style.display = 'none';
|
||||||
|
|
||||||
|
// Если мы в настройках, добавляем фиксированное позиционирование
|
||||||
|
if (this.container === document.body) {
|
||||||
|
this.dropdown.style.position = 'fixed';
|
||||||
|
}
|
||||||
|
|
||||||
// Добавляем dropdown в контейнер
|
// Добавляем dropdown в контейнер
|
||||||
this.container.appendChild(this.dropdown);
|
this.container.appendChild(this.dropdown);
|
||||||
}
|
}
|
||||||
|
|
@ -235,17 +249,49 @@ export class ReferenceAutocomplete {
|
||||||
// Чтобы получить корректные размеры, устанавливаем display: 'block'
|
// Чтобы получить корректные размеры, устанавливаем display: 'block'
|
||||||
this.dropdown.style.display = 'block';
|
this.dropdown.style.display = 'block';
|
||||||
|
|
||||||
|
const dropdownRect = this.dropdown.getBoundingClientRect();
|
||||||
|
const viewportHeight = window.innerHeight;
|
||||||
|
const viewportWidth = window.innerWidth;
|
||||||
|
|
||||||
|
let top: number;
|
||||||
|
let left: number;
|
||||||
|
let dropdownMinWidth: number;
|
||||||
|
|
||||||
|
const isChat = this.container.classList.contains(CHAT_CONTAINER_SELECTOR);
|
||||||
|
|
||||||
|
console.log("isChat: " + isChat)
|
||||||
|
|
||||||
const inputRect = this.inputElement.getBoundingClientRect();
|
const inputRect = this.inputElement.getBoundingClientRect();
|
||||||
const containerRect = this.container.getBoundingClientRect();
|
const containerRect = this.container.getBoundingClientRect();
|
||||||
const dropdownRect = this.dropdown.getBoundingClientRect(); // Получаем размеры дропдауна
|
|
||||||
const viewportHeight = window.innerHeight;
|
if (!isChat) {
|
||||||
|
// Позиционируем относительно inputElement, т.к. он уже в потоке документа
|
||||||
|
// Левый край дропдауна совпадает с левым краем курсора
|
||||||
|
left = containerRect.x - 35; // 35 - эмпирика, но что там должно быть, не понял
|
||||||
|
console.log("left: " + left)
|
||||||
|
|
||||||
|
// Дефолтное положение - под курсором
|
||||||
|
top = cursorRect.bottom - containerRect.top + 5; // +5px отступ
|
||||||
|
|
||||||
|
// Проверяем, есть ли место снизу
|
||||||
|
const spaceBelow = viewportHeight - cursorRect.bottom;
|
||||||
|
const spaceAbove = cursorRect.top;
|
||||||
|
|
||||||
|
if (spaceBelow < dropdownRect.height + 10 && spaceAbove > dropdownRect.height + 10) {
|
||||||
|
// Если снизу мало места и сверху достаточно, размещаем сверху
|
||||||
|
top = cursorRect.top - containerRect.top - dropdownRect.height - 5; // -5px отступ
|
||||||
|
}
|
||||||
|
|
||||||
|
dropdownMinWidth = 200;
|
||||||
|
} else {
|
||||||
|
// Старая логика для чата (Relative к container)
|
||||||
|
|
||||||
// Позиционируем относительно inputElement, т.к. он уже в потоке документа
|
// Позиционируем относительно inputElement, т.к. он уже в потоке документа
|
||||||
// Левый край дропдауна совпадает с левым краем курсора
|
// Левый край дропдауна совпадает с левым краем курсора
|
||||||
let left = cursorRect.left - containerRect.left;
|
left = cursorRect.left - containerRect.left;
|
||||||
|
|
||||||
// Дефолтное положение - под курсором
|
// Дефолтное положение - под курсором
|
||||||
let top = cursorRect.bottom - containerRect.top + 5; // +5px отступ
|
top = cursorRect.bottom - containerRect.top + 5; // +5px отступ
|
||||||
|
|
||||||
// Проверяем, есть ли место снизу
|
// Проверяем, есть ли место снизу
|
||||||
const spaceBelow = viewportHeight - cursorRect.bottom;
|
const spaceBelow = viewportHeight - cursorRect.bottom;
|
||||||
|
|
@ -267,11 +313,14 @@ export class ReferenceAutocomplete {
|
||||||
left = 0;
|
left = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.dropdown.style.top = `${top}px`;
|
|
||||||
this.dropdown.style.left = `${left}px`;
|
|
||||||
// Устанавливаем ширину дропдауна равной ширине inputElement, если он больше
|
// Устанавливаем ширину дропдауна равной ширине inputElement, если он больше
|
||||||
// Это предотвратит слишком узкий дропдаун, если имя файла короткое
|
// Это предотвратит слишком узкий дропдаун, если имя файла короткое
|
||||||
this.dropdown.style.minWidth = `${inputRect.width / 2}px`; // Минимум половина ширины инпута
|
dropdownMinWidth = inputRect.width / 2; // Минимум половина ширины инпута
|
||||||
|
}
|
||||||
|
|
||||||
|
this.dropdown.style.top = `${top}px`;
|
||||||
|
this.dropdown.style.left = `${left}px`;
|
||||||
|
this.dropdown.style.minWidth = `${dropdownMinWidth}px`;
|
||||||
this.dropdown.style.width = 'auto'; // Позволяем контенту определить ширину, но не меньше minWidth
|
this.dropdown.style.width = 'auto'; // Позволяем контенту определить ширину, но не меньше minWidth
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -74,13 +74,17 @@ export class ReferenceParser {
|
||||||
static getCurrentReference(text: string, cursorPosition: number): ReferenceMatch | null {
|
static getCurrentReference(text: string, cursorPosition: number): ReferenceMatch | null {
|
||||||
const beforeCursor = text.substring(0, cursorPosition);
|
const beforeCursor = text.substring(0, cursorPosition);
|
||||||
|
|
||||||
// ИЗМЕНЕНО: Поддержка !@, !#, !$
|
// Измененная регулярка: теперь она понимает, если после символа # идет кавычка
|
||||||
const incompleteRegex = /(![@#$]|[@#$])([^\s@#$"!]*?)$/;
|
// Группа 1: Префикс, Группа 2: Имя в кавычках, Группа 3: Имя без кавычек
|
||||||
|
const incompleteRegex = /(![@#$]|[@#$])(?:"([^"]*)|([^\s@#$"!]*?))$/;
|
||||||
const match = beforeCursor.match(incompleteRegex);
|
const match = beforeCursor.match(incompleteRegex);
|
||||||
|
|
||||||
if (match) {
|
if (match) {
|
||||||
const prefix = match[1];
|
const prefix = match[1];
|
||||||
const partialName = match[2];
|
const nameInQuotes = match[2]; // текст внутри кавычек
|
||||||
|
const nameWithoutQuotes = match[3]; // текст до первого пробела
|
||||||
|
const partialName = nameInQuotes !== undefined ? nameInQuotes : nameWithoutQuotes;
|
||||||
|
|
||||||
const startIndex = beforeCursor.length - match[0].length;
|
const startIndex = beforeCursor.length - match[0].length;
|
||||||
|
|
||||||
const typeChar = prefix.replace('!', '');
|
const typeChar = prefix.replace('!', '');
|
||||||
|
|
|
||||||
|
|
@ -71,6 +71,32 @@ export class TemplateEngine {
|
||||||
return expandedContent;
|
return expandedContent;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Главный метод для "сборки" промпта.
|
||||||
|
* Принимает либо чистый текст, либо путь вида #filename или просто текст с вкраплениями @{file}
|
||||||
|
*/
|
||||||
|
async processRawSettingValue(value: string): Promise<string> {
|
||||||
|
if (!value) return "";
|
||||||
|
|
||||||
|
// Регулярка теперь поддерживает опциональные кавычки:
|
||||||
|
// ^# - начинается на решетку
|
||||||
|
// (?:"([^"]+)"|([^\s]+)) - либо "имя с пробелом", либо имя_без_пробела
|
||||||
|
const fullLinkMatch = value.trim().match(/^#(?:"([^"]+)"|([^\s]+))$/);
|
||||||
|
|
||||||
|
if (fullLinkMatch) {
|
||||||
|
const promptName = fullLinkMatch[1] || fullLinkMatch[2]; // Берем из той группы, которая сработала
|
||||||
|
const allFiles = this.plugin.app.vault.getFiles();
|
||||||
|
const content = await this.loadPromptContent(promptName, allFiles);
|
||||||
|
if (content) {
|
||||||
|
return await this.expandPromptTemplate(content);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Если это не прямая ссылка на файл, а просто текст,
|
||||||
|
// всё равно прогоняем через расширитель (на случай если там внутри есть @{...})
|
||||||
|
return await this.expandPromptTemplate(value);
|
||||||
|
}
|
||||||
|
|
||||||
// ----------------------------------------------- Private Methods ---------------------------------------------------------------
|
// ----------------------------------------------- Private Methods ---------------------------------------------------------------
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user