Initial commit for llm-agent-plugin
This commit is contained in:
commit
7b8657a56d
271
Readme.md
Normal file
271
Readme.md
Normal 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
105
main.js
Normal 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
10
manifest.json
Normal 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
135
src/components/ChatPanel.js
Normal 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 };
|
||||
128
src/components/GraphPanel.js
Normal file
128
src/components/GraphPanel.js
Normal 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 };
|
||||
131
src/components/HistoryPanel.js
Normal file
131
src/components/HistoryPanel.js
Normal 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
28
src/utils/EventBus.js
Normal 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
209
src/views/ChatView.js
Normal 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
79
src/views/GraphView.js
Normal 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
61
styles.css
Normal 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
|
||||
Loading…
Reference in New Issue
Block a user