Add graph options with custom system prompt, make more correct handling for New Chat button

This commit is contained in:
dimitrievgs 2025-10-19 16:05:05 +03:00
parent fd6e7d12c5
commit 1a7c1de507
8 changed files with 839 additions and 388 deletions

762
main.js

File diff suppressed because one or more lines are too long

View File

@ -4,7 +4,7 @@ import { GraphView, GRAPH_VIEW_TYPE } from './src/views/GraphView';
import { ChatView, CHAT_VIEW_TYPE } from './src/views/ChatView';
import { WebSocketService } from './src/utils/WebSocketService';
import 'src/css/styles.css'; // Путь к твоему новому CSS файлу
import 'src/css/styles.css';
// ----------------------------------------------- Settings ---------------------------------------------------------------

25
package-lock.json generated
View File

@ -9,7 +9,9 @@
"version": "1.0.0",
"license": "MIT",
"dependencies": {
"socket.io-client": "^4.8.1"
"@types/uuid": "^11.0.0",
"socket.io-client": "^4.8.1",
"uuid": "^13.0.0"
},
"devDependencies": {
"@types/dagre": "^0.7.52",
@ -980,6 +982,15 @@
"@types/estree": "*"
}
},
"node_modules/@types/uuid": {
"version": "11.0.0",
"resolved": "https://registry.npmjs.org/@types/uuid/-/uuid-11.0.0.tgz",
"integrity": "sha512-HVyk8nj2m+jcFRNazzqyVKiZezyhDKrGUA3jlEcg/nZ6Ms+qHwocba1Y/AaVaznJTAM9xpdFSh+ptbNrhOGvZA==",
"deprecated": "This is a stub types definition. uuid provides its own type definitions, so you do not need this installed.",
"dependencies": {
"uuid": "*"
}
},
"node_modules/@typescript-eslint/eslint-plugin": {
"version": "5.29.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.29.0.tgz",
@ -2899,6 +2910,18 @@
"react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
}
},
"node_modules/uuid": {
"version": "13.0.0",
"resolved": "https://registry.npmjs.org/uuid/-/uuid-13.0.0.tgz",
"integrity": "sha512-XQegIaBTVUjSHliKqcnFqYypAd4S+WCYt5NIeRs6w/UAry7z8Y9j5ZwRRL4kzq9U3sD6v+85er9FvkEaBpji2w==",
"funding": [
"https://github.com/sponsors/broofa",
"https://github.com/sponsors/ctavan"
],
"bin": {
"uuid": "dist-node/bin/uuid"
}
},
"node_modules/w3c-keyname": {
"version": "2.2.8",
"resolved": "https://registry.npmjs.org/w3c-keyname/-/w3c-keyname-2.2.8.tgz",

View File

@ -29,6 +29,8 @@
"typescript": "^5.4.5"
},
"dependencies": {
"socket.io-client": "^4.8.1"
"@types/uuid": "^11.0.0",
"socket.io-client": "^4.8.1",
"uuid": "^13.0.0"
}
}

View File

@ -23,6 +23,7 @@ export class ChatPanel {
availableModels?: string[];
selectedModel?: string;
plugin: LLMAgentPlugin;
currentGraphCustomSystemPrompt: string | null;
};
chatHistory: ChatHistoryItem[];
messageInput: HTMLDivElement;
@ -31,6 +32,7 @@ export class ChatPanel {
modelSelectButton: HTMLButtonElement;
modelDropdown: HTMLDivElement;
selectedModel: string;
customPromptIndicator: HTMLSpanElement | null = null;
// ----------------------------------------------- Reference System Fields ---------------------------------------------------------------
private autocompleteManager: AutocompleteManager;
@ -47,7 +49,8 @@ export class ChatPanel {
onSendMessage: (message: string, selectedModel?: string) => void;
availableModels?: string[];
selectedModel?: string;
plugin: LLMAgentPlugin; // Обновленный тип
plugin: LLMAgentPlugin;
currentGraphCustomSystemPrompt: string | null;
}
) {
this.container = container;
@ -321,6 +324,10 @@ export class ChatPanel {
</svg>
</button>
<div class="model-dropdown" id="model-dropdown" style="display: none;"></div>
<span class="custom-prompt-indicator" id="custom-prompt-indicator" style="display: none;"
data-tooltip-position="top" aria-label="Используется пользовательский системный промпт для этого графа">
</span>
</div>
</div>
<div class="control-buttons-right">
@ -339,12 +346,14 @@ export class ChatPanel {
this.suggestionsContainer = this.container.querySelector('#suggestions') as HTMLDivElement;
this.modelSelectButton = this.container.querySelector('#model-select-button') as HTMLButtonElement;
this.modelDropdown = this.container.querySelector('#model-dropdown') as HTMLDivElement;
this.customPromptIndicator = this.container.querySelector('#custom-prompt-indicator') as HTMLSpanElement;
this.setupEventListeners();
this.setupReferenceSystem();
this.renderHistory();
this.updateModelDropdown();
this.updatePlaceholder();
this.updateCustomSystemPromptStatus(this.props.currentGraphCustomSystemPrompt); // Инициализируем статус
}
// ----------------------------------------------- Reference System Setup ---------------------------------------------------------------
@ -868,26 +877,41 @@ export class ChatPanel {
});
// Кнопка "Наверх сообщения"
const scrollToTopButton = toolsetContainer.createEl('button', {
cls: 'text-token-text-secondary hover:bg-token-bg-secondary rounded-lg ml-1',
attr: {
'aria-label': 'Перейти к началу сообщения',
'data-testid': 'scroll-to-message-top-button',
'data-state': 'closed'
}
});
const scrollToTopIconContainer = scrollToTopButton.createSpan({ cls: 'flex items-center justify-center touch:w-10 h-8 w-8' });
setIcon(scrollToTopIconContainer, 'arrow-up'); // Иконка стрелки вверх
scrollToTopButton.addEventListener('click', () => {
// Прокручиваем родительский контейнер (view-content) к началу этого сообщения
const scrollContainer = this.container.closest('.view-content') as HTMLElement;
if (scrollContainer) {
scrollContainer.scrollTop = messageElement.offsetTop;
}
});
const scrollToTopButton = toolsetContainer.createEl('button', {
cls: 'text-token-text-secondary hover:bg-token-bg-secondary rounded-lg ml-1',
attr: {
'aria-label': 'Перейти к началу сообщения',
'data-testid': 'scroll-to-message-top-button',
'data-state': 'closed'
}
});
const scrollToTopIconContainer = scrollToTopButton.createSpan({ cls: 'flex items-center justify-center touch:w-10 h-8 w-8' });
setIcon(scrollToTopIconContainer, 'arrow-up'); // Иконка стрелки вверх
scrollToTopButton.addEventListener('click', () => {
// Прокручиваем родительский контейнер (view-content) к началу этого сообщения
const scrollContainer = this.container.closest('.view-content') as HTMLElement;
if (scrollContainer) {
scrollContainer.scrollTop = messageElement.offsetTop;
}
});
}
private copyMessageContent(content: string): Promise<void> {
return navigator.clipboard.writeText(content);
}
updateCustomSystemPromptStatus(customSystemPrompt: string | null): void {
if (this.customPromptIndicator) {
if (customSystemPrompt) {
this.customPromptIndicator.style.display = 'inline-flex';
this.customPromptIndicator.setAttribute(
'aria-label',
`Используется пользовательский системный промпт: ${customSystemPrompt}`
);
} else {
this.customPromptIndicator.style.display = 'none';
this.customPromptIndicator.removeAttribute('aria-label');
}
}
}
}

View File

@ -0,0 +1,92 @@
// ----------------------------------------------- Obsidian Imports ----------------------------------------------------------
import { App, Modal, Setting } from 'obsidian';
// ----------------------------------------------- Interfaces ----------------------------------------------------------------
/**
* Интерфейс для свойств, передаваемых в модальное окно GraphSettingsModal.
*/
interface GraphSettingsModalProps {
app: App;
graphId: string;
graphTitle: string;
initialCustomSystemPrompt: string | null;
onSave: (graphId: string, customSystemPrompt: string | null) => Promise<void>;
}
// ----------------------------------------------- GraphSettingsModal --------------------------------------------------------
/**
* Модальное окно для настройки специфического системного промпта для конкретного графа.
*/
export class GraphSettingsModal extends Modal {
private graphId: string;
private graphTitle: string;
private customSystemPrompt: string | null;
private initialCustomSystemPrompt: string | null;
private onSave: (graphId: string, customSystemPrompt: string | null) => Promise<void>;
constructor(app: App, props: GraphSettingsModalProps) {
super(app);
this.graphId = props.graphId;
this.graphTitle = props.graphTitle;
this.customSystemPrompt = props.initialCustomSystemPrompt;
this.initialCustomSystemPrompt = props.initialCustomSystemPrompt;
this.onSave = props.onSave;
this.titleEl.setText(`Настройки графа: ${this.graphTitle}`);
this.modalEl.addClass('llm-agent-graph-settings-modal');
}
onOpen(): void {
const { contentEl } = this;
contentEl.empty();
// ----------------------------------------------- System Prompt Setting ------------------------------------------------
new Setting(contentEl)
.setName('Пользовательский системный промпт')
.setDesc('Этот промпт будет использоваться для данного графа вместо глобального системного промпта. Оставьте пустым, чтобы использовать глобальный промпт.')
.addTextArea(textArea => {
textArea
.setPlaceholder('Введите пользовательский системный промпт для этого графа...')
.setValue(this.customSystemPrompt || '')
.onChange(value => {
this.customSystemPrompt = value.trim() === '' ? null : value.trim();
});
textArea.inputEl.rows = 6;
textArea.inputEl.style.minWidth = '400px';
textArea.inputEl.style.width = '100%';
});
// ----------------------------------------------- Action Buttons -------------------------------------------------------
new Setting(contentEl)
.addButton(button => {
button
.setButtonText('Сохранить')
.setCta()
.onClick(async () => {
await this.onSave(this.graphId, this.customSystemPrompt);
this.close();
});
})
.addButton(button => {
button
.setButtonText('Сбросить на глобальный')
.setClass('mod-warning')
.onClick(async () => {
await this.onSave(this.graphId, null); // Отправляем null для сброса
this.close();
});
})
.addButton(button => {
button
.setButtonText('Отмена')
.onClick(() => {
this.close();
});
});
}
onClose(): void {
const { contentEl } = this;
contentEl.empty();
}
}

View File

@ -19,6 +19,7 @@ export class ChatView extends ItemView {
chatContainer: HTMLDivElement;
chatPanel: ChatPanel | null;
availableModels: string[];
currentGraphCustomSystemPrompt: string | null;
constructor(leaf: WorkspaceLeaf, plugin: LLMAgentPlugin) {
super(leaf);
@ -28,6 +29,7 @@ export class ChatView extends ItemView {
this.currentNode = null;
this.chatPanel = null;
this.availableModels = [];
this.currentGraphCustomSystemPrompt = null;
}
getViewType(): string {
@ -59,16 +61,17 @@ export class ChatView extends ItemView {
onSendMessage: this.handleSendMessage.bind(this),
availableModels: this.availableModels,
selectedModel: this.plugin.settings.defaultModel || 'gemini-2.5-flash',
plugin: this.plugin // Добавляем передачу plugin
plugin: this.plugin,
currentGraphCustomSystemPrompt: this.currentGraphCustomSystemPrompt
});
// Остальной код без изменений...
this.plugin.eventBus.on('node-selected', this.handleNodeSelected.bind(this));
this.plugin.eventBus.on('new-chat', this.handleNewChat.bind(this));
this.plugin.eventBus.on('graph-selected', this.handleGraphSelected.bind(this));
this.plugin.eventBus.on('regenerate-message', this.handleRegenerateMessage.bind(this)); // Добавлено
this.plugin.eventBus.on('regenerate-message', this.handleRegenerateMessage.bind(this));
this.plugin.eventBus.on('ws-node-type-changed', this.handleNodeTypeChanged.bind(this));
this.plugin.eventBus.on('start-new-independent-chat', this.handleStartNewIndependentChat.bind(this));
this.plugin.eventBus.on('graph-custom-prompt-updated', this.handleGraphCustomPromptUpdated.bind(this));
}
async onClose(): Promise<void> {
@ -82,6 +85,7 @@ export class ChatView extends ItemView {
this.plugin.eventBus.off('regenerate-message', this.handleRegenerateMessage);
this.plugin.eventBus.off('ws-node-type-changed', this.handleNodeTypeChanged);
this.plugin.eventBus.off('start-new-independent-chat', this.handleStartNewIndependentChat);
this.plugin.eventBus.off('graph-custom-prompt-updated', this.handleGraphCustomPromptUpdated);
}
// ----------------------------------------------- API Methods ---------------------------------------------------------------
@ -164,6 +168,15 @@ export class ChatView extends ItemView {
async handleSendMessage(message: string, selectedModel?: string): Promise<void> {
try {
if (!this.selectedGraphId) {
new Notice('Граф не выбран или не инициализирован. Пожалуйста, начните новый чат или выберите существующий.', 3000);
console.warn('selectedGraphId is null in handleSendMessage. Current state:', {
selectedGraphId: this.selectedGraphId,
currentNode: this.currentNode
});
return;
}
// Шаг 1: Создаём узел пользователя
const sendResponse = await fetch(`${this.plugin.settings.apiBaseUrl}/chat/send`, {
method: 'POST',
@ -180,7 +193,7 @@ export class ChatView extends ItemView {
}
const sendResult = await sendResponse.json();
this.selectedGraphId = sendResult.graph_id;
// this.selectedGraphId = sendResult.graph_id;
const userNodeId = sendResult.node_id;
// Обновляем граф с новым узлом пользователя
@ -191,14 +204,15 @@ export class ChatView extends ItemView {
// Шаг 2: Запускаем стриминг ответа
if (this.selectedGraphId) {
// Проверка на null
// Передаем null, так как это не регенерация, а новое сообщение
await this.streamLLMResponse(this.selectedGraphId, userNodeId, selectedModel, null);
const systemPromptToUse = this.currentGraphCustomSystemPrompt !== null ?
this.currentGraphCustomSystemPrompt :
this.plugin.settings.systemPrompt;
await this.streamLLMResponse(this.selectedGraphId, userNodeId, selectedModel, null, systemPromptToUse);
} else {
throw new Error('Graph ID не определён после отправки сообщения');
}
} catch (error: any) {
// Указываем тип any для error
console.error('Ошибка отправки сообщения:', error);
new Notice(`Ошибка: ${error?.message || error}`, 5000);
}
@ -223,45 +237,45 @@ export class ChatView extends ItemView {
async handleNewChat(): Promise<void> {
this.chatHistory = [];
this.currentNode = null;
this.selectedGraphId = null;
//this.selectedGraphId = null;
//this.currentGraphCustomSystemPrompt = null;
this.currentGraphCustomSystemPrompt = null;
if (this.chatPanel) {
await this.chatPanel.updateHistory(this.chatHistory);
this.chatPanel.updateCustomSystemPromptStatus(null);
}
}
/**
* Обрабатывает выбор графа из `GraphView`. Устанавливает выбранный граф и обновляет
* историю чата, загружая сообщения, относящиеся к этому графу.
* @param {{ graphId: string; currentNode: string | null }} data - Объект, содержащий ID выбранного графа и текущий узел.
* @returns {Promise<void>}
*/
async handleGraphSelected(data: { graphId: string; currentNode: string | null }): Promise<void> {
this.selectedGraphId = data.graphId;
this.currentNode = data.currentNode; // Может быть пустым, но это не проблема
this.currentNode = data.currentNode;
// Важно: custom_system_prompt будет обновлен через 'graph-custom-prompt-updated' событие,
// которое генерируется в GraphView после fetchGraphData.
// Здесь мы пока не трогаем this.currentGraphCustomSystemPrompt напрямую,
// но ожидаем, что GraphView его обновит.
await this.fetchMessagesFromRootToNode(data.currentNode);
}
/**
* Стримит ответ LLM.
* @param {string} graphId - ID графа.
* @param {string} userNodeId - ID узла пользователя.
* @param {string} selectedModel - Выбранная модель.
* @param {string | null} existingAssistantNodeId - Существующий ID узла ассистента для повторного использования (для регенерации).
*/
async streamLLMResponse(
graphId: string,
userNodeId: string,
selectedModel?: string,
existingAssistantNodeId: string | null = null
existingAssistantNodeId: string | null = null,
systemPrompt: string | null = null
): Promise<void> {
const body = {
graph_id: graphId,
user_node_id: userNodeId,
system_prompt: this.plugin.settings.systemPrompt,
system_prompt: systemPrompt,
model: selectedModel || this.plugin.settings.defaultModel
};
// Если есть существующий ID узла ассистента, добавляем его в тело запроса
if (existingAssistantNodeId) {
Object.assign(body, { existing_assistant_node_id: existingAssistantNodeId });
}
@ -269,7 +283,7 @@ export class ChatView extends ItemView {
const response = await fetch(`${this.plugin.settings.apiBaseUrl}/chat/stream`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(body) // Используем модифицированное тело запроса
body: JSON.stringify(body)
});
if (!response.ok) {
@ -299,7 +313,6 @@ export class ChatView extends ItemView {
if (data.type === 'node_created') {
assistantNodeId = data.node_id;
// Если узел переиспользуется, нужно очистить его содержимое в UI
if (data.reused) {
this.clearStreamingMessage(assistantNodeId);
}
@ -314,13 +327,12 @@ export class ChatView extends ItemView {
console.error('Ошибка:', data.error);
if (assistantNodeId) {
this.markNodeAsError(assistantNodeId, data.error);
// Эмиттируем событие, чтобы GraphView обновил отображение узла с ошибкой
this.plugin.eventBus.emit('graph-selected', {
graphId: graphId,
currentNode: assistantNodeId
});
}
break; // Прекращаем обработку при ошибке
break;
} else if (data.type === 'done') {
this.currentNode = assistantNodeId;
this.plugin.eventBus.emit('graph-selected', {
@ -402,15 +414,20 @@ export class ChatView extends ItemView {
}
async handleRegenerateMessage(data: { graphId: string; nodeId: string; model: string }): Promise<void> {
// НОВОЕ: Добавляем 'model' в тип данных
try {
// Определяем системный промпт для отправки: кастомный для графа, если есть, иначе глобальный
const systemPromptToUse = this.currentGraphCustomSystemPrompt !== null ?
this.currentGraphCustomSystemPrompt :
this.plugin.settings.systemPrompt;
const response = await fetch(`${this.plugin.settings.apiBaseUrl}/chat/regenerate`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
graph_id: data.graphId,
node_id: data.nodeId,
model: data.model // НОВОЕ: Передаем выбранную модель для регенерации
model: data.model,
system_prompt: systemPromptToUse
})
});
@ -419,14 +436,13 @@ export class ChatView extends ItemView {
}
const result = await response.json();
// НОВОЕ: Получаем ID существующего узла ассистента и ID пользовательского узла
const existingAssistantNodeId = result.existing_assistant_node_id;
const userNodeId = result.user_node_id;
const modelToUse = result.model;
const systemPromptFromBackend = result.system_prompt;
// Запускаем стриминг, передавая ID существующего узла для повторного использования
if (data.graphId) {
await this.streamLLMResponse(data.graphId, userNodeId, modelToUse, existingAssistantNodeId);
await this.streamLLMResponse(data.graphId, userNodeId, modelToUse, existingAssistantNodeId, systemPromptFromBackend);
}
} catch (error: any) {
console.error('Ошибка регенерации:', error);
@ -434,16 +450,17 @@ export class ChatView extends ItemView {
}
}
/**
* Обрабатывает событие начала нового независимого чата.
* Сбрасывает текущее состояние ChatView, но не влияет на GraphView.
*/
/**
* Обрабатывает событие начала нового независимого чата.
* Сбрасывает текущее состояние ChatView, но не влияет на GraphView.
*/
async handleStartNewIndependentChat(): Promise<void> {
this.chatHistory = [];
this.currentNode = null;
// Оставляем this.selectedGraphId без изменений
if (this.chatPanel) {
await this.chatPanel.updateHistory(this.chatHistory);
this.chatPanel.updateCustomSystemPromptStatus(null);
}
// Опционально: можно уведомить пользователя о начале нового чата
new Notice('Начат новый чат. Текущий граф не изменен.', 2000);
@ -461,4 +478,19 @@ export class ChatView extends ItemView {
}
}
}
}
handleGraphCustomPromptUpdated(data: { graph_id: string; custom_system_prompt: string | null }): void {
if (this.selectedGraphId === data.graph_id) {
this.currentGraphCustomSystemPrompt = data.custom_system_prompt;
if (this.chatPanel) {
this.chatPanel.updateCustomSystemPromptStatus(this.currentGraphCustomSystemPrompt);
}
} else if (this.selectedGraphId === null && data.graph_id === null) {
// Сброс промпта, если граф не выбран
this.currentGraphCustomSystemPrompt = null;
if (this.chatPanel) {
this.chatPanel.updateCustomSystemPromptStatus(null);
}
}
}
}

View File

@ -7,11 +7,13 @@
import { ItemView, WorkspaceLeaf, Notice } from 'obsidian';
// ----------------------------------------------- Plugin Imports ------------------------------------------------------------
import LLMAgentPlugin from 'main'; // Предполагаем, что main.ts экспортирует LLMAgentPlugin по умолчанию
import * as React from 'react';
import * as ReactDOM from 'react-dom/client';
import { HistoryPanel } from '../components/HistoryPanel'; // Предполагаем, что HistoryPanel это JS-класс
import { GraphPanelComponent } from '../components/GraphPanel'; // Импортируем React-компонент и ReactDOM
import * as React from 'react';
import * as ReactDOM from 'react-dom/client'; // <-- ИСПРАВЛЕНО: Импортируем 'react-dom/client' для createRoot
import { Dialog } from 'src/utils/Dialog';
import { GraphSettingsModal } from '../modals/GraphSettingsModal';
export const GRAPH_VIEW_TYPE = 'llm-agent-graph-view';
@ -25,6 +27,7 @@ export class GraphView extends ItemView {
selectedGraphId: string | null;
selectedNodeIds: Set<string>; // Добавляем Set для хранения ID выделенных узлов
selectedGraphTitle: string | null;
currentGraphCustomSystemPrompt: string | null;
// UI Elements
historyPanel: HistoryPanel | null;
@ -38,6 +41,7 @@ export class GraphView extends ItemView {
copySelectedButton: HTMLButtonElement | null = null;
deleteSelectedButton: HTMLButtonElement | null = null;
newIndependentChatActionButton: HTMLButtonElement | null = null;
graphSettingsButton: HTMLButtonElement | null = null;
constructor(leaf: WorkspaceLeaf, plugin: LLMAgentPlugin) {
super(leaf);
@ -49,8 +53,9 @@ 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>();
this.selectedGraphTitle = null;
this.currentGraphCustomSystemPrompt = null;
}
/**
@ -210,7 +215,6 @@ export class GraphView extends ItemView {
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': 'Копировать выделенные узлы' },
@ -218,7 +222,6 @@ export class GraphView extends ItemView {
});
this.copySelectedButton.addEventListener('click', this.handleCopySelectedNodes.bind(this));
// Кнопка "Удалить выделенные"
this.deleteSelectedButton = toolbar.createEl('button', {
cls: 'global-action-button',
attr: { 'aria-label': 'Удалить выделенные узлы', 'data-tooltip': 'Удалить выделенные узлы' },
@ -226,20 +229,79 @@ export class GraphView extends ItemView {
});
this.deleteSelectedButton.addEventListener('click', this.handleDeleteSelectedNodes.bind(this));
// Кнопка "Новый чат" (сохраняя текущий граф)
this.newIndependentChatActionButton = toolbar.createEl('button', {
cls: 'global-action-button',
attr: { 'aria-label': 'Новый независимый чат', 'data-tooltip': 'Начать новую беседу, формируя отдельный, независимый подграф' },
text: '✨' // Или 'Новый чат'
});
this.newIndependentChatActionButton.addEventListener('click', () => {
// Эмитируем новое событие, которое будет слушать ТОЛЬКО ChatView
this.plugin.eventBus.emit('start-new-independent-chat');
});
this.newIndependentChatActionButton = toolbar.createEl('button', {
cls: 'global-action-button',
attr: { 'aria-label': 'Новый независимый чат', 'data-tooltip': 'Начать новую беседу, формируя отдельный, независимый подграф' },
text: '✨' // Или 'Новый чат'
});
this.newIndependentChatActionButton.addEventListener('click', () => {
// Эмитируем новое событие, которое будет слушать ТОЛЬКО ChatView
this.plugin.eventBus.emit('start-new-independent-chat');
});
this.graphSettingsButton = toolbar.createEl('button', {
cls: 'global-action-button',
attr: { 'aria-label': 'Настройки графа', 'data-tooltip': 'Настроить системный промпт для текущего графа' },
text: '⚙️' // Иконка шестеренки
});
this.graphSettingsButton.addEventListener('click', this.handleGraphSettingsClick.bind(this));
// Деактивируем кнопку по умолчанию, пока граф не выбран
this.graphSettingsButton.setAttr('disabled', 'true');
}
// ----------------------------------------------- Graph Panel Integration --------------------------------------------------
/**
* Обработчик клика по кнопке "Настройки графа".
*/
handleGraphSettingsClick(): void {
if (!this.selectedGraphId || !this.selectedGraphTitle) {
new Notice('Сначала выберите граф для настройки.', 3000);
return;
}
new GraphSettingsModal(this.app, {
app: this.app,
graphId: this.selectedGraphId,
graphTitle: this.selectedGraphTitle,
initialCustomSystemPrompt: this.currentGraphCustomSystemPrompt,
onSave: this.saveGraphCustomSystemPrompt.bind(this)
}).open();
}
/**
* Сохраняет пользовательский системный промпт для текущего графа.
* @param {string} graphId - ID графа.
* @param {string | null} customSystemPrompt - Новый системный промпт или null для сброса.
* @returns {Promise<void>}
*/
async saveGraphCustomSystemPrompt(graphId: string, customSystemPrompt: string | null): Promise<void> {
if (!graphId) {
new Notice('Граф не выбран для сохранения настроек.', 3000);
return;
}
try {
const response = await fetch(`${this.plugin.settings.apiBaseUrl}/graphs/${graphId}/settings`, {
method: 'PATCH',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ custom_system_prompt: customSystemPrompt })
});
if (!response.ok) {
const errorText = await response.text();
throw new Error(`Ошибка сохранения системного промпта: ${response.status} - ${errorText}`);
}
new Notice('Системный промпт графа успешно обновлен.', 2000);
// Обновляем данные графа, чтобы получить актуальный промпт и перерисовать UI
await this.fetchGraphData(graphId);
} catch (error) {
console.error(`Ошибка при сохранении системного промпта для графа ${graphId}:`, error);
new Notice(`Ошибка при сохранении системного промпта: ${error.message}`, 5000);
}
}
/**
* Инициализирует GraphPanelComponent (React компонент) в graphContainer.
* Использует ReactDOM.createRoot для React 18.
@ -278,9 +340,9 @@ 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 выделенных узлов
onNodeSelectionChange: this.handleNodeSelectionChange.bind(this),
onNodeDelete: this.handleDeleteNode.bind(this),
selectedNodeIds: this.selectedNodeIds,
activeNodeId: this.currentNode,
app: this.plugin.app
})
@ -329,7 +391,11 @@ export class GraphView extends ItemView {
this.selectedGraphTitle = null; // Сброс заголовка
this.updateTitle(); // Обновляем заголовок
this.renderGraphPanel();
this.plugin.eventBus.emit('chat-history-updated', { graphId: null, messages: [], current_node_id: null });
// Нет подписчиков: this.plugin.eventBus.emit('chat-history-updated', { graphId: null, messages: [], current_node_id: null });
this.plugin.eventBus.emit('graph-custom-prompt-updated', { graph_id: null, custom_system_prompt: null }); // Уведомляем ChatView о сбросе
if (this.graphSettingsButton) {
this.graphSettingsButton.setAttr('disabled', 'true');
}
return;
}
@ -351,22 +417,36 @@ export class GraphView extends ItemView {
// Устанавливаем заголовок графа
this.selectedGraphTitle = data.title || data.first_message || `Граф ${graphId.substring(0, 8)}...`;
this.updateTitle(); // Обновляем заголовок
this.updateTitle();
this.currentGraphCustomSystemPrompt = data.custom_system_prompt || null;
if (this.graphSettingsButton) {
this.graphSettingsButton.setAttr('disabled', null); // Активируем
}
// Перерендериваем граф
this.renderGraphPanel();
// Также обновляем ChatView с полученными сообщениями и текущим узлом
this.plugin.eventBus.emit('chat-history-updated', {
/* Нет подписчиков: this.plugin.eventBus.emit('chat-history-updated', {
graphId: graphId,
messages: data.messages || [],
current_node_id: data.current_node_id // Передаем актуальный current_node_id
});*/
this.plugin.eventBus.emit('graph-custom-prompt-updated', {
graph_id: graphId,
custom_system_prompt: this.currentGraphCustomSystemPrompt
});
} catch (error) {
console.error('Ошибка загрузки данных графа:', error);
this.selectedGraphTitle = null;
this.currentGraphCustomSystemPrompt = null; // Сброс кастомного промпта при ошибке
this.updateTitle(); // Обновляем заголовок
// Optionally: show a notice to the user
// new Notice(`Ошибка загрузки данных графа: ${error.message}`);
if (this.graphSettingsButton) {
this.graphSettingsButton.setAttr('disabled', 'true');
}
this.plugin.eventBus.emit('graph-custom-prompt-updated', { graph_id: graphId, custom_system_prompt: null });
}
}
@ -440,33 +520,63 @@ export class GraphView extends ItemView {
* Обработчик начала нового чата.
* Очищает текущие данные графа и перерендеривает его.
*/
handleNewChat(): void {
async handleNewChat(): Promise<void> {
this.graphData = { nodes: [], edges: [] };
this.currentNode = null;
this.selectedGraphId = null;
this.selectedNodeIds.clear(); // Очищаем выделения
this.selectedGraphTitle = null; // Сброс заголовка при новом чате
this.updateTitle(); // Обновляем заголовок
this.renderGraphPanel();
this.fetchGraphsList(); // Обновить список графов, чтобы показать новый пустой чат
this.plugin.eventBus.emit('chat-history-updated', { graphId: null, messages: [], current_node_id: null });
this.selectedNodeIds.clear();
this.selectedGraphTitle = null;
this.currentGraphCustomSystemPrompt = null;
try {
const response = await fetch(`${this.plugin.settings.apiBaseUrl}/graphs/new`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' }
});
if (!response.ok) {
throw new Error(`Ошибка при создании нового графа на бэкенде: ${response.status}`);
}
const result = await response.json();
const newGraphId = result.graph_id;
this.selectedGraphId = newGraphId;
this.selectedGraphTitle = 'Новый граф'; // Будет обновлен fetchGraphData, если бэкенд вернет title
this.plugin.eventBus.emit('graph-selected', { graphId: this.selectedGraphId, currentNode: null });
this.fetchGraphsList(); // Обновляем список графов в HistoryPanel
// Нет подписчиков: this.plugin.eventBus.emit('chat-history-updated', { graphId: this.selectedGraphId, messages: [], current_node_id: null });
this.plugin.eventBus.emit('graph-custom-prompt-updated', { graph_id: this.selectedGraphId, custom_system_prompt: null });
if (this.graphSettingsButton) {
this.graphSettingsButton.setAttr('disabled', null);
}
} catch (error: any) {
console.error('Ошибка при создании нового чата:', error);
new Notice(`Ошибка при создании нового чата: ${error?.message || error}`, 5000);
// Сброс состояния в случае ошибки, чтобы не было "фантомного" графа
this.selectedGraphId = null;
this.selectedGraphTitle = null;
this.updateTitle();
if (this.graphSettingsButton) {
this.graphSettingsButton.setAttr('disabled', 'true');
}
}
}
/**
* Обработчик выбора графа из панели истории.
* Устанавливает выбранный граф и загружает его данные.
* @param {{ graphId: string; currentNode: string | null }} data - Объект, содержащий ID выбранного графа и текущий узел.
* @returns {Promise<void>}
*/
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 - Данные об удаляемом узле.
*/
async handleDeleteNodeFromChat(data: { graphId: string; nodeId: string }): Promise<void> {
if (data.graphId === this.selectedGraphId) {