Fix context menu for HistoryPanel, add rename graph button

This commit is contained in:
dimitrievgs 2025-10-14 05:39:59 +03:00
parent 78e17e10dc
commit 7be9aa3d2e
5 changed files with 692 additions and 103 deletions

306
main.js

File diff suppressed because one or more lines are too long

View File

@ -3,7 +3,9 @@
* прошлых разговоров, начала новых чатов и удаления записей графов.
*/
import LLMAgentPlugin from 'main';
import { EventBus } from '../utils/EventBus';
import { Dialog } from 'src/utils/Dialog';
export class HistoryPanel {
container: HTMLElement;
@ -11,23 +13,33 @@ export class HistoryPanel {
graphs: any[];
eventBus: EventBus;
};
plugin: LLMAgentPlugin;
graphs: any[];
newChatButton: HTMLButtonElement;
clearAllButton: HTMLButtonElement;
chatList: HTMLElement;
contextMenu: HTMLElement;
deleteButton: HTMLButtonElement;
renameButton: HTMLButtonElement;
contextMenuGraphId: string | null;
currentPage: number;
itemsPerPage: number;
paginationContainer: HTMLElement;
constructor(
container: HTMLElement,
props: {
graphs: any[];
eventBus: EventBus;
}
},
plugin: LLMAgentPlugin
) {
this.container = container;
this.props = props;
this.plugin = plugin;
this.graphs = props.graphs || [];
this.currentPage = 1;
this.itemsPerPage = 40;
this.init();
}
@ -37,18 +49,26 @@ export class HistoryPanel {
<div class="llm-agent-history-panel">
<div class="history-header">
<h3>История диалогов</h3>
<div class="history-header-buttons">
<button id="new-chat-button" class="new-chat-button">+ Новый чат</button>
<button id="clear-all-button" class="clear-all-button">Очистить чаты</button>
</div>
</div>
<div class="chat-list" id="chat-list"></div>
<div class="context-menu" id="context-menu" style="display: none;">
<div class="pagination-container" id="pagination-container"></div>
<div class="menu" id="context-menu" style="display: none;">
<button id="rename-graph-button">Переименовать</button>
<button id="delete-graph-button">Удалить</button>
</div>
</div>
`;
this.newChatButton = this.container.querySelector('#new-chat-button') as HTMLButtonElement;
this.clearAllButton = this.container.querySelector('#clear-all-button') as HTMLButtonElement; // ДОБАВЛЕНО
this.chatList = this.container.querySelector('#chat-list') as HTMLElement;
this.paginationContainer = this.container.querySelector('#pagination-container') as HTMLElement; // ДОБАВЛЕНО
this.contextMenu = this.container.querySelector('#context-menu') as HTMLElement;
this.renameButton = this.container.querySelector('#rename-graph-button') as HTMLButtonElement; // ДОБАВЛЕНО
this.deleteButton = this.container.querySelector('#delete-graph-button') as HTMLButtonElement;
this.setupEventListeners();
@ -60,6 +80,16 @@ export class HistoryPanel {
this.props.eventBus.emit('new-chat');
});
this.clearAllButton.addEventListener('click', () => {
this.handleClearAllChats();
});
this.renameButton.addEventListener('click', () => {
if (this.contextMenuGraphId) {
this.handleRenameGraph(this.contextMenuGraphId);
}
});
this.deleteButton.addEventListener('click', () => {
if (this.contextMenuGraphId) {
this.handleDeleteGraph(this.contextMenuGraphId);
@ -75,7 +105,11 @@ export class HistoryPanel {
}
renderGraphs(): void {
this.chatList.innerHTML = this.graphs
const startIndex = (this.currentPage - 1) * this.itemsPerPage;
const endIndex = startIndex + this.itemsPerPage;
const graphsToShow = this.graphs.slice(startIndex, endIndex);
this.chatList.innerHTML = graphsToShow
.map((graph) => {
const titleClass = graph.title_generated ? 'graph-title-generated' : 'graph-title-pending';
const displayTitle = graph.title || graph.first_message || `Граф ${graph.id}`;
@ -117,13 +151,159 @@ export class HistoryPanel {
this.showContextMenu(e, (button as HTMLElement).dataset.graphId || '');
});
});
this.renderPagination();
}
renderPagination(): void {
const totalPages = Math.ceil(this.graphs.length / this.itemsPerPage);
if (totalPages <= 1) {
this.paginationContainer.innerHTML = '';
return;
}
let paginationHTML = '<div class="pagination-buttons">';
// Кнопка "Предыдущая"
if (this.currentPage > 1) {
paginationHTML += `<button class="pagination-button" data-page="${this.currentPage - 1}">←</button>`;
}
// Номера страниц
const maxVisiblePages = 5;
let startPage = Math.max(1, this.currentPage - Math.floor(maxVisiblePages / 2));
let endPage = Math.min(totalPages, startPage + maxVisiblePages - 1);
if (endPage - startPage < maxVisiblePages - 1) {
startPage = Math.max(1, endPage - maxVisiblePages + 1);
}
if (startPage > 1) {
paginationHTML += `<button class="pagination-button" data-page="1">1</button>`;
if (startPage > 2) {
paginationHTML += `<span class="pagination-ellipsis">...</span>`;
}
}
for (let i = startPage; i <= endPage; i++) {
const activeClass = i === this.currentPage ? 'active' : '';
paginationHTML += `<button class="pagination-button ${activeClass}" data-page="${i}">${i}</button>`;
}
if (endPage < totalPages) {
if (endPage < totalPages - 1) {
paginationHTML += `<span class="pagination-ellipsis">...</span>`;
}
paginationHTML += `<button class="pagination-button" data-page="${totalPages}">${totalPages}</button>`;
}
// Кнопка "Следующая"
if (this.currentPage < totalPages) {
paginationHTML += `<button class="pagination-button" data-page="${this.currentPage + 1}">→</button>`;
}
paginationHTML += '</div>';
this.paginationContainer.innerHTML = paginationHTML;
// Обработчики для кнопок пагинации
this.paginationContainer.querySelectorAll('.pagination-button').forEach((button) => {
button.addEventListener('click', (e) => {
const page = parseInt((e.target as HTMLElement).dataset.page || '1');
this.currentPage = page;
this.renderGraphs();
});
});
}
showContextMenu(event: Event, graphId: string): void {
event.stopPropagation();
this.contextMenuGraphId = graphId;
this.contextMenu.style.display = 'block';
this.contextMenu.style.left = `${(event as MouseEvent).clientX}px`;
this.contextMenu.style.top = `${(event as MouseEvent).clientY}px`;
const buttonElement = event.target as HTMLElement;
const rect = buttonElement.getBoundingClientRect();
const workspaceLeafContent = buttonElement.closest('.workspace-leaf-content') as HTMLElement;
let relativeContainerRect = { left: 0, top: 0 };
if (workspaceLeafContent) {
relativeContainerRect = workspaceLeafContent.getBoundingClientRect();
} else {
// Если .workspace-leaf-content не найден, используем container как запасной вариант
relativeContainerRect = this.container.getBoundingClientRect();
}
this.contextMenu.style.display = 'flex';
this.contextMenu.style.left = `${rect.left - relativeContainerRect.left}px`;
this.contextMenu.style.top = `${rect.bottom - relativeContainerRect.top + 4}px`;
}
async handleRenameGraph(graphId: string): Promise<void> {
try {
const graph = this.graphs.find((g) => g.id === graphId);
if (!graph) return;
const currentTitle = graph.title || graph.first_message || '';
// Создаём простой промпт для ввода нового названия
const newTitle = await Dialog.prompt(this.plugin.app, 'Введите новое название:', currentTitle);
if (newTitle === null || newTitle.trim() === '') {
return; // Пользователь отменил или ввёл пустое название
}
const response = await fetch(`http://localhost:5000/api/graphs/${graphId}/rename`, {
method: 'PATCH',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({ title: newTitle.trim() })
});
if (!response.ok) {
throw new Error(`HTTP ошибка! статус: ${response.status}`);
}
// Обновляем локальный список графов
const graphIndex = this.graphs.findIndex((g) => g.id === graphId);
if (graphIndex !== -1) {
this.graphs[graphIndex].title = newTitle.trim();
this.graphs[graphIndex].title_generated = true;
}
this.renderGraphs();
this.contextMenu.style.display = 'none';
} catch (error) {
console.error('Ошибка переименования графа:', error);
}
}
async handleClearAllChats(): Promise<void> {
try {
const confirmed = confirm('Вы уверены, что хотите удалить все чаты? Это действие нельзя отменить.');
if (!confirmed) {
return;
}
const response = await fetch(`http://localhost:5000/api/graphs`, {
method: 'DELETE'
});
if (!response.ok) {
throw new Error(`HTTP ошибка! статус: ${response.status}`);
}
// Очищаем список графов
this.graphs = [];
this.currentPage = 1;
this.renderGraphs();
// Уведомляем об очистке
this.props.eventBus.emit('all-chats-cleared');
} catch (error) {
console.error('Ошибка очистки всех чатов:', error);
}
}
async handleDeleteGraph(graphId: string): Promise<void> {
@ -138,6 +318,13 @@ export class HistoryPanel {
// Обновляем список графов
this.graphs = this.graphs.filter((graph) => graph.id !== graphId);
// Корректируем текущую страницу если нужно
const totalPages = Math.ceil(this.graphs.length / this.itemsPerPage);
if (this.currentPage > totalPages && totalPages > 0) {
this.currentPage = totalPages;
}
this.renderGraphs();
this.contextMenu.style.display = 'none';
} catch (error) {
@ -147,6 +334,7 @@ export class HistoryPanel {
updateGraphs(newGraphs: any[]): void {
this.graphs = newGraphs;
this.currentPage = 1;
this.renderGraphs();
}

View File

@ -960,6 +960,150 @@
z-index: 100; /* Чтобы быть над всем */
}
/* ----------------------------------------------- History Panel ------------------------------------------------------------ */
.llm-agent-history-panel { /* ДОБАВЛЕНО */
position: relative;
height: 100%;
display: flex;
flex-direction: column;
}
.llm-agent-history-panel .history-header { /* ДОБАВЛЕНО */
padding: 10px;
border-bottom: 1px solid var(--background-modifier-border);
}
.llm-agent-history-panel .history-header h3 { /* ДОБАВЛЕНО */
margin: 0 0 10px 0;
}
.llm-agent-history-panel .history-header-buttons { /* ДОБАВЛЕНО */
display: flex;
gap: 8px;
}
.llm-agent-history-panel .new-chat-button, /* ДОБАВЛЕНО */
.llm-agent-history-panel .clear-all-button { /* ДОБАВЛЕНО */
flex: 1;
padding: 8px 12px;
border: none;
border-radius: 6px;
cursor: pointer;
font-size: 12px;
transition: all 0.2s;
}
.llm-agent-history-panel .new-chat-button { /* ДОБАВЛЕНО */
background-color: var(--button-primary-background);
color: var(--button-primary-text-color);
}
.llm-agent-history-panel .new-chat-button:hover { /* ДОБАВЛЕНО */
background-color: var(--button-primary-hover-background);
}
.llm-agent-history-panel .clear-all-button { /* ДОБАВЛЕНО */
background-color: var(--button-secondary-background);
color: var(--button-secondary-text-color);
}
.llm-agent-history-panel .clear-all-button:hover { /* ДОБАВЛЕНО */
background-color: var(--button-secondary-hover-background);
}
.llm-agent-history-panel .chat-list { /* ДОБАВЛЕНО */
overflow-y: auto;
flex: 1;
}
.llm-agent-history-panel .chat-list-item { /* ДОБАВЛЕНО */
padding: 10px;
cursor: pointer;
border-bottom: 1px solid var(--background-modifier-border);
transition: background-color 0.2s;
}
.llm-agent-history-panel .chat-list-item:hover { /* ДОБАВЛЕНО */
background-color: var(--background-modifier-hover);
}
.llm-agent-history-panel .chat-item-content { /* ДОБАВЛЕНО */
display: flex;
justify-content: space-between;
align-items: center;
}
.llm-agent-history-panel .chat-item-text { /* ДОБАВЛЕНО */
flex: 1;
overflow: hidden;
}
.llm-agent-history-panel .truncate { /* ДОБАВЛЕНО */
display: block;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.llm-agent-history-panel .chat-item-options-button { /* ДОБАВЛЕНО */
background: none;
border: none;
cursor: pointer;
font-size: 16px;
padding: 4px 8px;
color: var(--text-muted);
border-radius: 4px;
transition: all 0.2s;
}
.llm-agent-history-panel .chat-item-options-button:hover { /* ДОБАВЛЕНО */
background-color: var(--background-modifier-hover);
color: var(--text-normal);
}
.llm-agent-history-panel .pagination-container { /* ДОБАВЛЕНО */
padding: 10px;
border-top: 1px solid var(--background-modifier-border);
}
.llm-agent-history-panel .pagination-buttons { /* ДОБАВЛЕНО */
display: flex;
justify-content: center;
align-items: center;
gap: 4px;
flex-wrap: wrap;
}
.llm-agent-history-panel .pagination-button { /* ДОБАВЛЕНО */
min-width: 32px;
height: 32px;
padding: 4px 8px;
border: 1px solid var(--background-modifier-border);
background-color: var(--background-secondary);
color: var(--text-normal);
border-radius: 4px;
cursor: pointer;
font-size: 12px;
transition: all 0.2s;
}
.llm-agent-history-panel .pagination-button:hover { /* ДОБАВЛЕНО */
background-color: var(--background-modifier-hover);
border-color: var(--interactive-accent);
}
.llm-agent-history-panel .pagination-button.active { /* ДОБАВЛЕНО */
background-color: var(--interactive-accent);
color: var(--text-on-accent);
border-color: var(--interactive-accent);
}
.llm-agent-history-panel .pagination-ellipsis { /* ДОБАВЛЕНО */
padding: 4px 8px;
color: var(--text-muted);
}
/* ----------------------------------------------- Dialog Styles ------------------------------------------------------------ */
/* Общие стили для сообщения в диалоге */

View File

@ -103,10 +103,14 @@ export class GraphView extends ItemView {
`;
// Инициализируем панель истории
this.historyPanel = new HistoryPanel(this.historyContainer, {
this.historyPanel = new HistoryPanel(
this.historyContainer,
{
graphs: this.graphsList,
eventBus: this.plugin.eventBus
});
},
this.plugin
);
// Создаем кнопки переключения и глобальные кнопки действий
this.addToggleButton();
@ -324,9 +328,7 @@ export class GraphView extends ItemView {
const data = await response.json();
// Чтобы не раздувать отправляемые эндпоинтом данные
const nodesWithGraphId = data.graph_nodes ?
data.graph_nodes.map((node: any) => ({ ...node, graph_id: graphId })) :
[];
const nodesWithGraphId = data.graph_nodes ? data.graph_nodes.map((node: any) => ({ ...node, graph_id: graphId })) : [];
this.graphData = {
nodes: nodesWithGraphId,

File diff suppressed because one or more lines are too long