handle race condition while making simultaneous changes

This commit is contained in:
dimitrievgs 2025-09-16 00:10:35 +03:00
parent 2613776c4c
commit 2bf5763eb1
5 changed files with 272 additions and 275 deletions

84
main.js

File diff suppressed because one or more lines are too long

View File

@ -9,15 +9,15 @@
import * as React from 'react'; import * as React from 'react';
import * as ReactDOM from 'react-dom/client'; // <-- ИСПРАВЛЕНО: для createRoot нужен 'react-dom/client' import * as ReactDOM from 'react-dom/client'; // <-- ИСПРАВЛЕНО: для createRoot нужен 'react-dom/client'
import ReactFlow, { import ReactFlow, {
addEdge, addEdge,
useNodesState, useNodesState,
useEdgesState, useEdgesState,
Background, Background,
Controls, Controls,
ReactFlowProvider, // <-- ИСПРАВЛЕНО: импортируем прямо ReactFlowProvider без переименования ReactFlowProvider, // <-- ИСПРАВЛЕНО: импортируем прямо ReactFlowProvider без переименования
Connection, Connection,
Edge, Edge,
BackgroundVariant // <-- ИСПРАВЛЕНО: используем BackgroundVariant для типизации BackgroundVariant // <-- ИСПРАВЛЕНО: используем BackgroundVariant для типизации
} from 'reactflow'; } from 'reactflow';
// <-- ИСПРАВЛЕНО: Это ключевой момент. Убедитесь, что esbuild настроен на обработку CSS импортов. // <-- ИСПРАВЛЕНО: Это ключевой момент. Убедитесь, что esbuild настроен на обработку CSS импортов.
@ -37,80 +37,88 @@ import { Handle, Position } from 'reactflow';
// Простой компонент для узла типа 'user' // Простой компонент для узла типа 'user'
// Это базовый пример. Ты можешь стилизовать его по своему усмотрению. // Это базовый пример. Ты можешь стилизовать его по своему усмотрению.
const UserNode: React.FC<any> = ({ data, isConnectable }) => { const UserNode: React.FC<any> = ({ data, isConnectable }) => {
return React.createElement(React.Fragment, null, // Используем React.Fragment для возврата нескольких элементов без лишнего div return React.createElement(
React.createElement(Handle, { React.Fragment,
type: "source", null, // Используем React.Fragment для возврата нескольких элементов без лишнего div
position: Position.Bottom, React.createElement(Handle, {
isConnectable: isConnectable, type: 'source',
}), position: Position.Bottom,
React.createElement(Handle, { isConnectable: isConnectable
type: "target", }),
position: Position.Top, React.createElement(Handle, {
isConnectable: isConnectable, type: 'target',
}), position: Position.Top,
data.label isConnectable: isConnectable
); }),
data.label
);
}; };
// Простой компонент для узла типа 'llm' // Простой компонент для узла типа 'llm'
const LLMNode: React.FC<any> = ({ data, isConnectable }) => { const LLMNode: React.FC<any> = ({ data, isConnectable }) => {
return React.createElement(React.Fragment, null, return React.createElement(
React.createElement(Handle, { React.Fragment,
type: "source", null,
position: Position.Bottom, React.createElement(Handle, {
isConnectable: isConnectable, type: 'source',
}), position: Position.Bottom,
React.createElement(Handle, { isConnectable: isConnectable
type: "target", }),
position: Position.Top, React.createElement(Handle, {
isConnectable: isConnectable, type: 'target',
}), position: Position.Top,
data.label isConnectable: isConnectable
); }),
data.label
);
}; };
// Простой компонент для узла типа 'tool' // Простой компонент для узла типа 'tool'
const ToolNode: React.FC<any> = ({ data, isConnectable }) => { const ToolNode: React.FC<any> = ({ data, isConnectable }) => {
return React.createElement(React.Fragment, null, return React.createElement(
React.createElement(Handle, { React.Fragment,
type: "source", null,
position: Position.Bottom, React.createElement(Handle, {
isConnectable: isConnectable, type: 'source',
}), position: Position.Bottom,
React.createElement(Handle, { isConnectable: isConnectable
type: "target", }),
position: Position.Top, React.createElement(Handle, {
isConnectable: isConnectable, type: 'target',
}), position: Position.Top,
data.label isConnectable: isConnectable
); }),
data.label
);
}; };
// Простой компонент для узла типа 'error' // Простой компонент для узла типа 'error'
const ErrorNode: React.FC<any> = ({ data, isConnectable }) => { const ErrorNode: React.FC<any> = ({ data, isConnectable }) => {
return React.createElement(React.Fragment, null, return React.createElement(
React.createElement(Handle, { React.Fragment,
type: "source", null,
position: Position.Bottom, React.createElement(Handle, {
isConnectable: isConnectable, type: 'source',
}), position: Position.Bottom,
React.createElement(Handle, { isConnectable: isConnectable
type: "target", }),
position: Position.Top, React.createElement(Handle, {
isConnectable: isConnectable, type: 'target',
}), position: Position.Top,
data.label isConnectable: isConnectable
); }),
data.label
);
}; };
// Регистрируем кастомные типы узлов // Регистрируем кастомные типы узлов
const nodeTypes = { const nodeTypes = {
user: UserNode, user: UserNode,
llm: LLMNode, llm: LLMNode,
tool: ToolNode, tool: ToolNode,
error: ErrorNode, error: ErrorNode
// Если у тебя есть node.type === 'default', то можешь его тоже определить, // Если у тебя есть node.type === 'default', то можешь его тоже определить,
// либо использовать встроенный 'default' React Flow. // либо использовать встроенный 'default' React Flow.
}; };
// ----------------------------------------------- Dagre Layout ----------------------------------------------------------------- // ----------------------------------------------- Dagre Layout -----------------------------------------------------------------
@ -118,10 +126,10 @@ import dagre from 'dagre'; // Импортируем библиотеку dagre
// Типы для Dagre (были уже корректны) // Типы для Dagre (были уже корректны)
interface DagreNode { interface DagreNode {
x: number; x: number;
y: number; y: number;
width: number; width: number;
height: number; height: number;
} }
// Инициализируем объект Dagre Graph // Инициализируем объект Dagre Graph
@ -140,43 +148,43 @@ const nodeHeight = 36;
* @returns {{nodes: any[], edges: any[]}} Объект с обновлёнными узлами и рёбрами, содержащими позиции. * @returns {{nodes: any[], edges: any[]}} Объект с обновлёнными узлами и рёбрами, содержащими позиции.
*/ */
const getLayoutedElements = (nodes: any[], edges: any[], direction: 'TB' | 'LR' = 'TB') => { const getLayoutedElements = (nodes: any[], edges: any[], direction: 'TB' | 'LR' = 'TB') => {
const isHorizontal = direction === 'LR'; // Проверяем, горизонтальное ли расположение const isHorizontal = direction === 'LR'; // Проверяем, горизонтальное ли расположение
// ДОБАВЛЕНО: ranksep и nodesep для лучшего отступа между узлами // ДОБАВЛЕНО: ranksep и nodesep для лучшего отступа между узлами
dagreGraph.setGraph({ rankdir: direction, ranksep: 100, nodesep: 50 }); dagreGraph.setGraph({ rankdir: direction, ranksep: 100, nodesep: 50 });
// Добавляем узлы в граф Dagre с их размерами // Добавляем узлы в граф Dagre с их размерами
nodes.forEach((node) => { nodes.forEach((node) => {
// Убедимся, что узел имеет уникальный id и необходимые размеры для Dagre // Убедимся, что узел имеет уникальный id и необходимые размеры для Dagre
dagreGraph.setNode(node.id, { width: nodeWidth, height: nodeHeight }); dagreGraph.setNode(node.id, { width: nodeWidth, height: nodeHeight });
}); });
// Добавляем рёбра в граф Dagre // Добавляем рёбра в граф Dagre
edges.forEach((edge) => { edges.forEach((edge) => {
// Dagre ожидает id, source, target // Dagre ожидает id, source, target
dagreGraph.setEdge(edge.source, edge.target); dagreGraph.setEdge(edge.source, edge.target);
}); });
// Выполняем расчёт расположения графа // Выполняем расчёт расположения графа
dagre.layout(dagreGraph); dagre.layout(dagreGraph);
// Обновляем позиции узлов React Flow на основе расчётов Dagre // Обновляем позиции узлов React Flow на основе расчётов Dagre
const newNodes = nodes.map((node) => { const newNodes = nodes.map((node) => {
const nodeWithPosition: DagreNode = dagreGraph.node(node.id); // Получаем данные узла из Dagre const nodeWithPosition: DagreNode = dagreGraph.node(node.id); // Получаем данные узла из Dagre
// Смещаем позицию узла Dagre (якорь в центре) к верхнему левому углу, // Смещаем позицию узла Dagre (якорь в центре) к верхнему левому углу,
// чтобы она соответствовала точке привязки узла React Flow (верхний левый угол). // чтобы она соответствовала точке привязки узла React Flow (верхний левый угол).
return { return {
...node, ...node,
// Обновляем targetPosition и sourcePosition в зависимости от направления для корректного отображения ручек // Обновляем targetPosition и sourcePosition в зависимости от направления для корректного отображения ручек
targetPosition: isHorizontal ? 'left' : 'top', targetPosition: isHorizontal ? 'left' : 'top',
sourcePosition: isHorizontal ? 'right' : 'bottom', sourcePosition: isHorizontal ? 'right' : 'bottom',
position: { position: {
x: nodeWithPosition.x - nodeWidth / 2, // Корректировка X x: nodeWithPosition.x - nodeWidth / 2, // Корректировка X
y: nodeWithPosition.y - nodeHeight / 2, // Корректировка Y y: nodeWithPosition.y - nodeHeight / 2 // Корректировка Y
}, }
}; };
}); });
return { nodes: newNodes, edges }; // Возвращаем обновлённые узлы и рёбра return { nodes: newNodes, edges }; // Возвращаем обновлённые узлы и рёбра
}; };
/** /**
@ -185,8 +193,8 @@ const getLayoutedElements = (nodes: any[], edges: any[], direction: 'TB' | 'LR'
* @property {(nodeId: string) => void} onNodeClick - Обработчик клика по узлу. * @property {(nodeId: string) => void} onNodeClick - Обработчик клика по узлу.
*/ */
interface GraphPanelProps { interface GraphPanelProps {
graphData: { nodes: any[]; edges: any[]; }; graphData: { nodes: any[]; edges: any[] };
onNodeClick: (nodeId: string) => void; onNodeClick: (nodeId: string) => void;
} }
/** /**
@ -195,88 +203,96 @@ interface GraphPanelProps {
* @returns {React.ReactElement} Элемент React. * @returns {React.ReactElement} Элемент React.
*/ */
export const GraphPanelComponent: React.FC<GraphPanelProps> = ({ graphData, onNodeClick }) => { export const GraphPanelComponent: React.FC<GraphPanelProps> = ({ graphData, onNodeClick }) => {
// ----------------------------------------------- React Hooks --------------------------------------------------------------- // ----------------------------------------------- React Hooks ---------------------------------------------------------------
// Инициализируем состояния для узлов и рёбер React Flow // Инициализируем состояния для узлов и рёбер React Flow
const [nodes, setNodes, onNodesChange] = useNodesState([]); const [nodes, setNodes, onNodesChange] = useNodesState([]);
const [edges, setEdges, onEdgesChange] = useEdgesState([]); const [edges, setEdges, onEdgesChange] = useEdgesState([]);
// Используем useEffect для применения Dagre layout при изменении данных графа // Используем useEffect для применения Dagre layout при изменении данных графа
React.useEffect(() => { React.useEffect(() => {
// Проверяем, есть ли данные для построения графа // Проверяем, есть ли данные для построения графа
if (graphData.nodes.length > 0 || graphData.edges.length > 0) { if (graphData.nodes.length > 0 || graphData.edges.length > 0) {
console.log("Applying Dagre layout with data:", graphData); console.log('Applying Dagre layout with data:', graphData);
// Преобразуем входящие данные в формат, необходимый для Dagre и React Flow // Преобразуем входящие данные в формат, необходимый для Dagre и React Flow
// Убеждаемся, что каждый узел имеет данные.label для отображения // Убеждаемся, что каждый узел имеет данные.label для отображения
const initialNodesForLayout = graphData.nodes.map(node => ({ const initialNodesForLayout = graphData.nodes.map((node) => ({
id: node.id, id: node.id,
// ДОБАВЛЕНО: используем node.type, если он есть, иначе 'default' // ДОБАВЛЕНО: используем node.type, если он есть, иначе 'default'
type: node.type || 'default', type: node.type || 'default',
data: { label: node.data?.label || `Node ${node.id}` }, data: { label: node.data?.label || `Node ${node.id}` },
position: { x: 0, y: 0 } // Временная позиция, будет перезаписана Dagre position: { x: 0, y: 0 } // Временная позиция, будет перезаписана Dagre
})); }));
const initialEdgesForLayout = graphData.edges.map(edge => ({ const initialEdgesForLayout = graphData.edges.map((edge) => ({
id: edge.id, id: edge.id,
source: edge.source, source: edge.source,
target: edge.target, target: edge.target,
// Для рёбер можно добавить тип 'default' или ваш кастомный // Для рёбер можно добавить тип 'default' или ваш кастомный
type: edge.type || 'default', type: edge.type || 'default'
})); }));
// Применяем Dagre layout // Применяем Dagre layout
const { nodes: layoutedNodes, edges: layoutedEdges } = getLayoutedElements( const { nodes: layoutedNodes, edges: layoutedEdges } = getLayoutedElements(initialNodesForLayout, initialEdgesForLayout);
initialNodesForLayout,
initialEdgesForLayout
);
// Обновляем состояния узлов и рёбер React Flow // Обновляем состояния узлов и рёбер React Flow
setNodes(layoutedNodes); setNodes(layoutedNodes);
setEdges(layoutedEdges); setEdges(layoutedEdges);
} else { } else {
// Если данных нет, очищаем граф // Если данных нет, очищаем граф
setNodes([]); setNodes([]);
setEdges([]); setEdges([]);
} }
}, [graphData.nodes, graphData.edges]); // Зависимости: изменения в узлах и рёбрах графа }, [graphData.nodes, graphData.edges]); // Зависимости: изменения в узлах и рёбрах графа
// ----------------------------------------------- Event Handlers ------------------------------------------------------------ // ----------------------------------------------- Event Handlers ------------------------------------------------------------
/** /**
* Обработчик добавления нового соединения между узлами (если разрешено). * Обработчик добавления нового соединения между узлами (если разрешено).
* @param {Connection | Edge} params - Параметры нового соединения. * @param {Connection | Edge} params - Параметры нового соединения.
*/ */
const onConnect = React.useCallback( const onConnect = React.useCallback((params: Connection | Edge) => setEdges((eds) => addEdge(params, eds)), [setEdges]);
(params: Connection | Edge) => setEdges((eds) => addEdge(params, eds)),
[setEdges]
);
/** /**
* Обработчик клика по узлу. * Обработчик клика по узлу.
* @param {React.MouseEvent} event - Объект события клика. * @param {React.MouseEvent} event - Объект события клика.
* @param {any} node - Данные узла, по которому был клик. * @param {any} node - Данные узла, по которому был клик.
*/ */
const nodeClickHandler = React.useCallback((event: React.MouseEvent, node: any) => { const nodeClickHandler = React.useCallback(
onNodeClick(node.id); (event: React.MouseEvent, node: any) => {
}, [onNodeClick]); onNodeClick(node.id);
},
[onNodeClick]
);
// ----------------------------------------------- Render -------------------------------------------------------------------- // ----------------------------------------------- Render --------------------------------------------------------------------
return React.createElement('div', { className: 'graph-panel', style: { width: '100%', height: '100%' } }, return React.createElement(
// Оборачиваем ReactFlowProvider вокруг ReactFlow для доступа к контексту React Flow 'div',
React.createElement(ReactFlowProvider, null, { className: 'graph-panel', style: { width: '100%', height: '100%' } },
React.createElement(ReactFlow, { // Оборачиваем ReactFlowProvider вокруг ReactFlow для доступа к контексту React Flow
nodes, React.createElement(
edges, ReactFlowProvider,
onNodesChange, // Обработчик изменения узлов null,
onEdgesChange, // Обработчик изменения рёбер React.createElement(
onConnect, // Обработчик подключения ReactFlow,
onNodeClick: nodeClickHandler, // Обработчик клика по узлу {
fitView: true, // Автоматически центрирует и масштабирует граф при загрузке nodes,
attributionPosition: 'bottom-right', // Позиция атрибуции React Flow edges,
nodeTypes, // <-- ИСПРАВЛЕНО: Передаем зарегистрированные типы узлов onNodesChange, // Обработчик изменения узлов
}, onEdgesChange, // Обработчик изменения рёбер
// Компоненты Controls и Background являются дочерними для ReactFlow onConnect, // Обработчик подключения
React.createElement(Controls, null), onNodeClick: nodeClickHandler, // Обработчик клика по узлу
React.createElement(Background, { variant: BackgroundVariant.Dots, gap: 12, size: 1 }) fitView: true, // Автоматически центрирует и масштабирует граф при загрузке
) attributionPosition: 'bottom-right', // Позиция атрибуции React Flow
) nodeTypes,
); // Дополнительные параметры для лучшего отображения
zoomOnScroll: true,
panOnDrag: true,
minZoom: 0.1,
maxZoom: 2
},
// Компоненты Controls и Background являются дочерними для ReactFlow
React.createElement(Controls, null),
React.createElement(Background, { variant: BackgroundVariant.Dots, gap: 12, size: 1 })
)
)
);
}; };

View File

@ -9,7 +9,7 @@ export class HistoryPanel {
container: HTMLElement; container: HTMLElement;
props: { props: {
graphs: any[]; graphs: any[];
onGraphSelect: (graphId: string) => void; onGraphSelect: (data: { graphId: string; currentNode: string | null }) => void;
eventBus: EventBus; eventBus: EventBus;
}; };
graphs: any[]; graphs: any[];
@ -23,7 +23,7 @@ export class HistoryPanel {
container: HTMLElement, container: HTMLElement,
props: { props: {
graphs: any[]; graphs: any[];
onGraphSelect: (graphId: string) => void; onGraphSelect: (data: { graphId: string; currentNode: string | null }) => void;
eventBus: EventBus; eventBus: EventBus;
} }
) { ) {
@ -104,7 +104,7 @@ export class HistoryPanel {
if (!(e.target as HTMLElement).classList.contains('chat-item-options-button')) { if (!(e.target as HTMLElement).classList.contains('chat-item-options-button')) {
const graphId = (item as HTMLElement).dataset.graphId; const graphId = (item as HTMLElement).dataset.graphId;
if (graphId && this.props.onGraphSelect) { if (graphId && this.props.onGraphSelect) {
this.props.onGraphSelect(graphId); this.props.onGraphSelect({ graphId: graphId, currentNode: null });
} }
} }
}); });

View File

@ -124,36 +124,32 @@ export class ChatView extends ItemView {
}); });
if (!response.ok) { if (!response.ok) {
throw new Error(`HTTP ошибка! статус: ${response.status}`); // Пытаемся прочитать ошибку из тела ответа, если это возможно
const errorData = await response.json().catch(() => ({ message: response.statusText }));
throw new Error(`HTTP ошибка при отправке сообщения! Статус: ${response.status} - ${errorData.message}`);
} }
const result = await response.json(); const result = await response.json();
// Обновляем graph_id и current_node_id всегда
this.selectedGraphId = result.graph_id; this.selectedGraphId = result.graph_id;
const newCurrentNodeId = result.graph_visualization_data.current_node_id; const newCurrentNodeId = result.graph_visualization_data.current_node_id;
this.currentNode = newCurrentNodeId; this.currentNode = newCurrentNodeId;
// Обновляем граф // Сначала обновим GraphView, чтобы он запросил последнюю версию графа
this.plugin.eventBus.emit('graph-updated', { // Это предотвратит состояние гонки, когда разные запросы возвращают частичные графы.
graphData: { // Вместо того, чтобы отправлять частичные данные, мы запрашиваем полный граф у бэкенда.
nodes: result.graph_visualization_data.nodes || [], this.plugin.eventBus.emit('graph-selected', { graphId: this.selectedGraphId, currentNode: this.currentNode });
edges: result.graph_visualization_data.edges || []
},
currentNode: newCurrentNodeId
});
// Загружаем сообщения // Затем обновим ChatView, загрузив историю до нового current_node_id
if (newCurrentNodeId) { if (newCurrentNodeId) {
await this.fetchMessagesFromRootToNode(newCurrentNodeId); await this.fetchMessagesFromRootToNode(newCurrentNodeId);
} else { } else if (this.selectedGraphId) {
// ИСПРАВЛЕНО: Убедиться, что type передается, если это возможно // Если newCurrentNodeId по какой-то причине не вернулся, но graph_id есть,
this.chatHistory = result.messages.map((msg: any) => ({ // пытаемся загрузить историю по последнему известному узлу или просто весь граф (чтобы отобразить пустую историю, если граф новый).
...msg, // В этом случае, если current_node_id отсутствует, historyPanel должна очиститься.
type: msg.type || msg.role // Используем type, если есть, иначе role как fallback this.currentNode = null; // Сбрасываем, если no current_node_id
})) || []; this.fetchMessagesFromRootToNode(''); // Передаем пустую строку или null, чтобы очистить историю
if (this.chatPanel) {
this.chatPanel.updateHistory(this.chatHistory);
}
} }
} catch (error) { } catch (error) {

View File

@ -99,7 +99,7 @@ export class GraphView extends ItemView {
// Инициализируем панель истории // Инициализируем панель истории
this.historyPanel = new HistoryPanel(this.historyContainer, { this.historyPanel = new HistoryPanel(this.historyContainer, {
graphs: this.graphsList, graphs: this.graphsList,
onGraphSelect: this.handleGraphSelect.bind(this), onGraphSelect: this.handleGraphSelected.bind(this),
eventBus: this.plugin.eventBus eventBus: this.plugin.eventBus
}); });
@ -107,8 +107,8 @@ export class GraphView extends ItemView {
this.initializeGraphPanel(); this.initializeGraphPanel();
// Подписываемся на события // Подписываемся на события
this.plugin.eventBus.on('graph-updated', this.handleGraphUpdate.bind(this));
this.plugin.eventBus.on('new-chat', this.handleNewChat.bind(this)); this.plugin.eventBus.on('new-chat', this.handleNewChat.bind(this));
this.plugin.eventBus.on('graph-selected', this.handleGraphSelected.bind(this));
// Загружаем список графов // Загружаем список графов
await this.fetchGraphsList(); await this.fetchGraphsList();
@ -130,8 +130,8 @@ export class GraphView extends ItemView {
this.reactRoot = null; this.reactRoot = null;
} }
this.plugin.eventBus.off('graph-updated', this.handleGraphUpdate);
this.plugin.eventBus.off('new-chat', this.handleNewChat); this.plugin.eventBus.off('new-chat', this.handleNewChat);
this.plugin.eventBus.off('graph-selected', this.handleGraphSelected); // Отписываемся и отсюда
} }
// ----------------------------------------------- Graph Panel Integration -------------------------------------------------- // ----------------------------------------------- Graph Panel Integration --------------------------------------------------
@ -212,6 +212,14 @@ export class GraphView extends ItemView {
*/ */
async fetchGraphData(graphId: string): Promise<void> { async fetchGraphData(graphId: string): Promise<void> {
try { try {
if (!graphId) {
// Если graphId пуст, очищаем граф
this.graphData = { nodes: [], edges: [] };
this.currentNode = null;
this.renderGraphPanel();
return;
}
const response = await fetch(`${this.plugin.settings.apiBaseUrl}/graphs/${graphId}`); const response = await fetch(`${this.plugin.settings.apiBaseUrl}/graphs/${graphId}`);
if (!response.ok) { if (!response.ok) {
const errorText = await response.text(); const errorText = await response.text();
@ -246,17 +254,6 @@ export class GraphView extends ItemView {
this.plugin.eventBus.emit('node-selected', { nodeId }); this.plugin.eventBus.emit('node-selected', { nodeId });
} }
/**
* Обработчик обновления данных графа (например, после нового сообщения).
* Обновляет `graphData` и перерендеривает граф.
* @param {{ graphData: { nodes: any[], edges: any[] }, currentNode: string | null }} data - Обновленные данные графа.
*/
handleGraphUpdate(data: { graphData: { nodes: any[]; edges: any[] }; currentNode: string | null }): void {
this.graphData = data.graphData;
this.currentNode = data.currentNode;
this.renderGraphPanel();
}
/** /**
* Обработчик начала нового чата. * Обработчик начала нового чата.
* Очищает текущие данные графа и перерендеривает его. * Очищает текущие данные графа и перерендеривает его.
@ -272,12 +269,14 @@ export class GraphView extends ItemView {
/** /**
* Обработчик выбора графа из панели истории. * Обработчик выбора графа из панели истории.
* Устанавливает выбранный граф и загружает его данные. * Устанавливает выбранный граф и загружает его данные.
* @param {string} graphId - ID выбранного графа. * @param {{ graphId: string; currentNode: string | null }} data - Объект, содержащий ID выбранного графа и текущий узел.
* @returns {Promise<void>}
*/ */
async handleGraphSelect(graphId: string): Promise<void> { async handleGraphSelected(data: { graphId: string; currentNode: string | null }): Promise<void> {
this.selectedGraphId = graphId; this.selectedGraphId = data.graphId;
await this.fetchGraphData(graphId); // currentNode здесь может быть неактуальным, fetchGraphData загрузит актуальный current_node_id
// Также сообщаем ChatView, что выбран новый граф и он должен обновить свою историю await this.fetchGraphData(data.graphId);
this.plugin.eventBus.emit('graph-selected', { graphId, currentNode: this.currentNode }); // Note: Больше не нужно эмитить 'graph-selected' обратно в ChatView, т.к. ChatView уже инициировал это.
// ChatView должен сам обновить свою историю после вызова этого метода или после своего собственного fetch.
} }
} }