Initial commit for llm-agent-plugin

This commit is contained in:
dimitrievgs 2025-09-12 00:39:44 +03:00
commit 7b8657a56d
10 changed files with 1157 additions and 0 deletions

271
Readme.md Normal file
View File

@ -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 должен быть запущен перед использованием плагина. При первом запуске может потребоваться установка дополнительных зависимостей.

105
main.js Normal file
View File

@ -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;

10
manifest.json Normal file
View File

@ -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
}

135
src/components/ChatPanel.js Normal file
View File

@ -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 = `
<div class="llm-agent-chat-panel">
<div class="chat-history" id="chat-history"></div>
<div class="chat-input-form">
<div class="command-input">
<input type="text" id="message-input" placeholder="Введите сообщение или команду (/)..." />
<div class="suggestions" id="suggestions" style="display: none;"></div>
</div>
<button type="button" id="send-button">Отправить</button>
</div>
</div>
`;
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 => `<div class="suggestion-item">${suggestion}</div>`)
.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 => `
<div class="message ${msg.role}">
<div class="message-content">${this.processMessageContent(msg.content)}</div>
</div>
`)
.join('');
// Прокручиваем к концу
historyContainer.scrollTop = historyContainer.scrollHeight;
}
processMessageContent(content) {
// Простейшая обработка Markdown для изображений
return content
.replace(/<img([^>]*?)>/g, '<img$1 style="max-width: 100%; height: auto;">')
.replace(/\n/g, '<br>');
}
updateHistory(newHistory) {
this.chatHistory = newHistory;
this.renderHistory();
}
destroy() {
this.container.innerHTML = '';
}
}
module.exports = { ChatPanel };

View File

@ -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 = `
<div class="llm-agent-graph-panel">
<div id="reactflow-container" style="width: 100%; height: 100%;"></div>
</div>
`;
// Загружаем 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 = `
<div class="simple-graph">
<div class="graph-nodes" id="graph-nodes"></div>
<svg class="graph-edges" id="graph-edges"></svg>
</div>
`;
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 = `
<div class="fallback-graph">
<p>Упрощенное представление графа</p>
<div id="nodes-list"></div>
</div>
`;
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 };

View File

@ -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 = `
<div class="llm-agent-history-panel">
<div class="history-header">
<h3>История диалогов</h3>
<button id="new-chat-button" class="new-chat-button">+ Новый чат</button>
</div>
<div class="chat-list" id="chat-list"></div>
<div class="context-menu" id="context-menu" style="display: none;">
<button id="delete-graph-button">Удалить</button>
</div>
</div>
`;
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 => `
<div class="chat-list-item" data-graph-id="${graph.id}">
<div class="chat-item-content">
<div class="chat-item-text">
<span class="truncate">
${graph.first_message || `Граф ${graph.id}`}
</span>
</div>
<div class="chat-item-actions">
<button class="chat-item-options-button" data-graph-id="${graph.id}">
</button>
</div>
</div>
</div>
`)
.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 };

28
src/utils/EventBus.js Normal file
View File

@ -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 };

209
src/views/ChatView.js Normal file
View File

@ -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 };

79
src/views/GraphView.js Normal file
View File

@ -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 };

61
styles.css Normal file
View File

@ -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