handle race condition while making simultaneous changes
This commit is contained in:
parent
2613776c4c
commit
2bf5763eb1
|
|
@ -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 })
|
||||||
|
)
|
||||||
|
)
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|
@ -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 });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -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) {
|
||||||
|
|
|
||||||
|
|
@ -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.
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user