diff --git a/src/components/GraphPanel.ts b/src/components/GraphPanel.ts index e52730f..29ff09f 100644 --- a/src/components/GraphPanel.ts +++ b/src/components/GraphPanel.ts @@ -37,8 +37,12 @@ import { Handle, Position } from 'reactflow'; // Простой компонент для узла типа 'user' // Это базовый пример. Ты можешь стилизовать его по своему усмотрению. const UserNode: React.FC = ({ data, isConnectable }) => { + const displayLabel = data.title || data.label || 'Пользователь'; + const titleGenerated = data.title_generated; + return React.createElement( React.Fragment, + // className: `custom-node user-node ${titleGenerated ? 'title-generated' : 'title-pending'}`, null, // Используем React.Fragment для возврата нескольких элементов без лишнего div React.createElement(Handle, { type: 'source', @@ -50,12 +54,15 @@ const UserNode: React.FC = ({ data, isConnectable }) => { position: Position.Top, isConnectable: isConnectable }), - data.label + displayLabel ); }; // Простой компонент для узла типа 'llm' const LLMNode: React.FC = ({ data, isConnectable }) => { + const displayLabel = data.title || data.label || 'LLM'; + const titleGenerated = data.title_generated; + return React.createElement( React.Fragment, null, @@ -69,12 +76,15 @@ const LLMNode: React.FC = ({ data, isConnectable }) => { position: Position.Top, isConnectable: isConnectable }), - data.label + displayLabel ); }; // Простой компонент для узла типа 'tool' const ToolNode: React.FC = ({ data, isConnectable }) => { + const displayLabel = data.title || data.label || 'Инструмент'; + const titleGenerated = data.title_generated; + return React.createElement( React.Fragment, null, @@ -88,12 +98,15 @@ const ToolNode: React.FC = ({ data, isConnectable }) => { position: Position.Top, isConnectable: isConnectable }), - data.label + displayLabel ); }; // Простой компонент для узла типа 'error' const ErrorNode: React.FC = ({ data, isConnectable }) => { + const displayLabel = data.title || data.label || 'Ошибка'; + const titleGenerated = data.title_generated; + return React.createElement( React.Fragment, null, @@ -107,7 +120,7 @@ const ErrorNode: React.FC = ({ data, isConnectable }) => { position: Position.Top, isConnectable: isConnectable }), - data.label + displayLabel ); }; @@ -137,8 +150,8 @@ const dagreGraph = new dagre.graphlib.Graph().setDefaultEdgeLabel(() => ({})); // Определяем размеры узлов для корректного расположения. // Может потребоваться корректировка в зависимости от реального размера узла в CSS. -const nodeWidth = 120; -const nodeHeight = 40; +const nodeWidth = 120; +const nodeHeight = 40; /** * Применяет алгоритм расположения Dagre к узлам и рёбрам графа. @@ -150,13 +163,13 @@ const nodeHeight = 40; const getLayoutedElements = (nodes: any[], edges: any[], direction: 'TB' | 'LR' = 'TB') => { const isHorizontal = direction === 'LR'; // Компактные настройки для расположения в 2 ряда - dagreGraph.setGraph({ - rankdir: direction, - ranksep: 40, // Уменьшенное расстояние между рангами (рядами) - nodesep: 10, // Уменьшенное расстояние между узлами в одном ранге - edgesep: 20, // Расстояние между рёбрами - marginx: 20, // Отступы по X - marginy: 20 // Отступы по Y + dagreGraph.setGraph({ + rankdir: direction, + ranksep: 40, // Уменьшенное расстояние между рангами (рядами) + nodesep: 10, // Уменьшенное расстояние между узлами в одном ранге + edgesep: 20, // Расстояние между рёбрами + marginx: 20, // Отступы по X + marginy: 20 // Отступы по Y }); // Добавляем узлы в граф Dagre с динамическими размерами @@ -164,10 +177,10 @@ const getLayoutedElements = (nodes: any[], edges: any[], direction: 'TB' | 'LR' // Пытаемся найти элемент узла в DOM для получения реальных размеров const nodeElement = document.querySelector(`[data-id="${node.id}"]`) as HTMLElement; const dimensions = getNodeDimensions(nodeElement); - - dagreGraph.setNode(node.id, { + + dagreGraph.setNode(node.id, { width: Math.min(dimensions.width + 10, 140), // Ограничиваем максимальную ширину - height: Math.min(dimensions.height + 10, 50) // Ограничиваем максимальную высоту + height: Math.min(dimensions.height + 10, 50) // Ограничиваем максимальную высоту }); }); @@ -184,7 +197,7 @@ const getLayoutedElements = (nodes: any[], edges: any[], direction: 'TB' | 'LR' const newNodes = nodes.map((node) => { const nodeWithPosition: DagreNode = dagreGraph.node(node.id); const nodeDimensions = dagreGraph.node(node.id); - + return { ...node, targetPosition: isHorizontal ? 'left' : 'top', @@ -204,11 +217,11 @@ const getNodeDimensions = (nodeElement: HTMLElement | null): { width: number; he if (!nodeElement) { return { width: 120, height: 40 }; // Fallback размеры } - + const rect = nodeElement.getBoundingClientRect(); return { width: Math.max(Math.min(rect.width, 130), 100), // Ограничиваем ширину: мин 100px, макс 130px - height: Math.max(Math.min(rect.height, 45), 30) // Ограничиваем высоту: мин 30px, макс 45px + height: Math.max(Math.min(rect.height, 45), 30) // Ограничиваем высоту: мин 30px, макс 45px }; }; @@ -242,10 +255,14 @@ export const GraphPanelComponent: React.FC = ({ graphData, onNo const initialNodesForLayout = graphData.nodes.map((node) => ({ id: node.id, type: node.type || 'default', - data: { label: node.data?.label || `Node ${node.id}` }, + data: { + label: node.data?.label || `Node ${node.id}`, + title: node.data?.title, + title_generated: node.data?.title_generated || false + }, position: { x: 0, y: 0 } })); - + const initialEdgesForLayout = graphData.edges.map((edge) => ({ id: edge.id, source: edge.source, @@ -259,10 +276,7 @@ export const GraphPanelComponent: React.FC = ({ graphData, onNo // Применяем layout с задержкой, чтобы DOM успел отрендерить узлы setTimeout(() => { - const { nodes: layoutedNodes, edges: layoutedEdges } = getLayoutedElements( - initialNodesForLayout, - initialEdgesForLayout - ); + const { nodes: layoutedNodes, edges: layoutedEdges } = getLayoutedElements(initialNodesForLayout, initialEdgesForLayout); setNodes(layoutedNodes); setEdges(layoutedEdges); }, 100); // Задержка 100ms для рендеринга DOM diff --git a/src/components/HistoryPanel.ts b/src/components/HistoryPanel.ts index 82eaceb..f2d2c5f 100644 --- a/src/components/HistoryPanel.ts +++ b/src/components/HistoryPanel.ts @@ -22,7 +22,7 @@ export class HistoryPanel { container: HTMLElement, props: { graphs: any[]; - eventBus: EventBus; + eventBus: EventBus; } ) { this.container = container; @@ -57,7 +57,7 @@ export class HistoryPanel { setupEventListeners(): void { this.newChatButton.addEventListener('click', () => { - this.props.eventBus.emit('new-chat'); + this.props.eventBus.emit('new-chat'); }); this.deleteButton.addEventListener('click', () => { @@ -76,13 +76,16 @@ export class HistoryPanel { renderGraphs(): void { this.chatList.innerHTML = this.graphs - .map( - (graph) => ` + .map((graph) => { + const titleClass = graph.title_generated ? 'graph-title-generated' : 'graph-title-pending'; + const displayTitle = graph.title || graph.first_message || `Граф ${graph.id}`; + + return `
- - ${graph.first_message || `Граф ${graph.id}`} + + ${displayTitle}
@@ -92,8 +95,8 @@ export class HistoryPanel {
- ` - ) + `; + }) .join(''); // Добавляем обработчики событий @@ -102,7 +105,7 @@ export class HistoryPanel { if (!(e.target as HTMLElement).classList.contains('chat-item-options-button')) { const graphId = (item as HTMLElement).dataset.graphId; if (graphId) { - this.props.eventBus.emit('graph-selected', { graphId: graphId, currentNode: null }); + this.props.eventBus.emit('graph-selected', { graphId: graphId, currentNode: null }); } } }); diff --git a/src/views/ChatView.ts b/src/views/ChatView.ts index 8689a1f..ea1dcfc 100644 --- a/src/views/ChatView.ts +++ b/src/views/ChatView.ts @@ -19,6 +19,8 @@ export class ChatView extends ItemView { chatContainer: HTMLDivElement; chatPanel: ChatPanel | null; availableModels: string[]; + // TODO: сделать аккуратнее - добавлен таймер для обновления заголовков + titleUpdateTimer: NodeJS.Timeout | null; constructor(leaf: WorkspaceLeaf, plugin: LLMAgentPlugin) { super(leaf); @@ -28,6 +30,7 @@ export class ChatView extends ItemView { this.currentNode = null; this.chatPanel = null; this.availableModels = []; + this.titleUpdateTimer = null; } getViewType(): string { @@ -174,6 +177,18 @@ export class ChatView extends ItemView { // Вместо того, чтобы отправлять частичные данные, мы запрашиваем полный граф у бэкенда. this.plugin.eventBus.emit('graph-selected', { graphId: this.selectedGraphId, currentNode: this.currentNode }); + // TODO: сделать аккуратнее - устанавливаем таймер на 30 секунд для обновления заголовков + this.titleUpdateTimer = setTimeout(() => { + console.log('Обновление графа для получения сгенерированных заголовков...'); + if (this.selectedGraphId && this.currentNode) { + this.plugin.eventBus.emit('graph-selected', { + graphId: this.selectedGraphId, + currentNode: this.currentNode + }); + } + this.titleUpdateTimer = null; + }, 20000); // 20 секунд, эмпирически + // Затем обновим ChatView, загрузив историю до нового current_node_id if (newCurrentNodeId) { await this.fetchMessagesFromRootToNode(newCurrentNodeId);