Add graph options with custom system prompt, make more correct handling for New Chat button
This commit is contained in:
parent
fd6e7d12c5
commit
1a7c1de507
2
main.ts
2
main.ts
|
|
@ -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
25
package-lock.json
generated
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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');
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
92
src/modals/GraphSettingsModal.ts
Normal file
92
src/modals/GraphSettingsModal.ts
Normal 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();
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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) {
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user