313 lines
12 KiB
TypeScript
313 lines
12 KiB
TypeScript
/**
|
||
* Отображает и управляет интерактивным графом 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.
|
||
}
|
||
}
|