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 { ChatView, CHAT_VIEW_TYPE } from './src/views/ChatView';
import { WebSocketService } from './src/utils/WebSocketService'; import { WebSocketService } from './src/utils/WebSocketService';
import 'src/css/styles.css'; // Путь к твоему новому CSS файлу import 'src/css/styles.css';
// ----------------------------------------------- Settings --------------------------------------------------------------- // ----------------------------------------------- Settings ---------------------------------------------------------------

25
package-lock.json generated
View File

@ -9,7 +9,9 @@
"version": "1.0.0", "version": "1.0.0",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"socket.io-client": "^4.8.1" "@types/uuid": "^11.0.0",
"socket.io-client": "^4.8.1",
"uuid": "^13.0.0"
}, },
"devDependencies": { "devDependencies": {
"@types/dagre": "^0.7.52", "@types/dagre": "^0.7.52",
@ -980,6 +982,15 @@
"@types/estree": "*" "@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": { "node_modules/@typescript-eslint/eslint-plugin": {
"version": "5.29.0", "version": "5.29.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.29.0.tgz", "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" "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": { "node_modules/w3c-keyname": {
"version": "2.2.8", "version": "2.2.8",
"resolved": "https://registry.npmjs.org/w3c-keyname/-/w3c-keyname-2.2.8.tgz", "resolved": "https://registry.npmjs.org/w3c-keyname/-/w3c-keyname-2.2.8.tgz",

View File

@ -29,6 +29,8 @@
"typescript": "^5.4.5" "typescript": "^5.4.5"
}, },
"dependencies": { "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[]; availableModels?: string[];
selectedModel?: string; selectedModel?: string;
plugin: LLMAgentPlugin; plugin: LLMAgentPlugin;
currentGraphCustomSystemPrompt: string | null;
}; };
chatHistory: ChatHistoryItem[]; chatHistory: ChatHistoryItem[];
messageInput: HTMLDivElement; messageInput: HTMLDivElement;
@ -31,6 +32,7 @@ export class ChatPanel {
modelSelectButton: HTMLButtonElement; modelSelectButton: HTMLButtonElement;
modelDropdown: HTMLDivElement; modelDropdown: HTMLDivElement;
selectedModel: string; selectedModel: string;
customPromptIndicator: HTMLSpanElement | null = null;
// ----------------------------------------------- Reference System Fields --------------------------------------------------------------- // ----------------------------------------------- Reference System Fields ---------------------------------------------------------------
private autocompleteManager: AutocompleteManager; private autocompleteManager: AutocompleteManager;
@ -47,7 +49,8 @@ export class ChatPanel {
onSendMessage: (message: string, selectedModel?: string) => void; onSendMessage: (message: string, selectedModel?: string) => void;
availableModels?: string[]; availableModels?: string[];
selectedModel?: string; selectedModel?: string;
plugin: LLMAgentPlugin; // Обновленный тип plugin: LLMAgentPlugin;
currentGraphCustomSystemPrompt: string | null;
} }
) { ) {
this.container = container; this.container = container;
@ -321,6 +324,10 @@ export class ChatPanel {
</svg> </svg>
</button> </button>
<div class="model-dropdown" id="model-dropdown" style="display: none;"></div> <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> </div>
<div class="control-buttons-right"> <div class="control-buttons-right">
@ -339,12 +346,14 @@ export class ChatPanel {
this.suggestionsContainer = this.container.querySelector('#suggestions') as HTMLDivElement; this.suggestionsContainer = this.container.querySelector('#suggestions') as HTMLDivElement;
this.modelSelectButton = this.container.querySelector('#model-select-button') as HTMLButtonElement; this.modelSelectButton = this.container.querySelector('#model-select-button') as HTMLButtonElement;
this.modelDropdown = this.container.querySelector('#model-dropdown') as HTMLDivElement; this.modelDropdown = this.container.querySelector('#model-dropdown') as HTMLDivElement;
this.customPromptIndicator = this.container.querySelector('#custom-prompt-indicator') as HTMLSpanElement;
this.setupEventListeners(); this.setupEventListeners();
this.setupReferenceSystem(); this.setupReferenceSystem();
this.renderHistory(); this.renderHistory();
this.updateModelDropdown(); this.updateModelDropdown();
this.updatePlaceholder(); this.updatePlaceholder();
this.updateCustomSystemPromptStatus(this.props.currentGraphCustomSystemPrompt); // Инициализируем статус
} }
// ----------------------------------------------- Reference System Setup --------------------------------------------------------------- // ----------------------------------------------- Reference System Setup ---------------------------------------------------------------
@ -868,26 +877,41 @@ export class ChatPanel {
}); });
// Кнопка "Наверх сообщения" // Кнопка "Наверх сообщения"
const scrollToTopButton = toolsetContainer.createEl('button', { const scrollToTopButton = toolsetContainer.createEl('button', {
cls: 'text-token-text-secondary hover:bg-token-bg-secondary rounded-lg ml-1', cls: 'text-token-text-secondary hover:bg-token-bg-secondary rounded-lg ml-1',
attr: { attr: {
'aria-label': 'Перейти к началу сообщения', 'aria-label': 'Перейти к началу сообщения',
'data-testid': 'scroll-to-message-top-button', 'data-testid': 'scroll-to-message-top-button',
'data-state': 'closed' 'data-state': 'closed'
} }
}); });
const scrollToTopIconContainer = scrollToTopButton.createSpan({ cls: 'flex items-center justify-center touch:w-10 h-8 w-8' }); const scrollToTopIconContainer = scrollToTopButton.createSpan({ cls: 'flex items-center justify-center touch:w-10 h-8 w-8' });
setIcon(scrollToTopIconContainer, 'arrow-up'); // Иконка стрелки вверх setIcon(scrollToTopIconContainer, 'arrow-up'); // Иконка стрелки вверх
scrollToTopButton.addEventListener('click', () => { scrollToTopButton.addEventListener('click', () => {
// Прокручиваем родительский контейнер (view-content) к началу этого сообщения // Прокручиваем родительский контейнер (view-content) к началу этого сообщения
const scrollContainer = this.container.closest('.view-content') as HTMLElement; const scrollContainer = this.container.closest('.view-content') as HTMLElement;
if (scrollContainer) { if (scrollContainer) {
scrollContainer.scrollTop = messageElement.offsetTop; scrollContainer.scrollTop = messageElement.offsetTop;
} }
}); });
} }
private copyMessageContent(content: string): Promise<void> { private copyMessageContent(content: string): Promise<void> {
return navigator.clipboard.writeText(content); 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; chatContainer: HTMLDivElement;
chatPanel: ChatPanel | null; chatPanel: ChatPanel | null;
availableModels: string[]; availableModels: string[];
currentGraphCustomSystemPrompt: string | null;
constructor(leaf: WorkspaceLeaf, plugin: LLMAgentPlugin) { constructor(leaf: WorkspaceLeaf, plugin: LLMAgentPlugin) {
super(leaf); super(leaf);
@ -28,6 +29,7 @@ export class ChatView extends ItemView {
this.currentNode = null; this.currentNode = null;
this.chatPanel = null; this.chatPanel = null;
this.availableModels = []; this.availableModels = [];
this.currentGraphCustomSystemPrompt = null;
} }
getViewType(): string { getViewType(): string {
@ -59,16 +61,17 @@ export class ChatView extends ItemView {
onSendMessage: this.handleSendMessage.bind(this), onSendMessage: this.handleSendMessage.bind(this),
availableModels: this.availableModels, availableModels: this.availableModels,
selectedModel: this.plugin.settings.defaultModel || 'gemini-2.5-flash', 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('node-selected', this.handleNodeSelected.bind(this));
this.plugin.eventBus.on('new-chat', this.handleNewChat.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('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('ws-node-type-changed', this.handleNodeTypeChanged.bind(this));
this.plugin.eventBus.on('start-new-independent-chat', this.handleStartNewIndependentChat.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> { 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('regenerate-message', this.handleRegenerateMessage);
this.plugin.eventBus.off('ws-node-type-changed', this.handleNodeTypeChanged); this.plugin.eventBus.off('ws-node-type-changed', this.handleNodeTypeChanged);
this.plugin.eventBus.off('start-new-independent-chat', this.handleStartNewIndependentChat); this.plugin.eventBus.off('start-new-independent-chat', this.handleStartNewIndependentChat);
this.plugin.eventBus.off('graph-custom-prompt-updated', this.handleGraphCustomPromptUpdated);
} }
// ----------------------------------------------- API Methods --------------------------------------------------------------- // ----------------------------------------------- API Methods ---------------------------------------------------------------
@ -164,6 +168,15 @@ export class ChatView extends ItemView {
async handleSendMessage(message: string, selectedModel?: string): Promise<void> { async handleSendMessage(message: string, selectedModel?: string): Promise<void> {
try { try {
if (!this.selectedGraphId) {
new Notice('Граф не выбран или не инициализирован. Пожалуйста, начните новый чат или выберите существующий.', 3000);
console.warn('selectedGraphId is null in handleSendMessage. Current state:', {
selectedGraphId: this.selectedGraphId,
currentNode: this.currentNode
});
return;
}
// Шаг 1: Создаём узел пользователя // Шаг 1: Создаём узел пользователя
const sendResponse = await fetch(`${this.plugin.settings.apiBaseUrl}/chat/send`, { const sendResponse = await fetch(`${this.plugin.settings.apiBaseUrl}/chat/send`, {
method: 'POST', method: 'POST',
@ -180,7 +193,7 @@ export class ChatView extends ItemView {
} }
const sendResult = await sendResponse.json(); const sendResult = await sendResponse.json();
this.selectedGraphId = sendResult.graph_id; // this.selectedGraphId = sendResult.graph_id;
const userNodeId = sendResult.node_id; const userNodeId = sendResult.node_id;
// Обновляем граф с новым узлом пользователя // Обновляем граф с новым узлом пользователя
@ -191,14 +204,15 @@ export class ChatView extends ItemView {
// Шаг 2: Запускаем стриминг ответа // Шаг 2: Запускаем стриминг ответа
if (this.selectedGraphId) { if (this.selectedGraphId) {
// Проверка на null const systemPromptToUse = this.currentGraphCustomSystemPrompt !== null ?
// Передаем null, так как это не регенерация, а новое сообщение this.currentGraphCustomSystemPrompt :
await this.streamLLMResponse(this.selectedGraphId, userNodeId, selectedModel, null); this.plugin.settings.systemPrompt;
await this.streamLLMResponse(this.selectedGraphId, userNodeId, selectedModel, null, systemPromptToUse);
} else { } else {
throw new Error('Graph ID не определён после отправки сообщения'); throw new Error('Graph ID не определён после отправки сообщения');
} }
} catch (error: any) { } catch (error: any) {
// Указываем тип any для error
console.error('Ошибка отправки сообщения:', error); console.error('Ошибка отправки сообщения:', error);
new Notice(`Ошибка: ${error?.message || error}`, 5000); new Notice(`Ошибка: ${error?.message || error}`, 5000);
} }
@ -223,45 +237,45 @@ export class ChatView extends ItemView {
async handleNewChat(): Promise<void> { async handleNewChat(): Promise<void> {
this.chatHistory = []; this.chatHistory = [];
this.currentNode = null; this.currentNode = null;
this.selectedGraphId = null; //this.selectedGraphId = null;
//this.currentGraphCustomSystemPrompt = null;
this.currentGraphCustomSystemPrompt = null;
if (this.chatPanel) { if (this.chatPanel) {
await this.chatPanel.updateHistory(this.chatHistory); await this.chatPanel.updateHistory(this.chatHistory);
this.chatPanel.updateCustomSystemPromptStatus(null);
} }
} }
/** /**
* Обрабатывает выбор графа из `GraphView`. Устанавливает выбранный граф и обновляет * Обрабатывает выбор графа из `GraphView`. Устанавливает выбранный граф и обновляет
* историю чата, загружая сообщения, относящиеся к этому графу.
* @param {{ graphId: string; currentNode: string | null }} data - Объект, содержащий ID выбранного графа и текущий узел.
* @returns {Promise<void>}
*/ */
async handleGraphSelected(data: { graphId: string; currentNode: string | null }): Promise<void> { async handleGraphSelected(data: { graphId: string; currentNode: string | null }): Promise<void> {
this.selectedGraphId = data.graphId; 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); await this.fetchMessagesFromRootToNode(data.currentNode);
} }
/** /**
* Стримит ответ LLM. * Стримит ответ LLM.
* @param {string} graphId - ID графа.
* @param {string} userNodeId - ID узла пользователя.
* @param {string} selectedModel - Выбранная модель.
* @param {string | null} existingAssistantNodeId - Существующий ID узла ассистента для повторного использования (для регенерации).
*/ */
async streamLLMResponse( async streamLLMResponse(
graphId: string, graphId: string,
userNodeId: string, userNodeId: string,
selectedModel?: string, selectedModel?: string,
existingAssistantNodeId: string | null = null existingAssistantNodeId: string | null = null,
systemPrompt: string | null = null
): Promise<void> { ): Promise<void> {
const body = { const body = {
graph_id: graphId, graph_id: graphId,
user_node_id: userNodeId, user_node_id: userNodeId,
system_prompt: this.plugin.settings.systemPrompt, system_prompt: systemPrompt,
model: selectedModel || this.plugin.settings.defaultModel model: selectedModel || this.plugin.settings.defaultModel
}; };
// Если есть существующий ID узла ассистента, добавляем его в тело запроса
if (existingAssistantNodeId) { if (existingAssistantNodeId) {
Object.assign(body, { existing_assistant_node_id: 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`, { const response = await fetch(`${this.plugin.settings.apiBaseUrl}/chat/stream`, {
method: 'POST', method: 'POST',
headers: { 'Content-Type': 'application/json' }, headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(body) // Используем модифицированное тело запроса body: JSON.stringify(body)
}); });
if (!response.ok) { if (!response.ok) {
@ -299,7 +313,6 @@ export class ChatView extends ItemView {
if (data.type === 'node_created') { if (data.type === 'node_created') {
assistantNodeId = data.node_id; assistantNodeId = data.node_id;
// Если узел переиспользуется, нужно очистить его содержимое в UI
if (data.reused) { if (data.reused) {
this.clearStreamingMessage(assistantNodeId); this.clearStreamingMessage(assistantNodeId);
} }
@ -314,13 +327,12 @@ export class ChatView extends ItemView {
console.error('Ошибка:', data.error); console.error('Ошибка:', data.error);
if (assistantNodeId) { if (assistantNodeId) {
this.markNodeAsError(assistantNodeId, data.error); this.markNodeAsError(assistantNodeId, data.error);
// Эмиттируем событие, чтобы GraphView обновил отображение узла с ошибкой
this.plugin.eventBus.emit('graph-selected', { this.plugin.eventBus.emit('graph-selected', {
graphId: graphId, graphId: graphId,
currentNode: assistantNodeId currentNode: assistantNodeId
}); });
} }
break; // Прекращаем обработку при ошибке break;
} else if (data.type === 'done') { } else if (data.type === 'done') {
this.currentNode = assistantNodeId; this.currentNode = assistantNodeId;
this.plugin.eventBus.emit('graph-selected', { 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> { async handleRegenerateMessage(data: { graphId: string; nodeId: string; model: string }): Promise<void> {
// НОВОЕ: Добавляем 'model' в тип данных
try { try {
// Определяем системный промпт для отправки: кастомный для графа, если есть, иначе глобальный
const systemPromptToUse = this.currentGraphCustomSystemPrompt !== null ?
this.currentGraphCustomSystemPrompt :
this.plugin.settings.systemPrompt;
const response = await fetch(`${this.plugin.settings.apiBaseUrl}/chat/regenerate`, { const response = await fetch(`${this.plugin.settings.apiBaseUrl}/chat/regenerate`, {
method: 'POST', method: 'POST',
headers: { 'Content-Type': 'application/json' }, headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ body: JSON.stringify({
graph_id: data.graphId, graph_id: data.graphId,
node_id: data.nodeId, 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(); const result = await response.json();
// НОВОЕ: Получаем ID существующего узла ассистента и ID пользовательского узла
const existingAssistantNodeId = result.existing_assistant_node_id; const existingAssistantNodeId = result.existing_assistant_node_id;
const userNodeId = result.user_node_id; const userNodeId = result.user_node_id;
const modelToUse = result.model; const modelToUse = result.model;
const systemPromptFromBackend = result.system_prompt;
// Запускаем стриминг, передавая ID существующего узла для повторного использования
if (data.graphId) { if (data.graphId) {
await this.streamLLMResponse(data.graphId, userNodeId, modelToUse, existingAssistantNodeId); await this.streamLLMResponse(data.graphId, userNodeId, modelToUse, existingAssistantNodeId, systemPromptFromBackend);
} }
} catch (error: any) { } catch (error: any) {
console.error('Ошибка регенерации:', error); console.error('Ошибка регенерации:', error);
@ -434,16 +450,17 @@ export class ChatView extends ItemView {
} }
} }
/** /**
* Обрабатывает событие начала нового независимого чата. * Обрабатывает событие начала нового независимого чата.
* Сбрасывает текущее состояние ChatView, но не влияет на GraphView. * Сбрасывает текущее состояние ChatView, но не влияет на GraphView.
*/ */
async handleStartNewIndependentChat(): Promise<void> { async handleStartNewIndependentChat(): Promise<void> {
this.chatHistory = []; this.chatHistory = [];
this.currentNode = null; this.currentNode = null;
// Оставляем this.selectedGraphId без изменений // Оставляем this.selectedGraphId без изменений
if (this.chatPanel) { if (this.chatPanel) {
await this.chatPanel.updateHistory(this.chatHistory); await this.chatPanel.updateHistory(this.chatHistory);
this.chatPanel.updateCustomSystemPromptStatus(null);
} }
// Опционально: можно уведомить пользователя о начале нового чата // Опционально: можно уведомить пользователя о начале нового чата
new Notice('Начат новый чат. Текущий граф не изменен.', 2000); 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'; import { ItemView, WorkspaceLeaf, Notice } from 'obsidian';
// ----------------------------------------------- Plugin Imports ------------------------------------------------------------ // ----------------------------------------------- Plugin Imports ------------------------------------------------------------
import LLMAgentPlugin from 'main'; // Предполагаем, что main.ts экспортирует LLMAgentPlugin по умолчанию 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 { HistoryPanel } from '../components/HistoryPanel'; // Предполагаем, что HistoryPanel это JS-класс
import { GraphPanelComponent } from '../components/GraphPanel'; // Импортируем React-компонент и ReactDOM 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 { Dialog } from 'src/utils/Dialog';
import { GraphSettingsModal } from '../modals/GraphSettingsModal';
export const GRAPH_VIEW_TYPE = 'llm-agent-graph-view'; export const GRAPH_VIEW_TYPE = 'llm-agent-graph-view';
@ -25,6 +27,7 @@ export class GraphView extends ItemView {
selectedGraphId: string | null; selectedGraphId: string | null;
selectedNodeIds: Set<string>; // Добавляем Set для хранения ID выделенных узлов selectedNodeIds: Set<string>; // Добавляем Set для хранения ID выделенных узлов
selectedGraphTitle: string | null; selectedGraphTitle: string | null;
currentGraphCustomSystemPrompt: string | null;
// UI Elements // UI Elements
historyPanel: HistoryPanel | null; historyPanel: HistoryPanel | null;
@ -38,6 +41,7 @@ export class GraphView extends ItemView {
copySelectedButton: HTMLButtonElement | null = null; copySelectedButton: HTMLButtonElement | null = null;
deleteSelectedButton: HTMLButtonElement | null = null; deleteSelectedButton: HTMLButtonElement | null = null;
newIndependentChatActionButton: HTMLButtonElement | null = null; newIndependentChatActionButton: HTMLButtonElement | null = null;
graphSettingsButton: HTMLButtonElement | null = null;
constructor(leaf: WorkspaceLeaf, plugin: LLMAgentPlugin) { constructor(leaf: WorkspaceLeaf, plugin: LLMAgentPlugin) {
super(leaf); super(leaf);
@ -49,8 +53,9 @@ export class GraphView extends ItemView {
this.historyPanel = null; this.historyPanel = null;
this.reactRoot = null; this.reactRoot = null;
this.isHistoryCollapsed = true; this.isHistoryCollapsed = true;
this.selectedNodeIds = new Set<string>(); // Инициализируем Set this.selectedNodeIds = new Set<string>();
this.selectedGraphTitle = null; this.selectedGraphTitle = null;
this.currentGraphCustomSystemPrompt = null;
} }
/** /**
@ -210,7 +215,6 @@ export class GraphView extends ItemView {
const toolbar = this.graphContainer.querySelector('.graph-view-toolbar'); const toolbar = this.graphContainer.querySelector('.graph-view-toolbar');
if (!toolbar) return; if (!toolbar) return;
// Кнопка "Копировать выделенные"
this.copySelectedButton = toolbar.createEl('button', { this.copySelectedButton = toolbar.createEl('button', {
cls: 'global-action-button', cls: 'global-action-button',
attr: { 'aria-label': 'Копировать выделенные узлы', 'data-tooltip': 'Копировать выделенные узлы' }, attr: { 'aria-label': 'Копировать выделенные узлы', 'data-tooltip': 'Копировать выделенные узлы' },
@ -218,7 +222,6 @@ export class GraphView extends ItemView {
}); });
this.copySelectedButton.addEventListener('click', this.handleCopySelectedNodes.bind(this)); this.copySelectedButton.addEventListener('click', this.handleCopySelectedNodes.bind(this));
// Кнопка "Удалить выделенные"
this.deleteSelectedButton = toolbar.createEl('button', { this.deleteSelectedButton = toolbar.createEl('button', {
cls: 'global-action-button', cls: 'global-action-button',
attr: { 'aria-label': 'Удалить выделенные узлы', 'data-tooltip': 'Удалить выделенные узлы' }, attr: { 'aria-label': 'Удалить выделенные узлы', 'data-tooltip': 'Удалить выделенные узлы' },
@ -226,20 +229,79 @@ export class GraphView extends ItemView {
}); });
this.deleteSelectedButton.addEventListener('click', this.handleDeleteSelectedNodes.bind(this)); this.deleteSelectedButton.addEventListener('click', this.handleDeleteSelectedNodes.bind(this));
// Кнопка "Новый чат" (сохраняя текущий граф) this.newIndependentChatActionButton = toolbar.createEl('button', {
this.newIndependentChatActionButton = toolbar.createEl('button', { cls: 'global-action-button',
cls: 'global-action-button', attr: { 'aria-label': 'Новый независимый чат', 'data-tooltip': 'Начать новую беседу, формируя отдельный, независимый подграф' },
attr: { 'aria-label': 'Новый независимый чат', 'data-tooltip': 'Начать новую беседу, формируя отдельный, независимый подграф' }, text: '✨' // Или 'Новый чат'
text: '✨' // Или 'Новый чат' });
}); this.newIndependentChatActionButton.addEventListener('click', () => {
this.newIndependentChatActionButton.addEventListener('click', () => { // Эмитируем новое событие, которое будет слушать ТОЛЬКО ChatView
// Эмитируем новое событие, которое будет слушать ТОЛЬКО ChatView this.plugin.eventBus.emit('start-new-independent-chat');
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 -------------------------------------------------- // ----------------------------------------------- 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. * Инициализирует GraphPanelComponent (React компонент) в graphContainer.
* Использует ReactDOM.createRoot для React 18. * Использует ReactDOM.createRoot для React 18.
@ -278,9 +340,9 @@ export class GraphView extends ItemView {
edges: this.graphData.edges edges: this.graphData.edges
}, },
onNodeClick: this.handleNodeClick.bind(this), onNodeClick: this.handleNodeClick.bind(this),
onNodeSelectionChange: this.handleNodeSelectionChange.bind(this), // Передаем обработчик изменения выделения onNodeSelectionChange: this.handleNodeSelectionChange.bind(this),
onNodeDelete: this.handleDeleteNode.bind(this), // Передаем колбэк для удаления одиночного узла onNodeDelete: this.handleDeleteNode.bind(this),
selectedNodeIds: this.selectedNodeIds, // Передаем Set выделенных узлов selectedNodeIds: this.selectedNodeIds,
activeNodeId: this.currentNode, activeNodeId: this.currentNode,
app: this.plugin.app app: this.plugin.app
}) })
@ -329,7 +391,11 @@ export class GraphView extends ItemView {
this.selectedGraphTitle = null; // Сброс заголовка this.selectedGraphTitle = null; // Сброс заголовка
this.updateTitle(); // Обновляем заголовок this.updateTitle(); // Обновляем заголовок
this.renderGraphPanel(); 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; return;
} }
@ -351,22 +417,36 @@ export class GraphView extends ItemView {
// Устанавливаем заголовок графа // Устанавливаем заголовок графа
this.selectedGraphTitle = data.title || data.first_message || `Граф ${graphId.substring(0, 8)}...`; 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(); this.renderGraphPanel();
// Также обновляем ChatView с полученными сообщениями и текущим узлом // Также обновляем ChatView с полученными сообщениями и текущим узлом
this.plugin.eventBus.emit('chat-history-updated', { /* Нет подписчиков: this.plugin.eventBus.emit('chat-history-updated', {
graphId: graphId, graphId: graphId,
messages: data.messages || [], messages: data.messages || [],
current_node_id: data.current_node_id // Передаем актуальный current_node_id 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) { } catch (error) {
console.error('Ошибка загрузки данных графа:', error); console.error('Ошибка загрузки данных графа:', error);
this.selectedGraphTitle = null; this.selectedGraphTitle = null;
this.currentGraphCustomSystemPrompt = null; // Сброс кастомного промпта при ошибке
this.updateTitle(); // Обновляем заголовок this.updateTitle(); // Обновляем заголовок
// Optionally: show a notice to the user if (this.graphSettingsButton) {
// new Notice(`Ошибка загрузки данных графа: ${error.message}`); 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.graphData = { nodes: [], edges: [] };
this.currentNode = null; this.currentNode = null;
this.selectedGraphId = null; this.selectedNodeIds.clear();
this.selectedNodeIds.clear(); // Очищаем выделения this.selectedGraphTitle = null;
this.selectedGraphTitle = null; // Сброс заголовка при новом чате this.currentGraphCustomSystemPrompt = null;
this.updateTitle(); // Обновляем заголовок
this.renderGraphPanel(); try {
this.fetchGraphsList(); // Обновить список графов, чтобы показать новый пустой чат const response = await fetch(`${this.plugin.settings.apiBaseUrl}/graphs/new`, {
this.plugin.eventBus.emit('chat-history-updated', { graphId: null, messages: [], current_node_id: null }); 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> { async handleGraphSelected(data: { graphId: string; currentNode: string | null }): Promise<void> {
this.selectedGraphId = data.graphId; this.selectedGraphId = data.graphId;
this.selectedNodeIds.clear(); // Очищаем выделения при смене графа this.selectedNodeIds.clear();
await this.fetchGraphData(data.graphId); await this.fetchGraphData(data.graphId);
} }
/** /**
* Обработчик события удаления узла из ChatPanel. * Обработчик события удаления узла из ChatPanel.
* @param {{ graphId: string, nodeId: string }} data - Данные об удаляемом узле.
*/ */
async handleDeleteNodeFromChat(data: { graphId: string; nodeId: string }): Promise<void> { async handleDeleteNodeFromChat(data: { graphId: string; nodeId: string }): Promise<void> {
if (data.graphId === this.selectedGraphId) { if (data.graphId === this.selectedGraphId) {