First solution for voice panel
This commit is contained in:
parent
cb8ed95bcd
commit
aee60c1ac2
142
main.ts
142
main.ts
|
|
@ -17,6 +17,15 @@ interface LLMAgentSettings {
|
|||
enablePromptReferences: boolean;
|
||||
excludedFolders: string[];
|
||||
refCacheFolder: string;
|
||||
|
||||
voiceModel: string;
|
||||
voiceInterval: number;
|
||||
voiceDisplayLimit: number; // 3000
|
||||
voiceContextLimit: number; // N2 символов для LLM
|
||||
promptRegularPath: string;
|
||||
promptCommandsPath: string;
|
||||
markerStart: string; // "старт"
|
||||
markerStop: string; // "стоп"
|
||||
}
|
||||
|
||||
const DEFAULT_SETTINGS: LLMAgentSettings = {
|
||||
|
|
@ -27,7 +36,16 @@ const DEFAULT_SETTINGS: LLMAgentSettings = {
|
|||
enableFileReferences: true,
|
||||
enablePromptReferences: true,
|
||||
excludedFolders: ['.obsidian', 'llm-agent-backend', 'templates', 'ref-cache'], // Папка "templates" по умолчанию исключена
|
||||
refCacheFolder: 'ref-cache' // По умолчанию относительно vault
|
||||
refCacheFolder: 'ref-cache', // По умолчанию относительно vault
|
||||
|
||||
voiceModel: '',
|
||||
voiceInterval: 1,
|
||||
voiceDisplayLimit: 3000,
|
||||
voiceContextLimit: 3000,
|
||||
promptRegularPath: '',
|
||||
promptCommandsPath: '',
|
||||
markerStart: 'старт',
|
||||
markerStop: 'стоп'
|
||||
};
|
||||
|
||||
// ----------------------------------------------- Main Plugin Class ---------------------------------------------------------------
|
||||
|
|
@ -38,6 +56,30 @@ export default class LLMAgentPlugin extends Plugin {
|
|||
webSocketService: WebSocketService;
|
||||
activeStreamControllers: Map<string, AbortController>;
|
||||
|
||||
async syncVoiceSettings() {
|
||||
try {
|
||||
const response = await fetch(`${this.settings.apiBaseUrl}/settings/sync`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
systemPrompt: this.settings.systemPrompt,
|
||||
interval: this.settings.voiceInterval,
|
||||
context_limit: this.settings.voiceContextLimit,
|
||||
marker_start: this.settings.markerStart,
|
||||
marker_stop: this.settings.markerStop,
|
||||
// Передаем также пути к файлам промптов, если нужно
|
||||
prompt_regular: this.settings.promptRegularPath,
|
||||
prompt_commands: this.settings.promptCommandsPath
|
||||
})
|
||||
});
|
||||
if (response.ok) {
|
||||
console.log("LLM Agent: Voice settings synced with backend.");
|
||||
}
|
||||
} catch (e) {
|
||||
console.error("LLM Agent: Failed to sync settings", e);
|
||||
}
|
||||
}
|
||||
|
||||
async onload() {
|
||||
await this.loadSettings();
|
||||
|
||||
|
|
@ -84,6 +126,10 @@ export default class LLMAgentPlugin extends Plugin {
|
|||
|
||||
// This adds a settings tab so the user can configure various aspects of the plugin
|
||||
this.addSettingTab(new SampleSettingTab(this.app, this));
|
||||
|
||||
|
||||
// Вызываем один раз при запуске
|
||||
await this.syncVoiceSettings();
|
||||
}
|
||||
|
||||
onunload() {
|
||||
|
|
@ -165,15 +211,16 @@ class SampleSettingTab extends PluginSettingTab {
|
|||
|
||||
display(): void {
|
||||
const { containerEl } = this;
|
||||
|
||||
containerEl.empty();
|
||||
|
||||
containerEl.createEl('h2', { text: 'Основные настройки' });
|
||||
|
||||
new Setting(containerEl)
|
||||
.setName('API Base URL')
|
||||
.setDesc('Базовый URL для API LLM Agent')
|
||||
.addText((text) =>
|
||||
text
|
||||
.setPlaceholder('Введите URL')
|
||||
.setPlaceholder('http://localhost:5000/api')
|
||||
.setValue(this.plugin.settings.apiBaseUrl)
|
||||
.onChange(async (value) => {
|
||||
this.plugin.settings.apiBaseUrl = value;
|
||||
|
|
@ -181,7 +228,6 @@ class SampleSettingTab extends PluginSettingTab {
|
|||
})
|
||||
);
|
||||
|
||||
// Новое поле для системного промпта
|
||||
new Setting(containerEl)
|
||||
.setName('Системный промпт')
|
||||
.setDesc('Этот промпт будет отправляться LLM перед каждым диалогом.')
|
||||
|
|
@ -195,7 +241,92 @@ class SampleSettingTab extends PluginSettingTab {
|
|||
})
|
||||
);
|
||||
|
||||
// Новое поле для папки промптов
|
||||
containerEl.createEl('h2', { text: 'Голосовое управление (Voice Service)' });
|
||||
|
||||
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.syncVoiceSettings();
|
||||
})
|
||||
);
|
||||
|
||||
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.syncVoiceSettings();
|
||||
})
|
||||
);
|
||||
|
||||
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.syncVoiceSettings();
|
||||
})
|
||||
);
|
||||
|
||||
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.syncVoiceSettings();
|
||||
})
|
||||
);
|
||||
|
||||
new Setting(containerEl)
|
||||
.setName('Регулярный промпт')
|
||||
.setDesc('Инструкции для Промпта №1.')
|
||||
.addTextArea((text) =>
|
||||
text
|
||||
// .setPlaceholder('prompts/regular_analysis.md')
|
||||
.setValue(this.plugin.settings.promptRegularPath)
|
||||
.onChange(async (value) => {
|
||||
this.plugin.settings.promptRegularPath = value;
|
||||
await this.plugin.saveSettings();
|
||||
await this.plugin.syncVoiceSettings();
|
||||
})
|
||||
);
|
||||
|
||||
new Setting(containerEl)
|
||||
.setName('Промпт команд')
|
||||
.setDesc('Инструкции для Промпта №2.')
|
||||
.addTextArea((text) =>
|
||||
text
|
||||
// .setPlaceholder('prompts/voice_commands.md')
|
||||
.setValue(this.plugin.settings.promptCommandsPath)
|
||||
.onChange(async (value) => {
|
||||
this.plugin.settings.promptCommandsPath = value;
|
||||
await this.plugin.saveSettings();
|
||||
await this.plugin.syncVoiceSettings();
|
||||
})
|
||||
);
|
||||
|
||||
containerEl.createEl('h2', { text: 'Управление ссылками и файлами' });
|
||||
|
||||
new Setting(containerEl)
|
||||
.setName('Папка с промптами')
|
||||
.setDesc('Путь к папке, содержащей промпты для команд #/##')
|
||||
|
|
@ -256,6 +387,7 @@ class SampleSettingTab extends PluginSettingTab {
|
|||
);
|
||||
|
||||
// Динамический список исключенных папок
|
||||
containerEl.createEl('h3', { text: 'Исключенные папки' });
|
||||
this.plugin.settings.excludedFolders.forEach((folder, index) => {
|
||||
new Setting(containerEl)
|
||||
.setClass('llm-agent-excluded-folder-item') // Добавляем класс для стилизации
|
||||
|
|
|
|||
71
src/components/VoicePanel.ts
Normal file
71
src/components/VoicePanel.ts
Normal file
|
|
@ -0,0 +1,71 @@
|
|||
import LLMAgentPlugin from 'main';
|
||||
|
||||
export class VoicePanel {
|
||||
private activeTab: 'res1' | 'res2' | 'trans' = 'res1';
|
||||
|
||||
constructor(
|
||||
private container: HTMLElement,
|
||||
private plugin: LLMAgentPlugin
|
||||
) {
|
||||
this.render();
|
||||
this.startPolling();
|
||||
}
|
||||
|
||||
render() {
|
||||
this.container.innerHTML = `
|
||||
<div class="voice-panel-container">
|
||||
<div class="voice-header">
|
||||
<div class="v-controls">
|
||||
<button id="v-new" class="mod-cta">Start New</button>
|
||||
<button id="v-cont">Continue</button>
|
||||
<button id="v-stop" class="mod-warning">Stop</button>
|
||||
</div>
|
||||
<div class="v-tabs">
|
||||
<button class="v-tab active" data-tab="res1">Response 1</button>
|
||||
<button class="v-tab" data-tab="res2">Response 2</button>
|
||||
<button class="v-tab" data-tab="trans">Transcription</button>
|
||||
</div>
|
||||
</div>
|
||||
<div id="v-body" class="voice-body"></div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
// Навешиваем логику вкладок
|
||||
this.container.querySelectorAll('.v-tab').forEach((tab) => {
|
||||
tab.addEventListener('click', (e) => {
|
||||
this.container.querySelectorAll('.v-tab').forEach((t) => t.classList.remove('active'));
|
||||
(e.target as HTMLElement).classList.add('active');
|
||||
this.activeTab = (e.target as HTMLElement).dataset.tab as any;
|
||||
});
|
||||
});
|
||||
|
||||
// Навешиваем управление через API
|
||||
const control = async (action: string) => {
|
||||
await fetch(`${this.plugin.settings.apiBaseUrl}/voice/control`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ action })
|
||||
});
|
||||
};
|
||||
|
||||
this.container.querySelector('#v-new')?.addEventListener('click', () => control('start'));
|
||||
this.container.querySelector('#v-cont')?.addEventListener('click', () => control('continue'));
|
||||
this.container.querySelector('#v-stop')?.addEventListener('click', () => control('stop'));
|
||||
}
|
||||
|
||||
async startPolling() {
|
||||
setInterval(async () => {
|
||||
const res = await fetch(`${this.plugin.settings.apiBaseUrl}/voice/status`);
|
||||
const data = await res.json();
|
||||
this.updateContent(data);
|
||||
}, 3000);
|
||||
}
|
||||
|
||||
updateContent(data: any) {
|
||||
const body = this.container.querySelector('#v-body');
|
||||
if (!body) return;
|
||||
if (this.activeTab === 'trans') body.textContent = data.transcription;
|
||||
if (this.activeTab === 'res1') body.textContent = data.response1;
|
||||
if (this.activeTab === 'res2') body.innerHTML = data.response2.join('<br>---<br>');
|
||||
}
|
||||
}
|
||||
|
|
@ -740,9 +740,71 @@
|
|||
|
||||
.llm-agent-graph-main {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
/* background-color: var(--background-primary); */
|
||||
}
|
||||
|
||||
.upper-content-wrapper {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
/* Голосовая панель */
|
||||
.voice-panel-fixed-bottom {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
border-top: 1px solid var(--background-modifier-border);
|
||||
}
|
||||
|
||||
.voice-panel-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
padding: 5px;
|
||||
}
|
||||
|
||||
.voice-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding-bottom: 5px;
|
||||
border-bottom: 1px solid var(--background-modifier-border);
|
||||
}
|
||||
|
||||
.v-tabs {
|
||||
display: flex;
|
||||
gap: 2px;
|
||||
}
|
||||
|
||||
.v-tab {
|
||||
padding: 4px 8px;
|
||||
font-size: 11px;
|
||||
cursor: pointer;
|
||||
background: transparent;
|
||||
border: none;
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
.v-tab.active {
|
||||
color: var(--text-accent);
|
||||
border-bottom: 2px solid var(--text-accent);
|
||||
}
|
||||
|
||||
.voice-body {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
font-size: 12px;
|
||||
font-family: var(--font-monospace);
|
||||
padding: 10px;
|
||||
background-color: var(--background-primary);
|
||||
white-space: pre-wrap; /* Чтобы транскрибация не уходила в одну строку */
|
||||
}
|
||||
|
||||
/* Кнопки управления голосом */
|
||||
.v-controls button {
|
||||
margin-right: 5px;
|
||||
padding: 4px 10px;
|
||||
}
|
||||
|
||||
.llm-agent-history-sidebar {
|
||||
|
|
|
|||
|
|
@ -14,6 +14,7 @@ import { HistoryPanel } from '../components/HistoryPanel'; // Предполаг
|
|||
import { GraphPanelComponent } from '../components/GraphPanel'; // Импортируем React-компонент и ReactDOM
|
||||
import { Dialog } from 'src/utils/Dialog';
|
||||
import { GraphSettingsModal } from '../modals/GraphSettingsModal';
|
||||
import { VoicePanel } from '../components/VoicePanel';
|
||||
|
||||
export const GRAPH_VIEW_TYPE = 'llm-agent-graph-view';
|
||||
|
||||
|
|
@ -90,59 +91,80 @@ export class GraphView extends ItemView {
|
|||
const container = this.containerEl.children[1] as HTMLElement;
|
||||
container.empty();
|
||||
|
||||
// Создаем основной контейнер с flexbox layout
|
||||
// 1. Основной контейнер - теперь вертикальный стек
|
||||
const mainContainer = container.createDiv('llm-agent-graph-main');
|
||||
mainContainer.style.cssText = `
|
||||
display: flex;
|
||||
flex-direction: column; /* Элементы кладутся друг под друга */
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
background-color: var(--background-primary);
|
||||
overflow: hidden;
|
||||
`;
|
||||
|
||||
// Левая панель для истории
|
||||
this.historyContainer = mainContainer.createDiv('llm-agent-history-sidebar') as HTMLDivElement;
|
||||
// 2. Верхняя часть (История + Граф)
|
||||
const upperContent = mainContainer.createDiv('upper-content-wrapper');
|
||||
upperContent.style.cssText = `
|
||||
display: flex;
|
||||
flex-direction: row; /* Элементы бок о бок */
|
||||
flex: 1; /* Занимает всё свободное место (около 75-80%) */
|
||||
width: 100%;
|
||||
min-height: 0; /* Важно для корректного скролла внутри */
|
||||
`;
|
||||
|
||||
// 3. Левая панель для истории (внутри верхней части)
|
||||
this.historyContainer = upperContent.createDiv('llm-agent-history-sidebar');
|
||||
this.historyContainer.classList.add(this.isHistoryCollapsed ? 'collapsed' : 'expanded');
|
||||
|
||||
// Правая панель для графа
|
||||
this.graphContainer = mainContainer.createDiv('llm-agent-graph-container') as HTMLDivElement;
|
||||
// 4. Правая панель для графа (внутри верхней части)
|
||||
this.graphContainer = upperContent.createDiv('llm-agent-graph-container');
|
||||
this.graphContainer.style.cssText = `
|
||||
flex: 1;
|
||||
position: relative;
|
||||
background-color: var(--background-primary);
|
||||
`;
|
||||
|
||||
// Инициализируем панель истории
|
||||
// 5. Контейнер для голосовой панели (строго ВНИЗУ)
|
||||
const voiceContainer = mainContainer.createDiv('voice-panel-fixed-bottom');
|
||||
voiceContainer.style.cssText = `
|
||||
height: 200px; /* Фиксированная высота или % */
|
||||
width: 100%;
|
||||
border-top: 1px solid var(--divider-color);
|
||||
background-color: var(--background-secondary);
|
||||
overflow: hidden;
|
||||
flex-shrink: 0; /* Не дает панели сжиматься */
|
||||
`;
|
||||
|
||||
// Инициализация компонентов
|
||||
this.historyPanel = new HistoryPanel(
|
||||
this.historyContainer,
|
||||
{
|
||||
graphs: this.graphsList,
|
||||
eventBus: this.plugin.eventBus
|
||||
},
|
||||
{ graphs: this.graphsList, eventBus: this.plugin.eventBus },
|
||||
this.plugin
|
||||
);
|
||||
|
||||
// Создаем кнопки переключения и глобальные кнопки действий
|
||||
this.addToggleButton();
|
||||
this.addGlobalActionButtons(); // Добавляем вызов новой функции
|
||||
|
||||
// Инициализируем React Flow
|
||||
this.addGlobalActionButtons();
|
||||
this.initializeGraphPanel();
|
||||
|
||||
// Подписываемся на события
|
||||
// Инициализируем VoicePanel
|
||||
new VoicePanel(voiceContainer, this.plugin);
|
||||
|
||||
// Подписки на события
|
||||
this.setupEventListeners();
|
||||
|
||||
// Загружаем данные
|
||||
await this.fetchGraphsList();
|
||||
}
|
||||
|
||||
// Вынес для чистоты
|
||||
private setupEventListeners() {
|
||||
this.plugin.eventBus.on('new-chat', this.handleNewChat.bind(this));
|
||||
this.plugin.eventBus.on('graph-selected', this.handleGraphSelected.bind(this));
|
||||
this.plugin.eventBus.on('delete-node', this.handleDeleteNodeFromChat.bind(this)); // Слушаем событие из ChatPanel
|
||||
|
||||
// WebSocket Event Handlers
|
||||
this.plugin.eventBus.on('delete-node', this.handleDeleteNodeFromChat.bind(this));
|
||||
this.plugin.eventBus.on('ws-graph-title-updated', this.handleGraphTitleUpdated.bind(this));
|
||||
this.plugin.eventBus.on('ws-node-title-updated', this.handleNodeTitleUpdated.bind(this));
|
||||
this.plugin.eventBus.on('ws-node-type-changed', this.handleNodeTypeChanged.bind(this));
|
||||
|
||||
this.plugin.eventBus.on('stream-started', this.handleStreamStarted.bind(this));
|
||||
this.plugin.eventBus.on('stream-ended', this.handleStreamEnded.bind(this));
|
||||
|
||||
// Загружаем список графов
|
||||
await this.fetchGraphsList();
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user