/** * Отображает и управляет интерактивным графом 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} */ async onOpen(): Promise { 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} */ async onClose(): Promise { 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} */ async fetchGraphsList(): Promise { 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} */ async fetchGraphData(graphId: string): Promise { 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} */ async handleGraphSelected(data: { graphId: string; currentNode: string | null }): Promise { this.selectedGraphId = data.graphId; console.log("11111") // currentNode здесь может быть неактуальным, fetchGraphData загрузит актуальный current_node_id await this.fetchGraphData(data.graphId); // Note: Больше не нужно эмитить 'graph-selected' обратно в ChatView, т.к. ChatView уже инициировал это. // ChatView должен сам обновить свою историю после вызова этого метода или после своего собственного fetch. } }