From 7b8657a56de30d303995d615c842a6b6723d6ea0 Mon Sep 17 00:00:00 2001 From: dimitrievgs Date: Fri, 12 Sep 2025 00:39:44 +0300 Subject: [PATCH] Initial commit for llm-agent-plugin --- Readme.md | 271 +++++++++++++++++++++++++++++++++ main.js | 105 +++++++++++++ manifest.json | 10 ++ src/components/ChatPanel.js | 135 ++++++++++++++++ src/components/GraphPanel.js | 128 ++++++++++++++++ src/components/HistoryPanel.js | 131 ++++++++++++++++ src/utils/EventBus.js | 28 ++++ src/views/ChatView.js | 209 +++++++++++++++++++++++++ src/views/GraphView.js | 79 ++++++++++ styles.css | 61 ++++++++ 10 files changed, 1157 insertions(+) create mode 100644 Readme.md create mode 100644 main.js create mode 100644 manifest.json create mode 100644 src/components/ChatPanel.js create mode 100644 src/components/GraphPanel.js create mode 100644 src/components/HistoryPanel.js create mode 100644 src/utils/EventBus.js create mode 100644 src/views/ChatView.js create mode 100644 src/views/GraphView.js create mode 100644 styles.css diff --git a/Readme.md b/Readme.md new file mode 100644 index 0000000..91e3cba --- /dev/null +++ b/Readme.md @@ -0,0 +1,271 @@ +## Запрос + +Хочу перенести всё это в Obsidian, сделав его плагином (для личного пользования). ПРи этом в средней колонке, где граф отображаетс яштатный обсидиана или замтеки - тм будет react-flow граф с историей взаимдействия (деревом диалогов), где можно быстро будет попросить сгенерировать картинки и одновременно сразу нагенерить в разных ветках , например, локации и персонажей нескольких для НРИ (однорвременно запустить друг за другом запросы в разных ветках и пусть обрабатываются) впараллельно, я на графе всё увижу). А сам чат чтобы вёлся справа в колонк, гнде теги , бэклинки, проперти или Copilot помощник из известного плагина. сначала скажи можно ли перенести и чтобы бы ты посоветовал (пока без кода), может , предлоишь что-то улучшить? Да, граф в пространстве, где новые заметки открываются, он просто открывается там, в новом табе, где новые заметикти открываются ли графы обсидианы. Сделай перенос. Пусть бэкенд, как есть, а фронт перенеси полностью + +# LLM Agent для Obsidian + +Плагин Obsidian для работы с LLM агентом, включающий граф диалогов и интерфейс чата с поддержкой слеш-команд. + +## Структура проекта + +``` +ttrpg-obsidian-vault/ +├── .obsidian/ +│ └── plugins/ +│ └── llm-agent-plugin/ +│ ├── main.js +│ ├── manifest.json +│ ├── styles.css +│ └── src/ +│ ├── views/ +│ │ ├── GraphView.js +│ │ └── ChatView.js +│ ├── components/ +│ │ ├── ChatPanel.js +│ │ ├── CommandInput.js +│ │ ├── GraphPanel.js +│ │ └── HistoryPanel.js +│ └── utils/ +│ └── EventBus.js +├── llm-agent-backend/ # Backend на Python +│ ├── env/ # Виртуальное окружение Python +│ ├── app/ # Код приложения +│ │ ├── api.py +│ │ ├── workflows.py +│ │ └── ... +│ ├── requirements.txt +│ └── run.py # Точка входа +└── env/ +``` + +## Установка и запуск + +### 1. Настройка Backend + +1. Перейдите в папку backend: + ```bash + cd llm-agent-backend + ``` + +2. Активируйте виртуальное окружение: + ```bash + # Windows + env\Scripts\activate + + # Linux/macOS + source env/bin/activate + ``` + +3. Установите зависимости: + ```bash + pip install -r requirements.txt + ``` + +4. Запустите backend сервер: + ```bash + python run.py + ``` + + Backend будет доступен на `http://localhost:5000` + +### 2. Установка плагина в Obsidian + +1. **Автоматическая установка (если плагин уже находится в правильной папке):** + - Откройте Obsidian с этим хранилищем + - Перейдите в Настройки → Плагины сообщества + - Включите "Небезопасные плагины" (если требуется) + - Найдите "LLM Agent" в списке установленных плагинов + - Включите плагин + +2. **Ручная установка (если нужно скопировать файлы):** + ```bash + # Убедитесь, что файлы плагина находятся в: + .obsidian/plugins/llm-agent-plugin/ + ├── main.js + ├── manifest.json + ├── styles.css + └── src/ + ``` + +### 3. Использование + +1. **Запуск интерфейса:** + - Нажмите иконку 🧠 в левой панели Obsidian + - Или используйте команду "Открыть граф диалогов LLM Agent" (Ctrl+P) + +2. **Доступные слеш-команды:** + - `/help` - Список доступных команд + - `/imagine [промпт]` - Генерация изображений + - `/analyze [url] [промпт]` - Анализ изображения + - `/subtitles_meet` - Получение субтитров Google Meet + - `/subtitles_teams` - Получение субтитров MS Teams + - `/summarize` - Суммаризация истории диалога + - `/chat` - Обычный диалог с LLM + +3. **Работа с графом:** + - Граф отображается в центральной области Obsidian + - Чат и история - в правой боковой панели + - Кликайте по узлам графа для навигации по веткам диалога + - Используйте "Новый чат" для создания нового диалога + +## Конфигурация + +### Backend API URL + +По умолчанию плагин использует `http://localhost:5000/api`. Для изменения: + +1. Откройте файл `.obsidian/plugins/llm-agent-plugin/main.js` +2. Найдите строку: + ```javascript + const DEFAULT_SETTINGS = { + apiBaseUrl: 'http://localhost:5000/api' + }; + ``` +3. Измените URL на нужный + +### LLM модели + +Конфигурация моделей находится в `backend/llm-agent-backend/app/llm_client.py`: + +```python +MODELS = { + "gemini-2.5-flash": { + "apiBase": "https://your-api-endpoint.com", + "apiKey": "your-api-key", + # ... + } +} +``` + +## Устранение неполадок + +### Backend не запускается + +1. Проверьте, что виртуальное окружение активировано +2. Убедитесь, что все зависимости установлены: + ```bash + pip install flask flask-cors langgraph langchain-core requests + ``` +3. Проверьте порт 5000 - он должен быть свободен + +### Плагин не появляется в Obsidian + +1. Убедитесь, что файлы находятся в правильной папке: + ``` + .obsidian/plugins/llm-agent-plugin/main.js + ``` +2. Проверьте, что включены "Плагины сообщества" в настройках +3. Перезапустите Obsidian + +### Ошибки подключения к API + +1. Убедитесь, что backend запущен на порту 5000 +2. Проверьте консоль разработчика в Obsidian (Ctrl+Shift+I) +3. Проверьте настройки CORS в `backend/llm-agent-backend/app/api.py` + +### Граф не отображается + +1. Текущая версия использует упрощенную визуализацию +2. Для полноценного React Flow потребуется дополнительная настройка bundler'а + +## Разработка + +### Структура кода плагина + +- `main.js` - Основной класс плагина +- `src/views/` - Views для Obsidian (GraphView, ChatView) +- `src/components/` - React-like компоненты +- `src/utils/` - Утилиты (EventBus) + +### Отладка + +1. Откройте консоль разработчика в Obsidian (Ctrl+Shift+I) +2. Логи плагина доступны в консоли +3. Backend логи выводятся в терминал где запущен сервер + +### Добавление новых команд + +1. Добавьте команду в `backend/llm-agent-backend/app/nodes.py` в `CommandManager` +2. Создайте соответствующий узел обработки +3. Обновите маршрутизацию в `workflows.py` + +## API Documentation + +### Основные эндпоинты + +- `POST /api/chat` - Отправка сообщения +- `GET /api/graphs` - Список всех графов +- `GET /api/graphs/{id}` - Данные конкретного графа +- `DELETE /api/graphs/{id}` - Удаление графа +- `GET /api/messages/{graph_id}/{node_id}` - Сообщения от корня до узла + +## Лицензия + +Для личного использования. +``` + +**`backend/llm-agent-backend/requirements.txt`:** +```txt +flask==2.3.3 +flask-cors==4.0.0 +langgraph==0.0.40 +langchain-core==0.1.45 +requests==2.31.0 +sqlite3 +openai==1.3.0 +``` + +**`backend/llm-agent-backend/run.py`:** +```python +""" +Точка входа для запуска LLM Agent backend сервера. +Запускает Flask приложение с настройками для разработки. +""" + +import os +import sys + +# Добавляем папку app в Python path +sys.path.insert(0, os.path.join(os.path.dirname(__file__), 'app')) + +from api import app2 + +if __name__ == "__main__": + print("🚀 Запуск LLM Agent Backend сервера...") + print("📍 API будет доступно на: http://localhost:5000/api") + print("📊 Список графов: http://localhost:5000/api/graphs") + print("💬 Чат эндпоинт: http://localhost:5000/api/chat") + print("\n⚠️ Для остановки нажмите Ctrl+C\n") + + app2.run( + debug=True, + port=5000, + host='localhost' + ) +``` + +**`backend/llm-agent-backend/app/__init__.py`:** +```python +# Пустой файл для создания Python пакета +``` + +## Быстрый старт: + +1. **Запуск backend:** + ```bash + cd backend/llm-agent-backend + env\Scripts\activate # Windows + python run.py + ``` + +2. **Запуск Obsidian:** + - Откройте хранилище в Obsidian + - Включите плагин "LLM Agent" в настройках + - Нажмите иконку 🧠 в левой панели + +3. **Проверка работы:** + - В правой панели должна появиться история диалогов + - В центре - область для графа + - Отправьте сообщение `/help` для проверки + +**Важно:** Backend должен быть запущен перед использованием плагина. При первом запуске может потребоваться установка дополнительных зависимостей. \ No newline at end of file diff --git a/main.js b/main.js new file mode 100644 index 0000000..52d1d9f --- /dev/null +++ b/main.js @@ -0,0 +1,105 @@ +const { Plugin, ItemView, WorkspaceLeaf } = require('obsidian'); + +const { GraphView, GRAPH_VIEW_TYPE } = require('/src/views/GraphView.js'); +const { ChatView, CHAT_VIEW_TYPE } = require('./src/views/ChatView.js'); + +const { EventBus } = require('./src/utils/EventBus.js'); + +// ----------------------------------------------- Settings --------------------------------------------------------------- + +const DEFAULT_SETTINGS = { + apiBaseUrl: 'http://localhost:5000/api' +}; + +// ----------------------------------------------- Main Plugin Class --------------------------------------------------------------- + +class LLMAgentPlugin extends Plugin { + constructor() { + super(); + this.eventBus = new EventBus(); + this.settings = DEFAULT_SETTINGS; + } + + async onload() { + await this.loadSettings(); + + // Регистрируем кастомные 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 загружен'); + } + + onunload() { + 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); + } + + // ----------------------------------------------- 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 { + await this.app.workspace.getRightLeaf(false).setViewState({ + type: CHAT_VIEW_TYPE, + active: true, + }); + } + } + + // ----------------------------------------------- Event Handlers --------------------------------------------------------------- + + handleNewChat() { + this.eventBus.emit('new-chat'); + } +} + +module.exports = LLMAgentPlugin; \ No newline at end of file diff --git a/manifest.json b/manifest.json new file mode 100644 index 0000000..f831909 --- /dev/null +++ b/manifest.json @@ -0,0 +1,10 @@ +{ + "id": "llm-agent-plugin", + "name": "LLM Agent", + "version": "1.0.0", + "minAppVersion": "0.15.0", + "description": "LLM Agent with graph visualization and chat interface", + "author": "Your Name", + "authorUrl": "", + "isDesktopOnly": false +} \ No newline at end of file diff --git a/src/components/ChatPanel.js b/src/components/ChatPanel.js new file mode 100644 index 0000000..8a25be5 --- /dev/null +++ b/src/components/ChatPanel.js @@ -0,0 +1,135 @@ +class ChatPanel { + constructor(container, props) { + this.container = container; + this.props = props; + this.chatHistory = props.chatHistory || []; + + this.init(); + } + + init() { + this.container.innerHTML = ` +
+
+
+
+ + +
+ +
+
+ `; + + this.messageInput = this.container.querySelector('#message-input'); + this.sendButton = this.container.querySelector('#send-button'); + this.suggestionsContainer = this.container.querySelector('#suggestions'); + + this.setupEventListeners(); + this.renderHistory(); + } + + setupEventListeners() { + this.sendButton.addEventListener('click', () => this.handleSend()); + + this.messageInput.addEventListener('keydown', (e) => { + if (e.key === 'Enter') { + if (this.suggestionsContainer.style.display === 'block') { + const firstSuggestion = this.suggestionsContainer.querySelector('.suggestion-item'); + if (firstSuggestion) { + this.messageInput.value = firstSuggestion.textContent; + this.hideSuggestions(); + e.preventDefault(); + return; + } + } + this.handleSend(); + e.preventDefault(); + } + }); + + this.messageInput.addEventListener('input', (e) => { + this.handleInput(e.target.value); + }); + } + + handleInput(value) { + const commands = ['/help', '/imagine', '/analyze', '/subtitles_meet', '/subtitles_teams', '/summarize', '/chat']; + + if (value.startsWith('/')) { + const filteredCommands = commands.filter(cmd => + cmd.startsWith(value.toLowerCase()) + ); + this.showSuggestions(filteredCommands); + } else { + this.hideSuggestions(); + } + } + + showSuggestions(suggestions) { + if (suggestions.length === 0) { + this.hideSuggestions(); + return; + } + + this.suggestionsContainer.innerHTML = suggestions + .map(suggestion => `
${suggestion}
`) + .join(''); + + this.suggestionsContainer.style.display = 'block'; + + // Добавляем обработчики кликов на предложения + this.suggestionsContainer.querySelectorAll('.suggestion-item').forEach(item => { + item.addEventListener('click', () => { + this.messageInput.value = item.textContent; + this.hideSuggestions(); + this.messageInput.focus(); + }); + }); + } + + hideSuggestions() { + this.suggestionsContainer.style.display = 'none'; + } + + handleSend() { + const message = this.messageInput.value.trim(); + if (message && this.props.onSendMessage) { + this.props.onSendMessage(message); + this.messageInput.value = ''; + this.hideSuggestions(); + } + } + + renderHistory() { + const historyContainer = this.container.querySelector('#chat-history'); + historyContainer.innerHTML = this.chatHistory + .map(msg => ` +
+
${this.processMessageContent(msg.content)}
+
+ `) + .join(''); + + // Прокручиваем к концу + historyContainer.scrollTop = historyContainer.scrollHeight; + } + + processMessageContent(content) { + // Простейшая обработка Markdown для изображений + return content + .replace(/]*?)>/g, '') + .replace(/\n/g, '
'); + } + + updateHistory(newHistory) { + this.chatHistory = newHistory; + this.renderHistory(); + } + + destroy() { + this.container.innerHTML = ''; + } +} + +module.exports = { ChatPanel }; \ No newline at end of file diff --git a/src/components/GraphPanel.js b/src/components/GraphPanel.js new file mode 100644 index 0000000..aaf3c21 --- /dev/null +++ b/src/components/GraphPanel.js @@ -0,0 +1,128 @@ +// Адаптация оригинального GraphPanel для работы в Obsidian + +class GraphPanel { + constructor(container, props) { + this.container = container; + this.props = props; + this.nodes = []; + this.edges = []; + + this.init(); + } + + init() { + // Создаем структуру для React Flow + this.container.innerHTML = ` +
+
+
+ `; + + // Загружаем React Flow динамически (если доступен) + this.initReactFlow(); + } + + async initReactFlow() { + try { + // В среде Obsidian нужно будет подключить React Flow через CDN или bundler + // Для демонстрации создаем упрощенную визуализацию + this.createSimpleGraph(); + } catch (error) { + console.error('Ошибка инициализации React Flow:', error); + this.createFallbackGraph(); + } + } + + createSimpleGraph() { + const container = this.container.querySelector('#reactflow-container'); + container.innerHTML = ` +
+
+ +
+ `; + + this.renderNodes(); + this.renderEdges(); + } + + renderNodes() { + const nodesContainer = this.container.querySelector('#graph-nodes'); + nodesContainer.innerHTML = ''; + + this.nodes.forEach((node, index) => { + const nodeEl = document.createElement('div'); + nodeEl.className = `graph-node node-${node.type || 'default'}`; + nodeEl.style.cssText = ` + position: absolute; + left: ${50 + index * 150}px; + top: ${50 + Math.floor(index / 5) * 80}px; + padding: 8px 12px; + border: 2px solid #333; + border-radius: 4px; + background: #fff; + cursor: pointer; + max-width: 120px; + word-wrap: break-word; + `; + nodeEl.textContent = node.data?.label || `Node ${node.id}`; + nodeEl.dataset.nodeId = node.id; + + nodeEl.addEventListener('click', () => { + this.handleNodeClick(node.id); + }); + + nodesContainer.appendChild(nodeEl); + }); + } + + renderEdges() { + const edgesContainer = this.container.querySelector('#graph-edges'); + edgesContainer.innerHTML = ''; + // Упрощенная отрисовка связей - в реальной реализации нужно будет вычислять позиции + } + + createFallbackGraph() { + const container = this.container.querySelector('#reactflow-container'); + container.innerHTML = ` +
+

Упрощенное представление графа

+
+
+ `; + this.renderFallbackNodes(); + } + + renderFallbackNodes() { + const nodesList = this.container.querySelector('#nodes-list'); + nodesList.innerHTML = ''; + + this.nodes.forEach(node => { + const nodeEl = document.createElement('div'); + nodeEl.className = 'fallback-node'; + nodeEl.textContent = `${node.id}: ${node.data?.label || 'No label'}`; + nodeEl.addEventListener('click', () => this.handleNodeClick(node.id)); + nodesList.appendChild(nodeEl); + }); + } + + updateData(graphData) { + this.nodes = graphData.nodes || []; + this.edges = graphData.edges || []; + this.renderNodes(); + this.renderEdges(); + } + + handleNodeClick(nodeId) { + if (this.props.onNodeClick) { + this.props.onNodeClick(nodeId); + } + } + + destroy() { + // Очистка ресурсов + this.container.innerHTML = ''; + } +} + +module.exports = { GraphPanel }; \ No newline at end of file diff --git a/src/components/HistoryPanel.js b/src/components/HistoryPanel.js new file mode 100644 index 0000000..7278f67 --- /dev/null +++ b/src/components/HistoryPanel.js @@ -0,0 +1,131 @@ +class HistoryPanel { + constructor(container, props) { + this.container = container; + this.props = props; + this.graphs = props.graphs || []; + + this.init(); + } + + init() { + this.container.innerHTML = ` +
+
+

История диалогов

+ +
+
+ +
+ `; + + this.newChatButton = this.container.querySelector('#new-chat-button'); + this.chatList = this.container.querySelector('#chat-list'); + this.contextMenu = this.container.querySelector('#context-menu'); + this.deleteButton = this.container.querySelector('#delete-graph-button'); + + this.setupEventListeners(); + this.renderGraphs(); + } + + setupEventListeners() { + this.newChatButton.addEventListener('click', () => { + if (this.props.onNewChat) { + this.props.onNewChat(); + } + }); + + this.deleteButton.addEventListener('click', () => { + if (this.contextMenuGraphId) { + this.handleDeleteGraph(this.contextMenuGraphId); + } + }); + + // Закрытие контекстного меню при клике вне его + document.addEventListener('click', (e) => { + if (!this.contextMenu.contains(e.target)) { + this.contextMenu.style.display = 'none'; + } + }); + } + + renderGraphs() { + this.chatList.innerHTML = this.graphs + .map(graph => ` +
+
+
+ + ${graph.first_message || `Граф ${graph.id}`} + +
+
+ +
+
+
+ `) + .join(''); + + // Добавляем обработчики событий + this.chatList.querySelectorAll('.chat-list-item').forEach(item => { + item.addEventListener('click', (e) => { + if (!e.target.classList.contains('chat-item-options-button')) { + const graphId = item.dataset.graphId; + if (this.props.onGraphSelect) { + this.props.onGraphSelect(graphId); + } + } + }); + }); + + this.chatList.querySelectorAll('.chat-item-options-button').forEach(button => { + button.addEventListener('click', (e) => { + e.stopPropagation(); + this.showContextMenu(e, button.dataset.graphId); + }); + }); + } + + showContextMenu(event, graphId) { + this.contextMenuGraphId = graphId; + this.contextMenu.style.display = 'block'; + this.contextMenu.style.left = `${event.clientX}px`; + this.contextMenu.style.top = `${event.clientY}px`; + } + + async handleDeleteGraph(graphId) { + try { + const response = await fetch(`http://localhost:5000/api/graphs/${graphId}`, { + method: 'DELETE', + }); + + if (!response.ok) { + throw new Error(`HTTP ошибка! статус: ${response.status}`); + } + + // Обновляем список графов + this.graphs = this.graphs.filter(graph => graph.id !== graphId); + this.renderGraphs(); + this.contextMenu.style.display = 'none'; + + } catch (error) { + console.error('Ошибка удаления графа:', error); + } + } + + updateGraphs(newGraphs) { + this.graphs = newGraphs; + this.renderGraphs(); + } + + destroy() { + this.container.innerHTML = ''; + } +} + +module.exports = { HistoryPanel }; \ No newline at end of file diff --git a/src/utils/EventBus.js b/src/utils/EventBus.js new file mode 100644 index 0000000..d2d3e49 --- /dev/null +++ b/src/utils/EventBus.js @@ -0,0 +1,28 @@ +// ----------------------------------------------- Event Bus --------------------------------------------------------------- + +class EventBus { + constructor() { + this.events = {}; + } + + on(eventName, callback) { + if (!this.events[eventName]) { + this.events[eventName] = []; + } + this.events[eventName].push(callback); + } + + off(eventName, callback) { + if (this.events[eventName]) { + this.events[eventName] = this.events[eventName].filter(cb => cb !== callback); + } + } + + emit(eventName, data) { + if (this.events[eventName]) { + this.events[eventName].forEach(callback => callback(data)); + } + } +} + +module.exports = { EventBus }; \ No newline at end of file diff --git a/src/views/ChatView.js b/src/views/ChatView.js new file mode 100644 index 0000000..979199a --- /dev/null +++ b/src/views/ChatView.js @@ -0,0 +1,209 @@ +const { ItemView } = require('obsidian'); +const { ChatPanel } = require('../components/ChatPanel'); +const { HistoryPanel } = require('../components/HistoryPanel'); + +const CHAT_VIEW_TYPE = 'llm-agent-chat-view'; + +// ----------------------------------------------- Chat View --------------------------------------------------------------- + +class ChatView extends ItemView { + constructor(leaf, plugin) { + super(leaf); + this.plugin = plugin; + this.chatHistory = []; + this.graphsList = []; + this.selectedGraphId = null; + this.currentNode = null; + } + + getViewType() { + return CHAT_VIEW_TYPE; + } + + getDisplayText() { + return 'LLM Agent Chat'; + } + + getIcon() { + return 'message-circle'; + } + + async onOpen() { + const container = this.containerEl.children[1]; + container.empty(); + + // Создаем контейнеры для React компонентов + this.chatContainer = container.createDiv(); + this.chatContainer.addClass('llm-agent-chat-container'); + + this.historyContainer = container.createDiv(); + this.historyContainer.addClass('llm-agent-history-container'); + + // Инициализируем React компоненты + this.chatPanel = new ChatPanel(this.chatContainer, { + chatHistory: this.chatHistory, + onSendMessage: this.handleSendMessage.bind(this) + }); + + this.historyPanel = new HistoryPanel(this.historyContainer, { + graphs: this.graphsList, + onGraphSelect: this.handleGraphSelect.bind(this), + onNewChat: this.handleNewChat.bind(this) + }); + + // Подписываемся на события + this.plugin.eventBus.on('node-selected', this.handleNodeSelected.bind(this)); + this.plugin.eventBus.on('new-chat', this.handleNewChat.bind(this)); + + // Загружаем список графов + this.fetchGraphsList(); + } + + async onClose() { + if (this.chatPanel) { + this.chatPanel.destroy(); + } + if (this.historyPanel) { + this.historyPanel.destroy(); + } + this.plugin.eventBus.off('node-selected', this.handleNodeSelected); + this.plugin.eventBus.off('new-chat', this.handleNewChat); + } + + // ----------------------------------------------- API Methods --------------------------------------------------------------- + + async fetchGraphsList() { + try { + const response = await fetch(`${this.plugin.settings.apiBaseUrl}/graphs`); + const data = await response.json(); + this.graphsList = data; + if (this.historyPanel) { + this.historyPanel.updateGraphs(this.graphsList); + } + } catch (error) { + console.error('Ошибка загрузки списка графов:', error); + } + } + + async fetchGraphData(graphId) { + try { + const response = await fetch(`${this.plugin.settings.apiBaseUrl}/graphs/${graphId}`); + if (!response.ok) { + if (response.status === 404) { + console.error(`Граф с ID ${graphId} не найден.`); + } + throw new Error(`HTTP ошибка! статус: ${response.status}`); + } + const data = await response.json(); + + // Обновляем граф + this.plugin.eventBus.emit('graph-updated', { + graphData: { + nodes: data.graph_nodes || [], + edges: data.graph_edges || [] + }, + currentNode: data.current_node_id + }); + + // Обновляем историю чата + this.chatHistory = data.messages || []; + if (this.chatPanel) { + this.chatPanel.updateHistory(this.chatHistory); + } + } catch (error) { + console.error('Ошибка загрузки данных графа:', error); + } + } + + async fetchMessagesFromRootToNode(nodeId) { + try { + const response = await fetch(`${this.plugin.settings.apiBaseUrl}/messages/${this.selectedGraphId}/${nodeId}`); + const data = await response.json(); + this.chatHistory = data; + if (this.chatPanel) { + this.chatPanel.updateHistory(this.chatHistory); + } + } catch (error) { + console.error('Ошибка загрузки сообщений:', error); + } + } + + // ----------------------------------------------- Event Handlers --------------------------------------------------------------- + + async handleSendMessage(message) { + try { + const response = await fetch(`${this.plugin.settings.apiBaseUrl}/chat`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + message: message, + graph_id: this.selectedGraphId, + parent_node_id: this.currentNode, + }), + }); + + if (!response.ok) { + throw new Error(`HTTP ошибка! статус: ${response.status}`); + } + + const result = await response.json(); + + // Обновляем выбранный ID графа + this.selectedGraphId = result.graph_id; + + // Устанавливаем текущий узел + const newCurrentNodeId = result.graph_visualization_data.current_node_id; + this.currentNode = newCurrentNodeId; + + // Обновляем граф + this.plugin.eventBus.emit('graph-updated', { + graphData: { + nodes: result.graph_visualization_data.nodes || [], + edges: result.graph_visualization_data.edges || [] + }, + currentNode: newCurrentNodeId + }); + + // Загружаем сообщения от корня до нового текущего узла + if (newCurrentNodeId) { + await this.fetchMessagesFromRootToNode(newCurrentNodeId); + } else { + this.chatHistory = result.messages || []; + if (this.chatPanel) { + this.chatPanel.updateHistory(this.chatHistory); + } + } + + // Обновляем список графов + this.fetchGraphsList(); + + } catch (error) { + console.error('Ошибка отправки сообщения:', error); + } + } + + handleNodeSelected(data) { + this.currentNode = data.nodeId; + if (this.selectedGraphId) { + this.fetchMessagesFromRootToNode(data.nodeId); + } + } + + handleGraphSelect(graphId) { + this.selectedGraphId = graphId; + this.fetchGraphData(graphId); + } + + handleNewChat() { + this.chatHistory = []; + this.currentNode = null; + this.selectedGraphId = null; + if (this.chatPanel) { + this.chatPanel.updateHistory(this.chatHistory); + } + } +} + +module.exports = { ChatView, CHAT_VIEW_TYPE }; \ No newline at end of file diff --git a/src/views/GraphView.js b/src/views/GraphView.js new file mode 100644 index 0000000..a177e45 --- /dev/null +++ b/src/views/GraphView.js @@ -0,0 +1,79 @@ +const { ItemView } = require('obsidian'); +const { GraphPanel } = require('../components/GraphPanel'); + +const GRAPH_VIEW_TYPE = 'llm-agent-graph-view'; + +// ----------------------------------------------- Graph View --------------------------------------------------------------- + +class GraphView extends ItemView { + constructor(leaf, plugin) { + super(leaf); + this.plugin = plugin; + this.graphData = { nodes: [], edges: [] }; + this.currentNode = null; + } + + getViewType() { + return GRAPH_VIEW_TYPE; + } + + getDisplayText() { + return 'LLM Agent Graph'; + } + + getIcon() { + return 'brain-circuit'; + } + + async onOpen() { + const container = this.containerEl.children[1]; + container.empty(); + + // Создаем контейнер для React компонента + this.reactContainer = container.createDiv(); + this.reactContainer.addClass('llm-agent-graph-container'); + + // Инициализируем React компонент + this.graphPanel = new GraphPanel(this.reactContainer, { + graphData: this.graphData, + onNodeClick: this.handleNodeClick.bind(this) + }); + + // Подписываемся на события + this.plugin.eventBus.on('graph-updated', this.handleGraphUpdate.bind(this)); + this.plugin.eventBus.on('new-chat', this.handleNewChat.bind(this)); + } + + async onClose() { + if (this.graphPanel) { + this.graphPanel.destroy(); + } + this.plugin.eventBus.off('graph-updated', this.handleGraphUpdate); + this.plugin.eventBus.off('new-chat', this.handleNewChat); + } + + // ----------------------------------------------- Event Handlers --------------------------------------------------------------- + + handleNodeClick(nodeId) { + this.currentNode = nodeId; + this.plugin.eventBus.emit('node-selected', { nodeId }); + } + + handleGraphUpdate(data) { + this.graphData = data.graphData; + this.currentNode = data.currentNode; + if (this.graphPanel) { + this.graphPanel.updateData(this.graphData); + } + } + + handleNewChat() { + this.graphData = { nodes: [], edges: [] }; + this.currentNode = null; + if (this.graphPanel) { + this.graphPanel.updateData(this.graphData); + } + } +} + +module.exports = { GraphView, GRAPH_VIEW_TYPE }; \ No newline at end of file diff --git a/styles.css b/styles.css new file mode 100644 index 0000000..3782e56 --- /dev/null +++ b/styles.css @@ -0,0 +1,61 @@ +/* ----------------------------------------------- General Styles --------------------------------------------------------------- */ + +.llm-agent-graph-container, +.llm-agent-chat-container, +.llm-agent-history-container { + height: 100%; + width: 100%; + display: flex; + flex-direction: column; +} + +/* ----------------------------------------------- Graph Panel Styles --------------------------------------------------------------- */ + +.llm-agent-graph-panel { + height: 100%; + width: 100%; +} + +.simple-graph { + position: relative; + height: 100%; + width: 100%; + overflow: auto; +} + +.graph-nodes { + position: relative; + height: 100%; + width: 100%; +} + +.graph-node { + font-size: 12px; + border: 2px solid var(--background-modifier-border); + background: var(--background-primary); + color: var(--text-normal); + transition: all 0.2s; +} + +.graph-node:hover { + border-color: var(--interactive-accent); + transform: scale(1.05); +} + +.graph-node.node-user { + border-color: var(--color-blue); + background: var(--color-blue-rgb); +} + +.graph-node.node-llm { + border-color: var(--color-green); + background: var(--color-green-rgb); +} + +.graph-node.node-tool { + border-color: var(--color-orange); + background: var(--color-orange-rgb); +} + +.graph-node.node-error { + border \ No newline at end of file