639 lines
25 KiB
TypeScript
639 lines
25 KiB
TypeScript
/*
|
||
* ChatView управляет пользовательским интерфейсом чата, отображает историю сообщений,
|
||
* отправляет запросы LLM-агенту и обрабатывает ответы, обновляя состояние чата и графа.
|
||
*/
|
||
|
||
import { ItemView, Notice, WorkspaceLeaf } from 'obsidian';
|
||
import { ChatPanel } from '../components/ChatPanel';
|
||
import LLMAgentPlugin from 'main';
|
||
import { Attachment } from 'src/components/models/ChatHistoryItem';
|
||
import { TemplateEngine } from '../references/TemplateEngine';
|
||
|
||
export const CHAT_VIEW_TYPE = 'llm-agent-chat-view';
|
||
|
||
import { LLM_DEFAULTS } from 'src/constants';
|
||
|
||
// ----------------------------------------------- Chat View ---------------------------------------------------------------
|
||
|
||
export class ChatView extends ItemView {
|
||
plugin: LLMAgentPlugin;
|
||
chatHistory: any[];
|
||
selectedGraphId: string | null;
|
||
currentNode: string | null;
|
||
chatContainer: HTMLDivElement;
|
||
chatPanel: ChatPanel | null;
|
||
currentGraphCustomSystemPrompt: string | null;
|
||
currentStreamingNodeId: string | null = null;
|
||
|
||
constructor(leaf: WorkspaceLeaf, plugin: LLMAgentPlugin) {
|
||
super(leaf);
|
||
this.plugin = plugin;
|
||
this.chatHistory = [];
|
||
this.selectedGraphId = null;
|
||
this.currentNode = null;
|
||
this.chatPanel = null;
|
||
this.currentGraphCustomSystemPrompt = null;
|
||
}
|
||
|
||
getViewType(): string {
|
||
return CHAT_VIEW_TYPE;
|
||
}
|
||
|
||
getDisplayText(): string {
|
||
return 'LLM Agent Chat';
|
||
}
|
||
|
||
getIcon(): string {
|
||
return 'message-circle';
|
||
}
|
||
|
||
async onOpen(): Promise<void> {
|
||
const container = this.containerEl.children[1] as HTMLElement;
|
||
container.empty();
|
||
|
||
// Создаем только контейнер для чата (без истории)
|
||
this.chatContainer = container.createDiv() as HTMLDivElement;
|
||
this.chatContainer.addClass('llm-agent-chat-container');
|
||
|
||
// Загружаем доступные модели (TODO: проверить, что здесь это нужно, что не сделали этого ранее)
|
||
// await this.plugin.fetchAvailableModels();
|
||
|
||
// Инициализируем только чат панель с передачей plugin
|
||
this.chatPanel = new ChatPanel(this.chatContainer, {
|
||
chatHistory: this.chatHistory,
|
||
onSendMessage: this.handleSendMessage.bind(this),
|
||
selectedModel: this.plugin.settings.defaultModel || LLM_DEFAULTS.MODEL,
|
||
plugin: this.plugin,
|
||
currentGraphCustomSystemPrompt: this.currentGraphCustomSystemPrompt
|
||
});
|
||
|
||
this.plugin.eventBus.on('settings-changed', this.handleSettingsChanged.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('graph-selected', this.handleGraphSelected.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));
|
||
this.plugin.eventBus.on('stop-stream-request', this.handleStopStreamRequest.bind(this)); // НОВОЕ
|
||
this.plugin.eventBus.on('stream-started', this.handleStreamStarted.bind(this));
|
||
this.plugin.eventBus.on('stream-ended', this.handleStreamEnded.bind(this));
|
||
}
|
||
|
||
async onClose(): Promise<void> {
|
||
if (this.chatPanel) {
|
||
this.chatPanel.destroy();
|
||
this.chatPanel = null;
|
||
}
|
||
this.plugin.eventBus.off('settings-changed', this.handleSettingsChanged);
|
||
this.plugin.eventBus.off('node-selected', this.handleNodeSelected);
|
||
this.plugin.eventBus.off('new-chat', this.handleNewChat);
|
||
this.plugin.eventBus.off('graph-selected', this.handleGraphSelected);
|
||
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);
|
||
this.plugin.eventBus.off('stop-stream-request', this.handleStopStreamRequest); // НОВОЕ
|
||
this.plugin.eventBus.off('stream-started', this.handleStreamStarted);
|
||
this.plugin.eventBus.off('stream-ended', this.handleStreamEnded);
|
||
}
|
||
|
||
// ----------------------------------------------- API Methods ---------------------------------------------------------------
|
||
|
||
// TODO: проблемное место
|
||
/**
|
||
* Загружает историю сообщений от корня до указанного узла графа.
|
||
* Обновляет `chatHistory` и отображение в `chatPanel`.
|
||
* @param {string} nodeId - ID конечного узла для загрузки истории.
|
||
* @returns {Promise<void>}
|
||
*/
|
||
async fetchMessagesFromRootToNode(nodeId: string | null): Promise<void> {
|
||
try {
|
||
// Убедимся, что selectedGraphId установлен
|
||
if (!this.selectedGraphId) {
|
||
console.warn('selectedGraphId не установлен. Невозможно загрузить сообщения.');
|
||
return;
|
||
}
|
||
|
||
const url = nodeId
|
||
? `${this.plugin.settings.apiBaseUrl}/messages/${this.selectedGraphId}/${nodeId}`
|
||
: `${this.plugin.settings.apiBaseUrl}/messages/${this.selectedGraphId}/last`;
|
||
const response = await fetch(url);
|
||
|
||
if (!response.ok) {
|
||
const errorText = await response.text();
|
||
throw new Error(`Ошибка загрузки сообщений: ${response.status} - ${errorText}`);
|
||
}
|
||
const data = await response.json();
|
||
|
||
this.chatHistory =
|
||
data.map((msg: any) => ({
|
||
...msg,
|
||
type: msg.type || msg.role // Используем type, если есть, иначе role как fallback
|
||
})) || [];
|
||
|
||
// Уточнение, чтобы после выбора графа мы создавали узлы с не-пустым родителем
|
||
if (this.chatHistory.length > 0) {
|
||
// Устанавливаем currentNode на ID последнего сообщения в загруженной истории
|
||
this.currentNode = this.chatHistory[this.chatHistory.length - 1].node_id;
|
||
} else {
|
||
this.currentNode = null; // Если история пуста, current_node_id также null
|
||
}
|
||
|
||
if (this.chatPanel) {
|
||
await this.chatPanel.updateHistory(this.chatHistory);
|
||
}
|
||
} catch (error) {
|
||
console.error('Ошибка загрузки сообщений:', error);
|
||
// Optionally: show a notice to the user
|
||
// new Notice(`Ошибка загрузки сообщений: ${error.message}`);
|
||
|
||
/* Спорно, пока закомментил: this.chatHistory = []; // Очищаем историю при ошибке
|
||
this.currentNode = null; // Сбрасываем currentNode при ошибке
|
||
if (this.chatPanel) {
|
||
await this.chatPanel.updateHistory(this.chatHistory);
|
||
}*/
|
||
}
|
||
}
|
||
|
||
// ----------------------------------------------- Event Handlers ---------------------------------------------------------------
|
||
|
||
async handleSendMessage(message: string, selectedModel?: string, attachments?: Attachment[], customMaxTokens?: number, customTemperature?: number, agencyMode: boolean = false): Promise<void> {
|
||
try {
|
||
// ЗАЩИТА: Если граф не выбран, создаем его на лету, чтобы не потерять текст
|
||
if (!this.selectedGraphId) {
|
||
const createResponse = await fetch(`${this.plugin.settings.apiBaseUrl}/graphs/new`, {
|
||
method: 'POST',
|
||
headers: { 'Content-Type': 'application/json' }
|
||
});
|
||
|
||
if (!createResponse.ok) {
|
||
throw new Error(`Ошибка автоматического создания графа: ${createResponse.status}`);
|
||
}
|
||
|
||
const createResult = await createResponse.json();
|
||
this.selectedGraphId = createResult.graph_id;
|
||
|
||
// Уведомляем другие компоненты о том, что выбран новый граф
|
||
this.plugin.eventBus.emit('graph-selected', {
|
||
graphId: this.selectedGraphId,
|
||
currentNode: null
|
||
});
|
||
}
|
||
|
||
const cacheFolder = this.getCacheFolderPath();
|
||
|
||
// Шаг 1: Создаём узел пользователя с вложениями
|
||
const sendResponse = await fetch(`${this.plugin.settings.apiBaseUrl}/chat/send`, {
|
||
method: 'POST',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify({
|
||
message: message,
|
||
graph_id: this.selectedGraphId,
|
||
parent_node_id: this.currentNode,
|
||
attachments: attachments || [],
|
||
cache_folder: cacheFolder
|
||
})
|
||
});
|
||
|
||
if (!sendResponse.ok) {
|
||
throw new Error(`Ошибка при отправке сообщения: ${sendResponse.status}`);
|
||
}
|
||
|
||
const sendResult = await sendResponse.json();
|
||
const userNodeId = sendResult.node_id;
|
||
|
||
this.plugin.eventBus.emit('graph-selected', {
|
||
graphId: this.selectedGraphId,
|
||
currentNode: userNodeId
|
||
});
|
||
|
||
// Шаг 2: Запускаем стриминг ответа
|
||
if (this.selectedGraphId) {
|
||
const systemPromptToUse =
|
||
this.currentGraphCustomSystemPrompt !== null ? this.currentGraphCustomSystemPrompt : this.plugin.settings.systemPrompt;
|
||
|
||
await this.streamLLMResponse(this.selectedGraphId!, userNodeId, selectedModel, null, systemPromptToUse, cacheFolder, customMaxTokens, customTemperature, agencyMode);
|
||
} else {
|
||
throw new Error('Graph ID не определён после отправки сообщения');
|
||
}
|
||
} catch (error: any) {
|
||
console.error('Ошибка отправки сообщения:', error);
|
||
new Notice(`Ошибка: ${error?.message || error}`, 5000);
|
||
}
|
||
}
|
||
|
||
handleSettingsChanged(): void {
|
||
if (this.chatPanel)
|
||
{
|
||
// 1. Обновляем модель
|
||
this.chatPanel.setSelectedModel(this.plugin.settings.defaultModel);
|
||
|
||
// 2. Обновляем состояние тумблера из настроек плагина
|
||
if (this.chatPanel.agencyToggle) {
|
||
this.chatPanel.agencyToggle.checked = !!this.plugin.settings.agencyModeEnabled;
|
||
}
|
||
|
||
// 3. Уведомляем панель об обновлении всего объекта плагина (и настроек внутри)
|
||
this.chatPanel.updateSettings(this.plugin);
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Обрабатывает выбор узла на графе. Загружает историю сообщений до выбранного узла
|
||
* и обновляет панель чата.
|
||
* @param {{ nodeId: string }} data - Объект, содержащий ID выбранного узла.
|
||
*/
|
||
handleNodeSelected(data: { nodeId: string }): void {
|
||
this.currentNode = data.nodeId;
|
||
if (this.selectedGraphId) {
|
||
this.fetchMessagesFromRootToNode(data.nodeId).then(() => {
|
||
// После загрузки новой ветки проверяем, есть ли стриминг в ней
|
||
if (this.currentStreamingNodeId) {
|
||
const isInNewBranch = this.chatHistory.some((msg) => msg.node_id === this.currentStreamingNodeId);
|
||
if (this.chatPanel) {
|
||
this.chatPanel.updateStopButtonVisibility(isInNewBranch, isInNewBranch ? this.currentStreamingNodeId : null);
|
||
}
|
||
}
|
||
});
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Обрабатывает событие начала нового чата. Сбрасывает текущее состояние чата
|
||
* и очищает отображение истории.
|
||
*/
|
||
async handleNewChat(): Promise<void> {
|
||
this.chatHistory = [];
|
||
this.currentNode = null;
|
||
//this.selectedGraphId = null;
|
||
//this.currentGraphCustomSystemPrompt = null;
|
||
this.currentGraphCustomSystemPrompt = null;
|
||
if (this.chatPanel) {
|
||
await this.chatPanel.updateHistory(this.chatHistory);
|
||
this.chatPanel.updateCustomSystemPromptStatus(null);
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Обрабатывает выбор графа из `GraphView`. Устанавливает выбранный граф и обновляет
|
||
*/
|
||
async handleGraphSelected(data: { graphId: string; currentNode: string | null }): Promise<void> {
|
||
this.selectedGraphId = data.graphId;
|
||
this.currentNode = data.currentNode;
|
||
// Важно: custom_system_prompt будет обновлен через 'graph-custom-prompt-updated' событие,
|
||
// которое генерируется в GraphView после fetchGraphData.
|
||
// Здесь мы пока не трогаем this.currentGraphCustomSystemPrompt напрямую,
|
||
// но ожидаем, что GraphView его обновит.
|
||
await this.fetchMessagesFromRootToNode(data.currentNode);
|
||
}
|
||
|
||
/**
|
||
* Стримит ответ LLM.
|
||
*/
|
||
async streamLLMResponse(
|
||
graphId: string,
|
||
userNodeId: string,
|
||
selectedModel?: string,
|
||
existingAssistantNodeId: string | null = null,
|
||
systemPrompt: string | null = null,
|
||
cacheFolder?: string,
|
||
customMaxTokens?: number,
|
||
customTemperature?: number,
|
||
agencyMode: boolean = false
|
||
): Promise<void> {
|
||
// Разворачиваем системный промпт
|
||
let finalSystemPrompt = systemPrompt;
|
||
if (systemPrompt) {
|
||
const templateEngine = new TemplateEngine(this.plugin);
|
||
finalSystemPrompt = await templateEngine.processRawSettingValue(systemPrompt);
|
||
}
|
||
|
||
const temperature = customTemperature !== undefined
|
||
? customTemperature
|
||
: ((this.plugin.settings as any).temperatureChat !== undefined ? (this.plugin.settings as any).temperatureChat : LLM_DEFAULTS.TEMPERATURE);
|
||
|
||
const body = {
|
||
graph_id: graphId,
|
||
user_node_id: userNodeId,
|
||
system_prompt: finalSystemPrompt,
|
||
model: selectedModel || this.plugin.settings.defaultModel,
|
||
cache_folder: cacheFolder,
|
||
temperature: temperature,
|
||
max_tokens: customMaxTokens,
|
||
agency_mode: agencyMode
|
||
};
|
||
|
||
if (existingAssistantNodeId) {
|
||
Object.assign(body, { existing_assistant_node_id: existingAssistantNodeId });
|
||
}
|
||
|
||
const abortController = new AbortController();
|
||
let assistantNodeId: string | null = null;
|
||
|
||
let reader: ReadableStreamDefaultReader<Uint8Array> | null = null;
|
||
|
||
try {
|
||
const response = await fetch(`${this.plugin.settings.apiBaseUrl}/chat/stream`, {
|
||
method: 'POST',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify(body),
|
||
signal: abortController.signal
|
||
});
|
||
|
||
if (!response.ok) {
|
||
throw new Error(`Ошибка стриминга: ${response.status}`);
|
||
}
|
||
|
||
if (!response.body) {
|
||
throw new Error('Response body is null');
|
||
}
|
||
reader = response.body.getReader();
|
||
|
||
const decoder = new TextDecoder();
|
||
let accumulatedContent = '';
|
||
|
||
while (true) {
|
||
if (abortController.signal.aborted) {
|
||
break;
|
||
}
|
||
|
||
const { done, value } = await reader.read();
|
||
if (done) break;
|
||
|
||
const chunk = decoder.decode(value, { stream: true });
|
||
const lines = chunk.split('\n');
|
||
|
||
for (const line of lines) {
|
||
if (line.startsWith('data: ')) {
|
||
const data = JSON.parse(line.slice(6));
|
||
|
||
if (data.type === 'node_created') {
|
||
assistantNodeId = data.node_id;
|
||
|
||
this.plugin.eventBus.emit('stream-started', {
|
||
nodeId: assistantNodeId,
|
||
controller: abortController
|
||
});
|
||
|
||
if (data.reused) {
|
||
this.clearStreamingMessage(assistantNodeId!);
|
||
}
|
||
this.plugin.eventBus.emit('graph-selected', {
|
||
graphId: graphId,
|
||
currentNode: assistantNodeId
|
||
});
|
||
}
|
||
else if (data.type === 'tool_start' && assistantNodeId) {
|
||
if (this.chatPanel) this.chatPanel.toolExecutionUpdate(assistantNodeId, data.name, 'start', data.step);
|
||
} else if (data.type === 'tool_end' && assistantNodeId) {
|
||
if (this.chatPanel) this.chatPanel.toolExecutionUpdate(assistantNodeId, data.name, 'end', data.step);
|
||
} else if (data.type === 'chunk' && assistantNodeId) {
|
||
accumulatedContent += data.content;
|
||
this.updateStreamingMessage(assistantNodeId, accumulatedContent);
|
||
} else if (data.type === 'error') {
|
||
console.error('Ошибка:', data.error);
|
||
if (assistantNodeId) {
|
||
this.markNodeAsError(assistantNodeId, data.error);
|
||
this.plugin.eventBus.emit('graph-selected', {
|
||
graphId: graphId,
|
||
currentNode: assistantNodeId
|
||
});
|
||
|
||
this.plugin.eventBus.emit('stream-ended', { nodeId: assistantNodeId });
|
||
}
|
||
break;
|
||
} else if (data.type === 'done') {
|
||
this.currentNode = assistantNodeId;
|
||
this.plugin.eventBus.emit('graph-selected', {
|
||
graphId: graphId,
|
||
currentNode: assistantNodeId
|
||
});
|
||
|
||
if (assistantNodeId) {
|
||
this.plugin.eventBus.emit('stream-ended', { nodeId: assistantNodeId });
|
||
}
|
||
break;
|
||
}
|
||
}
|
||
}
|
||
}
|
||
} catch (error: any) {
|
||
if (error.name === 'AbortError') {
|
||
console.log('Стрим был отменен пользователем');
|
||
if (assistantNodeId) {
|
||
this.plugin.eventBus.emit('stream-ended', { nodeId: assistantNodeId });
|
||
}
|
||
} else {
|
||
throw error; // Пробрасываем другие ошибки
|
||
}
|
||
} finally {
|
||
reader?.releaseLock();
|
||
if (assistantNodeId) {
|
||
this.plugin.eventBus.emit('stream-ended', { nodeId: assistantNodeId });
|
||
}
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Очищает содержимое сообщения в истории чата, помечая его как заглушку.
|
||
* Используется для подготовки к регенерации.
|
||
* @param {string} nodeId - ID узла, содержимое которого нужно очистить.
|
||
*/
|
||
clearStreamingMessage(nodeId: string): void {
|
||
const message = this.chatHistory.find((msg) => msg.node_id === nodeId);
|
||
if (message) {
|
||
message.content = 'Получение ответа...';
|
||
message.type = 'llm';
|
||
|
||
// Точечное обновление вместо полного рендера всей панели
|
||
if (this.chatPanel) {
|
||
this.chatPanel.streamUpdate(nodeId, message.content);
|
||
}
|
||
}
|
||
}
|
||
|
||
|
||
updateStreamingMessage(nodeId: string, content: string): void {
|
||
// Надежный поиск по ID, а не по длине массива (предотвращает создание дублей)
|
||
let message = this.chatHistory.find((msg) => msg.node_id === nodeId);
|
||
|
||
if (message) {
|
||
message.content = content;
|
||
// Отправляем точечное обновление в панель чата
|
||
if (this.chatPanel) {
|
||
this.chatPanel.streamUpdate(nodeId, content);
|
||
}
|
||
} else {
|
||
// Если узла еще нет (первый чанк стрима), добавляем его ОДИН РАЗ
|
||
this.chatHistory.push({
|
||
role: 'assistant',
|
||
content: content,
|
||
node_id: nodeId,
|
||
graph_id: this.selectedGraphId || ''
|
||
});
|
||
|
||
// При самом первом появлении отрисовываем штатным способом (создается HTML-контейнер)
|
||
if (this.chatPanel) {
|
||
this.chatPanel.updateHistory(this.chatHistory);
|
||
}
|
||
}
|
||
}
|
||
|
||
markNodeAsError(nodeId: string, errorMessage: string): void {
|
||
const lastMessage = this.chatHistory[this.chatHistory.length - 1];
|
||
if (lastMessage && lastMessage.node_id === nodeId) {
|
||
lastMessage.type = 'error';
|
||
lastMessage.content = errorMessage;
|
||
} else {
|
||
// Если узел не последний, попробуйте найти его и обновить
|
||
const messageIndex = this.chatHistory.findIndex((msg) => msg.node_id === nodeId);
|
||
if (messageIndex !== -1) {
|
||
this.chatHistory[messageIndex].type = 'error';
|
||
this.chatHistory[messageIndex].content = errorMessage;
|
||
} else {
|
||
// Если узел не найден, добавьте новое сообщение об ошибке
|
||
this.chatHistory.push({
|
||
role: 'assistant',
|
||
content: errorMessage,
|
||
node_id: nodeId,
|
||
graph_id: this.selectedGraphId || '',
|
||
type: 'error'
|
||
});
|
||
}
|
||
}
|
||
|
||
if (this.chatPanel) {
|
||
this.chatPanel.updateHistory(this.chatHistory);
|
||
}
|
||
}
|
||
|
||
async handleRegenerateMessage(data: { graphId: string; nodeId: string; model: string, maxTokens?: number, temperature?: number, agencyMode?: boolean }): Promise<void> {
|
||
try {
|
||
const systemPromptToUse =
|
||
this.currentGraphCustomSystemPrompt !== null ? this.currentGraphCustomSystemPrompt : this.plugin.settings.systemPrompt;
|
||
|
||
const cacheFolder = this.getCacheFolderPath();
|
||
const temperature = data.temperature !== undefined
|
||
? data.temperature
|
||
: ((this.plugin.settings as any).temperatureChat !== undefined ? (this.plugin.settings as any).temperatureChat : LLM_DEFAULTS.TEMPERATURE);
|
||
|
||
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,
|
||
system_prompt: systemPromptToUse,
|
||
cache_folder: cacheFolder,
|
||
temperature: temperature,
|
||
max_tokens: data.maxTokens
|
||
})
|
||
});
|
||
|
||
if (!response.ok) {
|
||
throw new Error(`Ошибка регенерации: ${response.status}`);
|
||
}
|
||
|
||
const result = await response.json();
|
||
const existingAssistantNodeId = result.existing_assistant_node_id;
|
||
const userNodeId = result.user_node_id;
|
||
const modelToUse = result.model;
|
||
const systemPromptFromBackend = result.system_prompt;
|
||
const maxTokensFromBackend = result.max_tokens;
|
||
const temperatureFromBackend = result.temperature;
|
||
|
||
if (data.graphId) {
|
||
await this.streamLLMResponse(data.graphId, userNodeId, modelToUse, existingAssistantNodeId, systemPromptFromBackend,
|
||
cacheFolder, maxTokensFromBackend, temperatureFromBackend, data.agencyMode || false);
|
||
}
|
||
} catch (error: any) {
|
||
console.error('Ошибка регенерации:', error);
|
||
new Notice(`Ошибка регенерации: ${error?.message || error}`, 5000);
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Обрабатывает событие начала нового независимого чата.
|
||
* Сбрасывает текущее состояние 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);
|
||
}
|
||
|
||
handleStopStreamRequest(data: { nodeId: string }): void {
|
||
const controller = (this.plugin as any).activeStreamControllers?.get(data.nodeId);
|
||
if (controller) {
|
||
controller.abort();
|
||
new Notice('Генерация остановлена пользователем.', 2000);
|
||
}
|
||
}
|
||
|
||
handleStreamStarted(data: { nodeId: string; controller: AbortController }): void {
|
||
this.currentStreamingNodeId = data.nodeId;
|
||
// TODO: Временно не проверяем, что блок сообщения уже отрисован в панели,
|
||
// т.к. у нас пока к моменту вызова 'stream-started' сообщение ещё не отрисовано
|
||
// const isInCurrentBranch = this.chatHistory.some((msg) => msg.node_id === data.nodeId);
|
||
// if (this.chatPanel && isInCurrentBranch) {
|
||
if (this.chatPanel) {
|
||
this.chatPanel.updateStopButtonVisibility(true, data.nodeId);
|
||
}
|
||
}
|
||
|
||
handleStreamEnded(data: { nodeId: string }): void {
|
||
if (this.currentStreamingNodeId === data.nodeId) {
|
||
this.currentStreamingNodeId = null;
|
||
if (this.chatPanel) {
|
||
this.chatPanel.updateStopButtonVisibility(false, null);
|
||
}
|
||
}
|
||
}
|
||
|
||
// ----------------------------------------------- WebSocket Event Handlers ---------------------------------------------------------------
|
||
|
||
handleNodeTypeChanged(data: { graphId: string; nodeId: string; nodeType: string }): void {
|
||
// Обновляем тип узла в истории чата
|
||
const messageIndex = this.chatHistory.findIndex((msg) => msg.node_id === data.nodeId);
|
||
if (messageIndex !== -1) {
|
||
this.chatHistory[messageIndex].type = data.nodeType;
|
||
if (this.chatPanel) {
|
||
this.chatPanel.updateHistory(this.chatHistory);
|
||
}
|
||
}
|
||
}
|
||
|
||
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);
|
||
}
|
||
}
|
||
}
|
||
|
||
// ----------------------------------------------- Helpers ---------------------------------------------------------------
|
||
|
||
/**
|
||
* Возвращает абсолютный путь к папке кеша
|
||
*/
|
||
private getCacheFolderPath(): string {
|
||
// TODO: Нужно обработать случай, когда cacheFolder пустой
|
||
return this.chatPanel?.cacheManager?.getCacheFolderPath() ?? '';
|
||
}
|
||
}
|