Change to show graph title in graph panel

This commit is contained in:
dimitrievgs 2025-10-05 19:11:41 +03:00
parent 47cff8a121
commit b0d802241b
2 changed files with 259 additions and 215 deletions

35
main.js

File diff suppressed because one or more lines are too long

View File

@ -24,6 +24,7 @@ export class GraphView extends ItemView {
graphsList: any[];
selectedGraphId: string | null;
selectedNodeIds: Set<string>; // Добавляем Set для хранения ID выделенных узлов
selectedGraphTitle: string | null;
// UI Elements
historyPanel: HistoryPanel | null;
@ -33,9 +34,9 @@ export class GraphView extends ItemView {
toggleButton: HTMLButtonElement;
isHistoryCollapsed: boolean;
// Глобальные кнопки действий
copySelectedButton: HTMLButtonElement | null = null;
deleteSelectedButton: HTMLButtonElement | null = null;
// Глобальные кнопки действий
copySelectedButton: HTMLButtonElement | null = null;
deleteSelectedButton: HTMLButtonElement | null = null;
constructor(leaf: WorkspaceLeaf, plugin: LLMAgentPlugin) {
super(leaf);
@ -47,7 +48,8 @@ export class GraphView extends ItemView {
this.historyPanel = null;
this.reactRoot = null;
this.isHistoryCollapsed = true;
this.selectedNodeIds = new Set<string>(); // Инициализируем Set
this.selectedNodeIds = new Set<string>(); // Инициализируем Set
this.selectedGraphTitle = null;
}
/**
@ -61,7 +63,7 @@ export class GraphView extends ItemView {
* @returns {string} Отображаемое имя представления.
*/
getDisplayText(): string {
return 'LLM Agent Graph';
return this.selectedGraphTitle || 'LLM Agent Graph';
}
/**
@ -106,9 +108,9 @@ export class GraphView extends ItemView {
eventBus: this.plugin.eventBus
});
// Создаем кнопки переключения и глобальные кнопки действий
// Создаем кнопки переключения и глобальные кнопки действий
this.addToggleButton();
this.addGlobalActionButtons(); // Добавляем вызов новой функции
this.addGlobalActionButtons(); // Добавляем вызов новой функции
// Инициализируем React Flow
this.initializeGraphPanel();
@ -116,7 +118,7 @@ export class GraphView extends ItemView {
// Подписываемся на события
this.plugin.eventBus.on('new-chat', this.handleNewChat.bind(this));
this.plugin.eventBus.on('graph-selected', this.handleGraphSelected.bind(this));
this.plugin.eventBus.on('delete-node', this.handleDeleteNodeFromChat.bind(this)); // Слушаем событие из ChatPanel
this.plugin.eventBus.on('delete-node', this.handleDeleteNodeFromChat.bind(this)); // Слушаем событие из ChatPanel
// Загружаем список графов
await this.fetchGraphsList();
@ -140,9 +142,23 @@ export class GraphView extends ItemView {
this.plugin.eventBus.off('new-chat', this.handleNewChat);
this.plugin.eventBus.off('graph-selected', this.handleGraphSelected);
this.plugin.eventBus.off('delete-node', this.handleDeleteNodeFromChat);
this.plugin.eventBus.off('delete-node', this.handleDeleteNodeFromChat);
}
// ----------------------------------------------- Custom Title Update ---------------------------------------------------
/**
* Обновляет текстовое содержимое заголовка представления.
* Этот метод должен быть вызван для динамического изменения заголовка
* после первоначальной отрисовки представления.
*/
updateTitle(): void {
const titleElement = this.containerEl.querySelector('.view-header-title');
if (titleElement) {
titleElement.textContent = this.selectedGraphTitle || 'LLM Agent Graph';
}
}
// ----------------------------------------------- History Panel Toggle --------------------------------------------------
/**
@ -164,37 +180,37 @@ export class GraphView extends ItemView {
// ----------------------------------------------- Graph Panel Integration --------------------------------------------------
addToggleButton(): void {
const buttonWrapper = this.graphContainer.createDiv({ cls: 'graph-view-toolbar' }); // Создаем контейнер для кнопок
addToggleButton(): void {
const buttonWrapper = this.graphContainer.createDiv({ cls: 'graph-view-toolbar' }); // Создаем контейнер для кнопок
this.toggleButton = buttonWrapper.createEl('button', { cls: 'global-action-button' }); // history-toggle-button
this.toggleButton.textContent = this.isHistoryCollapsed ? '📋' : '❌';
this.toggleButton.addEventListener('click', this.toggleHistoryPanel.bind(this));
console.log('Toggle button created and appended:', this.toggleButton);
}
/**
* Добавляет глобальные кнопки действий для выделенных узлов.
*/
addGlobalActionButtons(): void {
const toolbar = this.graphContainer.querySelector('.graph-view-toolbar');
if (!toolbar) return;
/**
* Добавляет глобальные кнопки действий для выделенных узлов.
*/
addGlobalActionButtons(): void {
const toolbar = this.graphContainer.querySelector('.graph-view-toolbar');
if (!toolbar) return;
// Кнопка "Копировать выделенные"
this.copySelectedButton = toolbar.createEl('button', {
cls: 'global-action-button',
attr: { 'aria-label': 'Копировать выделенные узлы', 'data-tooltip': 'Копировать выделенные узлы' },
text: '📄'
});
this.copySelectedButton.addEventListener('click', this.handleCopySelectedNodes.bind(this));
// Кнопка "Копировать выделенные"
this.copySelectedButton = toolbar.createEl('button', {
cls: 'global-action-button',
attr: { 'aria-label': 'Копировать выделенные узлы', 'data-tooltip': 'Копировать выделенные узлы' },
text: '📄'
});
this.copySelectedButton.addEventListener('click', this.handleCopySelectedNodes.bind(this));
// Кнопка "Удалить выделенные"
this.deleteSelectedButton = toolbar.createEl('button', {
cls: 'global-action-button',
attr: { 'aria-label': 'Удалить выделенные узлы', 'data-tooltip': 'Удалить выделенные узлы' },
text: '🗑️'
});
this.deleteSelectedButton.addEventListener('click', this.handleDeleteSelectedNodes.bind(this));
}
// Кнопка "Удалить выделенные"
this.deleteSelectedButton = toolbar.createEl('button', {
cls: 'global-action-button',
attr: { 'aria-label': 'Удалить выделенные узлы', 'data-tooltip': 'Удалить выделенные узлы' },
text: '🗑️'
});
this.deleteSelectedButton.addEventListener('click', this.handleDeleteSelectedNodes.bind(this));
}
// ----------------------------------------------- Graph Panel Integration --------------------------------------------------
@ -204,9 +220,9 @@ export class GraphView extends ItemView {
*/
initializeGraphPanel(): void {
this.graphContainer.empty();
// Перемещаем кнопки обратно после очистки, если они были частью graphContainer
this.addToggleButton();
this.addGlobalActionButtons();
// Перемещаем кнопки обратно после очистки, если они были частью graphContainer
this.addToggleButton();
this.addGlobalActionButtons();
const wrapper = this.graphContainer.createDiv();
wrapper.style.cssText = 'width: 100%; height: 100%;';
@ -236,10 +252,10 @@ export class GraphView extends ItemView {
edges: this.graphData.edges
},
onNodeClick: this.handleNodeClick.bind(this),
onNodeSelectionChange: this.handleNodeSelectionChange.bind(this), // Передаем обработчик изменения выделения
onNodeDelete: this.handleDeleteNode.bind(this), // Передаем колбэк для удаления одиночного узла
selectedNodeIds: this.selectedNodeIds, // Передаем Set выделенных узлов
app: this.plugin.app
onNodeSelectionChange: this.handleNodeSelectionChange.bind(this), // Передаем обработчик изменения выделения
onNodeDelete: this.handleDeleteNode.bind(this), // Передаем колбэк для удаления одиночного узла
selectedNodeIds: this.selectedNodeIds, // Передаем Set выделенных узлов
app: this.plugin.app
})
);
} else {
@ -272,87 +288,92 @@ export class GraphView extends ItemView {
}
}
/**
* Загружает данные конкретного графа по его ID.
* @param {string} graphId - ID графа для загрузки.
* @returns {Promise<void>}
*/
async fetchGraphData(graphId: string): Promise<void> {
try {
if (!graphId) {
this.graphData = { nodes: [], edges: [] };
this.currentNode = null;
this.selectedNodeIds.clear(); // Очищаем выделения
this.renderGraphPanel();
this.plugin.eventBus.emit('chat-history-updated', { graphId: null, messages: [], current_node_id: null });
return;
}
/**
* Загружает данные конкретного графа по его ID.
* @param {string} graphId - ID графа для загрузки.
* @returns {Promise<void>}
*/
async fetchGraphData(graphId: string): Promise<void> {
try {
if (!graphId) {
this.graphData = { nodes: [], edges: [] };
this.currentNode = null;
this.selectedNodeIds.clear(); // Очищаем выделения
this.selectedGraphTitle = null; // Сброс заголовка
this.updateTitle(); // Обновляем заголовок
this.renderGraphPanel();
this.plugin.eventBus.emit('chat-history-updated', { graphId: null, messages: [], current_node_id: null });
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();
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.graphData = {
nodes: data.graph_nodes || [],
edges: data.graph_edges || []
};
this.currentNode = data.current_node_id;
// Перерендериваем граф
this.renderGraphPanel();
// Устанавливаем заголовок графа
this.selectedGraphTitle = data.title || data.first_message || `Граф ${graphId.substring(0, 8)}...`;
this.updateTitle(); // Обновляем заголовок
// Перерендериваем граф
this.renderGraphPanel();
// Также обновляем ChatView с полученными сообщениями и текущим узлом
this.plugin.eventBus.emit('chat-history-updated', {
graphId: graphId,
messages: data.messages || [],
current_node_id: data.current_node_id // Передаем актуальный current_node_id
});
// Также обновляем ChatView с полученными сообщениями и текущим узлом
this.plugin.eventBus.emit('chat-history-updated', {
graphId: graphId,
messages: data.messages || [],
current_node_id: data.current_node_id // Передаем актуальный current_node_id
});
} catch (error) {
console.error('Ошибка загрузки данных графа:', error);
this.selectedGraphTitle = null;
this.updateTitle(); // Обновляем заголовок
// Optionally: show a notice to the user
// new Notice(`Ошибка загрузки данных графа: ${error.message}`);
}
}
} catch (error) {
console.error('Ошибка загрузки данных графа:', error);
// Optionally: show a notice to the user
// new Notice(`Ошибка загрузки данных графа: ${error.message}`);
}
}
/**
* Удаляет узел графа по его ID.
* @param {string} graphId - ID графа.
* @param {string} nodeId - ID узла для удаления.
* @returns {Promise<void>}
*/
async handleDeleteNode(nodeId: string): Promise<void> {
if (!this.selectedGraphId) {
new Notice('Не выбран граф для удаления узла.', 3000);
return;
}
/**
* Удаляет узел графа по его ID.
* @param {string} graphId - ID графа.
* @param {string} nodeId - ID узла для удаления.
* @returns {Promise<void>}
*/
async handleDeleteNode(nodeId: string): Promise<void> {
if (!this.selectedGraphId) {
new Notice('Не выбран граф для удаления узла.', 3000);
return;
}
try {
const response = await fetch(`${this.plugin.settings.apiBaseUrl}/graphs/${this.selectedGraphId}/nodes/${nodeId}`, {
method: 'DELETE'
});
try {
const response = await fetch(`${this.plugin.settings.apiBaseUrl}/graphs/${this.selectedGraphId}/nodes/${nodeId}`, {
method: 'DELETE'
});
if (!response.ok) {
const errorText = await response.text();
throw new Error(`Ошибка удаления узла: ${response.status} - ${errorText}`);
}
if (!response.ok) {
const errorText = await response.text();
throw new Error(`Ошибка удаления узла: ${response.status} - ${errorText}`);
}
new Notice('Узел успешно удален.', 2000);
// После удаления узла, нужно обновить данные графа и перерисовать его.
// Это также обновит ChatPanel, если удаленное сообщение было частью текущей ветки.
await this.fetchGraphData(this.selectedGraphId);
new Notice('Узел успешно удален.', 2000);
// После удаления узла, нужно обновить данные графа и перерисовать его.
// Это также обновит ChatPanel, если удаленное сообщение было частью текущей ветки.
await this.fetchGraphData(this.selectedGraphId);
// Сбрасываем выделение, если удаленный узел был выделен
this.selectedNodeIds.delete(nodeId);
} catch (error) {
console.error(`Ошибка при удалении узла ${nodeId}:`, error);
new Notice(`Ошибка при удалении узла: ${error.message}`, 5000);
}
}
// Сбрасываем выделение, если удаленный узел был выделен
this.selectedNodeIds.delete(nodeId);
} catch (error) {
console.error(`Ошибка при удалении узла ${nodeId}:`, error);
new Notice(`Ошибка при удалении узла: ${error.message}`, 5000);
}
}
// ----------------------------------------------- Event Handlers ------------------------------------------------------------
@ -363,26 +384,26 @@ export class GraphView extends ItemView {
*/
handleNodeClick(nodeId: string): void {
this.currentNode = nodeId;
// Очищаем предыдущие выделения при обычном клике
if (this.selectedNodeIds.size > 0) {
this.selectedNodeIds.clear();
}
// Очищаем предыдущие выделения при обычном клике
if (this.selectedNodeIds.size > 0) {
this.selectedNodeIds.clear();
}
this.plugin.eventBus.emit('node-selected', { nodeId, graphId: this.selectedGraphId });
}
/**
* Обработчик изменения состояния выделения узла.
* @param {string} nodeId - ID узла.
* @param {boolean} isSelected - Новое состояние выделения.
*/
handleNodeSelectionChange(nodeId: string, isSelected: boolean): void {
if (isSelected) {
this.selectedNodeIds.add(nodeId);
} else {
this.selectedNodeIds.delete(nodeId);
}
this.renderGraphPanel(); // Перерендериваем для обновления визуального состояния выделения
}
/**
* Обработчик изменения состояния выделения узла.
* @param {string} nodeId - ID узла.
* @param {boolean} isSelected - Новое состояние выделения.
*/
handleNodeSelectionChange(nodeId: string, isSelected: boolean): void {
if (isSelected) {
this.selectedNodeIds.add(nodeId);
} else {
this.selectedNodeIds.delete(nodeId);
}
this.renderGraphPanel(); // Перерендериваем для обновления визуального состояния выделения
}
/**
* Обработчик начала нового чата.
@ -393,7 +414,9 @@ export class GraphView extends ItemView {
this.currentNode = null;
this.selectedGraphId = null;
this.selectedNodeIds.clear(); // Очищаем выделения
this.renderGraphPanel();
this.selectedGraphTitle = null; // Сброс заголовка при новом чате
this.updateTitle(); // Обновляем заголовок
this.renderGraphPanel();
this.fetchGraphsList(); // Обновить список графов, чтобы показать новый пустой чат
this.plugin.eventBus.emit('chat-history-updated', { graphId: null, messages: [], current_node_id: null });
}
@ -406,100 +429,100 @@ export class GraphView extends ItemView {
*/
async handleGraphSelected(data: { graphId: string; currentNode: string | null }): Promise<void> {
this.selectedGraphId = data.graphId;
this.selectedNodeIds.clear(); // Очищаем выделения при смене графа
this.selectedNodeIds.clear(); // Очищаем выделения при смене графа
await this.fetchGraphData(data.graphId);
}
/**
* Обработчик события удаления узла из ChatPanel.
* @param {{ graphId: string, nodeId: string }} data - Данные об удаляемом узле.
*/
/**
* Обработчик события удаления узла из ChatPanel.
* @param {{ graphId: string, nodeId: string }} data - Данные об удаляемом узле.
*/
async handleDeleteNodeFromChat(data: { graphId: string, nodeId: string }): Promise<void> {
if (data.graphId === this.selectedGraphId) {
await this.handleDeleteNode(data.nodeId);
} else {
new Notice('Сообщение относится к другому графу. Удаление невозможно из текущего контекста.', 3000);
}
}
if (data.graphId === this.selectedGraphId) {
await this.handleDeleteNode(data.nodeId);
} else {
new Notice('Сообщение относится к другому графу. Удаление невозможно из текущего контекста.', 3000);
}
}
/**
* Обработчик для кнопки "Копировать выделенные узлы".
*/
async handleCopySelectedNodes(): Promise<void> {
if (this.selectedNodeIds.size === 0) {
new Notice('Выберите узлы для копирования.', 2000);
return;
}
/**
* Обработчик для кнопки "Копировать выделенные узлы".
*/
async handleCopySelectedNodes(): Promise<void> {
if (this.selectedNodeIds.size === 0) {
new Notice('Выберите узлы для копирования.', 2000);
return;
}
const messagesToCopy: string[] = [];
// Собираем сообщения в порядке выделения
// Необходимо пройтись по nodes в graphData, чтобы получить актуальные данные сообщений
const orderedNodeIds = Array.from(this.selectedNodeIds); // Сохраняем порядок
for (const nodeId of orderedNodeIds) {
const messagesToCopy: string[] = [];
// Собираем сообщения в порядке выделения
// Необходимо пройтись по nodes в graphData, чтобы получить актуальные данные сообщений
const orderedNodeIds = Array.from(this.selectedNodeIds); // Сохраняем порядок
for (const nodeId of orderedNodeIds) {
const node = this.graphData.nodes.find(n => n.id === nodeId);
if (node && node.data && node.data.message && node.data.message.content) {
messagesToCopy.push(node.data.message.content);
}
}
if (node && node.data && node.data.message && node.data.message.content) {
messagesToCopy.push(node.data.message.content);
}
}
if (messagesToCopy.length > 0) {
try {
await navigator.clipboard.writeText(messagesToCopy.join('\n\n--- Разделитель --- \n\n'));
new Notice(`Скопировано ${messagesToCopy.length} сообщений.`, 2000);
} catch (err) {
console.error('Не удалось скопировать сообщения:', err);
new Notice('Ошибка при копировании сообщений.', 3000);
}
} else {
new Notice('Нет содержимого для копирования в выделенных узлах.', 2000);
}
}
if (messagesToCopy.length > 0) {
try {
await navigator.clipboard.writeText(messagesToCopy.join('\n\n--- Разделитель --- \n\n'));
new Notice(`Скопировано ${messagesToCopy.length} сообщений.`, 2000);
} catch (err) {
console.error('Не удалось скопировать сообщения:', err);
new Notice('Ошибка при копировании сообщений.', 3000);
}
} else {
new Notice('Нет содержимого для копирования в выделенных узлах.', 2000);
}
}
/**
* Обработчик для кнопки "Удалить выделенные узлы".
*/
async handleDeleteSelectedNodes(): Promise<void> {
if (this.selectedNodeIds.size === 0) {
new Notice('Выберите узлы для удаления.', 2000);
return;
}
/**
* Обработчик для кнопки "Удалить выделенные узлы".
*/
async handleDeleteSelectedNodes(): Promise<void> {
if (this.selectedNodeIds.size === 0) {
new Notice('Выберите узлы для удаления.', 2000);
return;
}
if (!this.selectedGraphId) {
new Notice('Не выбран граф для удаления узлов.', 3000);
return;
}
if (!this.selectedGraphId) {
new Notice('Не выбран граф для удаления узлов.', 3000);
return;
}
const confirmed = await Dialog.confirm(
this.plugin.app,
`Вы уверены, что хотите удалить ${this.selectedNodeIds.size} выделенных узлов?`
);
const confirmed = await Dialog.confirm(
this.plugin.app,
`Вы уверены, что хотите удалить ${this.selectedNodeIds.size} выделенных узлов?`
);
if (!confirmed)
return;
new Notice(`Начинаю удаление ${this.selectedNodeIds.size} узлов...`, 2000);
const nodesToDelete = Array.from(this.selectedNodeIds); // Сохраняем порядок выделения
new Notice(`Начинаю удаление ${this.selectedNodeIds.size} узлов...`, 2000);
const nodesToDelete = Array.from(this.selectedNodeIds); // Сохраняем порядок выделения
for (const nodeId of nodesToDelete) {
try {
const response = await fetch(`${this.plugin.settings.apiBaseUrl}/graphs/${this.selectedGraphId}/nodes/${nodeId}`, {
method: 'DELETE'
});
for (const nodeId of nodesToDelete) {
try {
const response = await fetch(`${this.plugin.settings.apiBaseUrl}/graphs/${this.selectedGraphId}/nodes/${nodeId}`, {
method: 'DELETE'
});
if (!response.ok) {
const errorText = await response.text();
throw new Error(`Ошибка удаления узла ${nodeId}: ${response.status} - ${errorText}`);
}
new Notice(`Узел ${nodeId.substring(0, 8)}... успешно удален.`, 1000);
} catch (error) {
console.error(`Ошибка при удалении узла ${nodeId}:`, error);
new Notice(`Ошибка при удалении узла ${nodeId.substring(0, 8)}...: ${error.message}`, 5000);
// Продолжаем попытки удаления остальных узлов, но фиксируем ошибку
}
}
if (!response.ok) {
const errorText = await response.text();
throw new Error(`Ошибка удаления узла ${nodeId}: ${response.status} - ${errorText}`);
}
new Notice(`Узел ${nodeId.substring(0, 8)}... успешно удален.`, 1000);
} catch (error) {
console.error(`Ошибка при удалении узла ${nodeId}:`, error);
new Notice(`Ошибка при удалении узла ${nodeId.substring(0, 8)}...: ${error.message}`, 5000);
// Продолжаем попытки удаления остальных узлов, но фиксируем ошибку
}
}
// После всех удалений очищаем выделение и обновляем граф полностью
this.selectedNodeIds.clear();
await this.fetchGraphData(this.selectedGraphId); // Полностью обновляем граф и чат-панель
new Notice('Операция удаления завершена.', 2000);
}
// После всех удалений очищаем выделение и обновляем граф полностью
this.selectedNodeIds.clear();
await this.fetchGraphData(this.selectedGraphId); // Полностью обновляем граф и чат-панель
new Notice('Операция удаления завершена.', 2000);
}
}