llm-agent-plugin/src/views/GraphView.ts
2025-09-25 01:39:05 +03:00

313 lines
12 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/**
* Отображает и управляет интерактивным графом LLM агента.
* Интегрирует историю чатов и визуализацию графа на основе React Flow.
*/
// ----------------------------------------------- Obsidian Imports ----------------------------------------------------------
import { ItemView, WorkspaceLeaf } from 'obsidian';
// ----------------------------------------------- Plugin Imports ------------------------------------------------------------
import LLMAgentPlugin from 'main'; // Предполагаем, что main.ts экспортирует LLMAgentPlugin по умолчанию
import { HistoryPanel } from '../components/HistoryPanel'; // Предполагаем, что HistoryPanel это JS-класс
import { GraphPanelComponent } from '../components/GraphPanel'; // Импортируем React-компонент и ReactDOM
import * as React from 'react';
import * as ReactDOM from 'react-dom/client'; // <-- ИСПРАВЛЕНО: Импортируем 'react-dom/client' для createRoot
export const GRAPH_VIEW_TYPE = 'llm-agent-graph-view';
// ----------------------------------------------- Graph View ---------------------------------------------------------------
export class GraphView extends ItemView {
plugin: LLMAgentPlugin;
graphData: { nodes: any[]; edges: any[] };
currentNode: string | null;
graphsList: any[];
selectedGraphId: string | null;
// UI Elements
historyPanel: HistoryPanel | null;
historyContainer: HTMLDivElement;
graphContainer: HTMLDivElement;
reactRoot: ReactDOM.Root | null;
toggleButton: HTMLButtonElement;
isHistoryCollapsed: boolean;
constructor(leaf: WorkspaceLeaf, plugin: LLMAgentPlugin) {
super(leaf);
this.plugin = plugin;
this.graphData = { nodes: [], edges: [] };
this.currentNode = null;
this.graphsList = [];
this.selectedGraphId = null;
this.historyPanel = null;
this.reactRoot = null; // Инициализируем null
this.isHistoryCollapsed = true;
}
/**
* @returns {string} Тип представления.
*/
getViewType(): string {
return GRAPH_VIEW_TYPE;
}
/**
* @returns {string} Отображаемое имя представления.
*/
getDisplayText(): string {
return 'LLM Agent Graph';
}
/**
* @returns {string} Иконка представления.
*/
getIcon(): string {
return 'brain-circuit';
}
/**
* Вызывается при открытии представления. Инициализирует UI и загружает данные.
* @returns {Promise<void>}
*/
async onOpen(): Promise<void> {
const container = this.containerEl.children[1] as HTMLElement;
container.empty();
// Создаем основной контейнер с flexbox layout
const mainContainer = container.createDiv('llm-agent-graph-main');
mainContainer.style.cssText = `
display: flex;
height: 100%;
width: 100%;
background-color: var(--background-primary); /* Убедимся, что фон установлен */
`;
// Левая панель для истории
this.historyContainer = mainContainer.createDiv('llm-agent-history-sidebar') as HTMLDivElement;
// Убираем инлайн стили - теперь в CSS
this.historyContainer.classList.add(this.isHistoryCollapsed ? 'collapsed' : 'expanded');
// Правая панель для графа
this.graphContainer = mainContainer.createDiv('llm-agent-graph-container') as HTMLDivElement;
this.graphContainer.style.cssText = `
flex: 1;
position: relative;
background-color: var(--background-primary); /* Убедимся, что фон установлен */
`;
// Инициализируем панель истории
this.historyPanel = new HistoryPanel(this.historyContainer, {
graphs: this.graphsList,
eventBus: this.plugin.eventBus
});
// Инициализируем React Flow
this.initializeGraphPanel();
// Создаем кнопку переключения
this.addToggleButton();
// Подписываемся на события
this.plugin.eventBus.on('new-chat', this.handleNewChat.bind(this));
this.plugin.eventBus.on('graph-selected', this.handleGraphSelected.bind(this));
// Загружаем список графов
await this.fetchGraphsList();
}
/**
* Вызывается при закрытии представления. Очищает ресурсы.
* @returns {Promise<void>}
*/
async onClose(): Promise<void> {
if (this.historyPanel) {
this.historyPanel.destroy();
this.historyPanel = null;
}
// Отмонтируем React-компонент
if (this.reactRoot) {
this.reactRoot.unmount();
this.reactRoot = null;
}
this.plugin.eventBus.off('new-chat', this.handleNewChat);
this.plugin.eventBus.off('graph-selected', this.handleGraphSelected); // Отписываемся и отсюда
}
// ----------------------------------------------- History Panel Toggle --------------------------------------------------
/**
* Переключает состояние панели истории между свернутым и развернутым.
*/
toggleHistoryPanel(): void {
this.isHistoryCollapsed = !this.isHistoryCollapsed;
if (this.isHistoryCollapsed) {
this.historyContainer.classList.remove('expanded');
this.historyContainer.classList.add('collapsed');
this.toggleButton.textContent = '📋';
} else {
this.historyContainer.classList.remove('collapsed');
this.historyContainer.classList.add('expanded');
this.toggleButton.textContent = '❌';
}
}
// ----------------------------------------------- Graph Panel Integration --------------------------------------------------
/**
* Инициализирует GraphPanelComponent (React компонент) в graphContainer.
* Использует ReactDOM.createRoot для React 18.
*/
initializeGraphPanel(): void {
// Создаем контейнер для React Flow
// Убедимся, что id уникален или используем ref для React 18
this.graphContainer.empty(); // Очищаем контейнер перед добавлением нового элемента
const wrapper = this.graphContainer.createDiv();
wrapper.style.cssText = 'width: 100%; height: 100%;';
if (!wrapper) {
console.error('Не удалось создать wrapper для монтирования React Flow.');
return;
}
// Создаем корневой элемент React 18
if (ReactDOM && typeof ReactDOM.createRoot === 'function') {
this.reactRoot = ReactDOM.createRoot(wrapper);
this.renderGraphPanel();
} else {
console.error('ReactDOM.createRoot не определен. Убедитесь, что "react-dom/client" правильно импортирован.');
}
}
addToggleButton(): void {
console.log('Creating toggle button...');
this.toggleButton = document.createElement('button');
this.toggleButton.className = 'history-toggle-button';
this.toggleButton.textContent = this.isHistoryCollapsed ? '📋' : '❌';
this.toggleButton.addEventListener('click', this.toggleHistoryPanel.bind(this));
this.graphContainer.appendChild(this.toggleButton);
console.log('Toggle button created and appended:', this.toggleButton);
}
/**
* Рендерит или обновляет GraphPanelComponent в DOM.
*/
renderGraphPanel(): void {
if (this.reactRoot) {
this.reactRoot.render(
React.createElement(GraphPanelComponent, {
graphData: {
nodes: this.graphData.nodes,
edges: this.graphData.edges
},
onNodeClick: this.handleNodeClick.bind(this)
})
);
} else {
console.warn('React root не инициализирован для рендера GraphPanelComponent.');
}
}
// ... (остальные методы API и Event Handlers без изменений)
// ----------------------------------------------- API Methods ---------------------------------------------------------------
/**
* Загружает список всех графов с бэкенда.
* @returns {Promise<void>}
*/
async fetchGraphsList(): Promise<void> {
try {
const response = await fetch(`${this.plugin.settings.apiBaseUrl}/graphs`);
if (!response.ok) {
const errorText = await response.text();
throw new Error(`Ошибка загрузки списка графов: ${response.status} - ${errorText}`);
}
const data = await response.json();
this.graphsList = data;
if (this.historyPanel) {
this.historyPanel.updateGraphs(this.graphsList);
}
} catch (error) {
console.error('Ошибка загрузки списка графов:', error);
// Optionally: show a notice to the user
// new Notice(`Ошибка загрузки списка графов: ${error.message}`);
}
}
/**
* Загружает данные конкретного графа по его ID.
* @param {string} graphId - ID графа для загрузки.
* @returns {Promise<void>}
*/
async fetchGraphData(graphId: string): Promise<void> {
try {
if (!graphId) {
// Если graphId пуст, очищаем граф
this.graphData = { nodes: [], edges: [] };
this.currentNode = null;
this.renderGraphPanel();
return;
}
const response = await fetch(`${this.plugin.settings.apiBaseUrl}/graphs/${graphId}`);
if (!response.ok) {
const errorText = await response.text();
throw new Error(`Ошибка загрузки данных графа: ${response.status} - ${errorText}`);
}
const data = await response.json();
this.graphData = {
nodes: data.graph_nodes || [],
edges: data.graph_edges || []
};
this.currentNode = data.current_node_id;
// Перерендериваем граф
this.renderGraphPanel();
} catch (error) {
console.error('Ошибка загрузки данных графа:', error);
// Optionally: show a notice to the user
// new Notice(`Ошибка загрузки данных графа: ${error.message}`);
}
}
// ----------------------------------------------- Event Handlers ------------------------------------------------------------
/**
* Обработчик клика по узлу графа.
* Устанавливает текущий узел и генерирует событие 'node-selected'.
* @param {string} nodeId - ID выбранного узла.
*/
handleNodeClick(nodeId: string): void {
this.currentNode = nodeId;
this.plugin.eventBus.emit('node-selected', { nodeId });
}
/**
* Обработчик начала нового чата.
* Очищает текущие данные графа и перерендеривает его.
*/
handleNewChat(): void {
this.graphData = { nodes: [], edges: [] };
this.currentNode = null;
this.selectedGraphId = null;
this.renderGraphPanel();
this.fetchGraphsList(); // Обновить список графов, чтобы показать новый пустой чат
}
/**
* Обработчик выбора графа из панели истории.
* Устанавливает выбранный граф и загружает его данные.
* @param {{ graphId: string; currentNode: string | null }} data - Объект, содержащий ID выбранного графа и текущий узел.
* @returns {Promise<void>}
*/
async handleGraphSelected(data: { graphId: string; currentNode: string | null }): Promise<void> {
this.selectedGraphId = data.graphId;
console.log("11111")
// currentNode здесь может быть неактуальным, fetchGraphData загрузит актуальный current_node_id
await this.fetchGraphData(data.graphId);
// Note: Больше не нужно эмитить 'graph-selected' обратно в ChatView, т.к. ChatView уже инициировал это.
// ChatView должен сам обновить свою историю после вызова этого метода или после своего собственного fetch.
}
}