Related to title generation

This commit is contained in:
dimitrievgs 2025-09-24 02:29:11 +03:00
parent 32f55ab086
commit d65996539a
3 changed files with 66 additions and 34 deletions

View File

@ -37,8 +37,12 @@ import { Handle, Position } from 'reactflow';
// Простой компонент для узла типа 'user' // Простой компонент для узла типа 'user'
// Это базовый пример. Ты можешь стилизовать его по своему усмотрению. // Это базовый пример. Ты можешь стилизовать его по своему усмотрению.
const UserNode: React.FC<any> = ({ data, isConnectable }) => { const UserNode: React.FC<any> = ({ data, isConnectable }) => {
const displayLabel = data.title || data.label || 'Пользователь';
const titleGenerated = data.title_generated;
return React.createElement( return React.createElement(
React.Fragment, React.Fragment,
// className: `custom-node user-node ${titleGenerated ? 'title-generated' : 'title-pending'}`,
null, // Используем React.Fragment для возврата нескольких элементов без лишнего div null, // Используем React.Fragment для возврата нескольких элементов без лишнего div
React.createElement(Handle, { React.createElement(Handle, {
type: 'source', type: 'source',
@ -50,12 +54,15 @@ const UserNode: React.FC<any> = ({ data, isConnectable }) => {
position: Position.Top, position: Position.Top,
isConnectable: isConnectable isConnectable: isConnectable
}), }),
data.label displayLabel
); );
}; };
// Простой компонент для узла типа 'llm' // Простой компонент для узла типа 'llm'
const LLMNode: React.FC<any> = ({ data, isConnectable }) => { const LLMNode: React.FC<any> = ({ data, isConnectable }) => {
const displayLabel = data.title || data.label || 'LLM';
const titleGenerated = data.title_generated;
return React.createElement( return React.createElement(
React.Fragment, React.Fragment,
null, null,
@ -69,12 +76,15 @@ const LLMNode: React.FC<any> = ({ data, isConnectable }) => {
position: Position.Top, position: Position.Top,
isConnectable: isConnectable isConnectable: isConnectable
}), }),
data.label displayLabel
); );
}; };
// Простой компонент для узла типа 'tool' // Простой компонент для узла типа 'tool'
const ToolNode: React.FC<any> = ({ data, isConnectable }) => { const ToolNode: React.FC<any> = ({ data, isConnectable }) => {
const displayLabel = data.title || data.label || 'Инструмент';
const titleGenerated = data.title_generated;
return React.createElement( return React.createElement(
React.Fragment, React.Fragment,
null, null,
@ -88,12 +98,15 @@ const ToolNode: React.FC<any> = ({ data, isConnectable }) => {
position: Position.Top, position: Position.Top,
isConnectable: isConnectable isConnectable: isConnectable
}), }),
data.label displayLabel
); );
}; };
// Простой компонент для узла типа 'error' // Простой компонент для узла типа 'error'
const ErrorNode: React.FC<any> = ({ data, isConnectable }) => { const ErrorNode: React.FC<any> = ({ data, isConnectable }) => {
const displayLabel = data.title || data.label || 'Ошибка';
const titleGenerated = data.title_generated;
return React.createElement( return React.createElement(
React.Fragment, React.Fragment,
null, null,
@ -107,7 +120,7 @@ const ErrorNode: React.FC<any> = ({ data, isConnectable }) => {
position: Position.Top, position: Position.Top,
isConnectable: isConnectable isConnectable: isConnectable
}), }),
data.label displayLabel
); );
}; };
@ -137,8 +150,8 @@ const dagreGraph = new dagre.graphlib.Graph().setDefaultEdgeLabel(() => ({}));
// Определяем размеры узлов для корректного расположения. // Определяем размеры узлов для корректного расположения.
// Может потребоваться корректировка в зависимости от реального размера узла в CSS. // Может потребоваться корректировка в зависимости от реального размера узла в CSS.
const nodeWidth = 120; const nodeWidth = 120;
const nodeHeight = 40; const nodeHeight = 40;
/** /**
* Применяет алгоритм расположения Dagre к узлам и рёбрам графа. * Применяет алгоритм расположения Dagre к узлам и рёбрам графа.
@ -150,13 +163,13 @@ const nodeHeight = 40;
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';
// Компактные настройки для расположения в 2 ряда // Компактные настройки для расположения в 2 ряда
dagreGraph.setGraph({ dagreGraph.setGraph({
rankdir: direction, rankdir: direction,
ranksep: 40, // Уменьшенное расстояние между рангами (рядами) ranksep: 40, // Уменьшенное расстояние между рангами (рядами)
nodesep: 10, // Уменьшенное расстояние между узлами в одном ранге nodesep: 10, // Уменьшенное расстояние между узлами в одном ранге
edgesep: 20, // Расстояние между рёбрами edgesep: 20, // Расстояние между рёбрами
marginx: 20, // Отступы по X marginx: 20, // Отступы по X
marginy: 20 // Отступы по Y marginy: 20 // Отступы по Y
}); });
// Добавляем узлы в граф Dagre с динамическими размерами // Добавляем узлы в граф Dagre с динамическими размерами
@ -164,10 +177,10 @@ const getLayoutedElements = (nodes: any[], edges: any[], direction: 'TB' | 'LR'
// Пытаемся найти элемент узла в DOM для получения реальных размеров // Пытаемся найти элемент узла в DOM для получения реальных размеров
const nodeElement = document.querySelector(`[data-id="${node.id}"]`) as HTMLElement; const nodeElement = document.querySelector(`[data-id="${node.id}"]`) as HTMLElement;
const dimensions = getNodeDimensions(nodeElement); const dimensions = getNodeDimensions(nodeElement);
dagreGraph.setNode(node.id, { dagreGraph.setNode(node.id, {
width: Math.min(dimensions.width + 10, 140), // Ограничиваем максимальную ширину 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 newNodes = nodes.map((node) => {
const nodeWithPosition: DagreNode = dagreGraph.node(node.id); const nodeWithPosition: DagreNode = dagreGraph.node(node.id);
const nodeDimensions = dagreGraph.node(node.id); const nodeDimensions = dagreGraph.node(node.id);
return { return {
...node, ...node,
targetPosition: isHorizontal ? 'left' : 'top', targetPosition: isHorizontal ? 'left' : 'top',
@ -204,11 +217,11 @@ const getNodeDimensions = (nodeElement: HTMLElement | null): { width: number; he
if (!nodeElement) { if (!nodeElement) {
return { width: 120, height: 40 }; // Fallback размеры return { width: 120, height: 40 }; // Fallback размеры
} }
const rect = nodeElement.getBoundingClientRect(); const rect = nodeElement.getBoundingClientRect();
return { return {
width: Math.max(Math.min(rect.width, 130), 100), // Ограничиваем ширину: мин 100px, макс 130px 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<GraphPanelProps> = ({ graphData, onNo
const initialNodesForLayout = graphData.nodes.map((node) => ({ const initialNodesForLayout = graphData.nodes.map((node) => ({
id: node.id, id: node.id,
type: node.type || 'default', 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 } position: { x: 0, y: 0 }
})); }));
const initialEdgesForLayout = graphData.edges.map((edge) => ({ const initialEdgesForLayout = graphData.edges.map((edge) => ({
id: edge.id, id: edge.id,
source: edge.source, source: edge.source,
@ -259,10 +276,7 @@ export const GraphPanelComponent: React.FC<GraphPanelProps> = ({ graphData, onNo
// Применяем layout с задержкой, чтобы DOM успел отрендерить узлы // Применяем layout с задержкой, чтобы DOM успел отрендерить узлы
setTimeout(() => { setTimeout(() => {
const { nodes: layoutedNodes, edges: layoutedEdges } = getLayoutedElements( const { nodes: layoutedNodes, edges: layoutedEdges } = getLayoutedElements(initialNodesForLayout, initialEdgesForLayout);
initialNodesForLayout,
initialEdgesForLayout
);
setNodes(layoutedNodes); setNodes(layoutedNodes);
setEdges(layoutedEdges); setEdges(layoutedEdges);
}, 100); // Задержка 100ms для рендеринга DOM }, 100); // Задержка 100ms для рендеринга DOM

View File

@ -22,7 +22,7 @@ export class HistoryPanel {
container: HTMLElement, container: HTMLElement,
props: { props: {
graphs: any[]; graphs: any[];
eventBus: EventBus; eventBus: EventBus;
} }
) { ) {
this.container = container; this.container = container;
@ -57,7 +57,7 @@ export class HistoryPanel {
setupEventListeners(): void { setupEventListeners(): void {
this.newChatButton.addEventListener('click', () => { this.newChatButton.addEventListener('click', () => {
this.props.eventBus.emit('new-chat'); this.props.eventBus.emit('new-chat');
}); });
this.deleteButton.addEventListener('click', () => { this.deleteButton.addEventListener('click', () => {
@ -76,13 +76,16 @@ export class HistoryPanel {
renderGraphs(): void { renderGraphs(): void {
this.chatList.innerHTML = this.graphs this.chatList.innerHTML = this.graphs
.map( .map((graph) => {
(graph) => ` const titleClass = graph.title_generated ? 'graph-title-generated' : 'graph-title-pending';
const displayTitle = graph.title || graph.first_message || `Граф ${graph.id}`;
return `
<div class="chat-list-item" data-graph-id="${graph.id}"> <div class="chat-list-item" data-graph-id="${graph.id}">
<div class="chat-item-content"> <div class="chat-item-content">
<div class="chat-item-text"> <div class="chat-item-text">
<span class="truncate"> <span class="truncate ${titleClass}">
${graph.first_message || `Граф ${graph.id}`} ${displayTitle}
</span> </span>
</div> </div>
<div class="chat-item-actions"> <div class="chat-item-actions">
@ -92,8 +95,8 @@ export class HistoryPanel {
</div> </div>
</div> </div>
</div> </div>
` `;
) })
.join(''); .join('');
// Добавляем обработчики событий // Добавляем обработчики событий
@ -102,7 +105,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) { if (graphId) {
this.props.eventBus.emit('graph-selected', { graphId: graphId, currentNode: null }); this.props.eventBus.emit('graph-selected', { graphId: graphId, currentNode: null });
} }
} }
}); });

View File

@ -19,6 +19,8 @@ export class ChatView extends ItemView {
chatContainer: HTMLDivElement; chatContainer: HTMLDivElement;
chatPanel: ChatPanel | null; chatPanel: ChatPanel | null;
availableModels: string[]; availableModels: string[];
// TODO: сделать аккуратнее - добавлен таймер для обновления заголовков
titleUpdateTimer: NodeJS.Timeout | null;
constructor(leaf: WorkspaceLeaf, plugin: LLMAgentPlugin) { constructor(leaf: WorkspaceLeaf, plugin: LLMAgentPlugin) {
super(leaf); super(leaf);
@ -28,6 +30,7 @@ export class ChatView extends ItemView {
this.currentNode = null; this.currentNode = null;
this.chatPanel = null; this.chatPanel = null;
this.availableModels = []; this.availableModels = [];
this.titleUpdateTimer = null;
} }
getViewType(): string { getViewType(): string {
@ -174,6 +177,18 @@ export class ChatView extends ItemView {
// Вместо того, чтобы отправлять частичные данные, мы запрашиваем полный граф у бэкенда. // Вместо того, чтобы отправлять частичные данные, мы запрашиваем полный граф у бэкенда.
this.plugin.eventBus.emit('graph-selected', { graphId: this.selectedGraphId, currentNode: this.currentNode }); 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 // Затем обновим ChatView, загрузив историю до нового current_node_id
if (newCurrentNodeId) { if (newCurrentNodeId) {
await this.fetchMessagesFromRootToNode(newCurrentNodeId); await this.fetchMessagesFromRootToNode(newCurrentNodeId);