Buf fix (emit new chat also for ChatView)

This commit is contained in:
dimitrievgs 2025-09-14 20:37:31 +03:00
parent 04a19b2508
commit dfe27f79ef
5 changed files with 255 additions and 314 deletions

70
main.js

File diff suppressed because one or more lines are too long

View File

@ -30,9 +30,7 @@ import { Handle, Position } from 'reactflow';
// Простой компонент для узла типа 'user' // Простой компонент для узла типа 'user'
// Это базовый пример. Ты можешь стилизовать его по своему усмотрению. // Это базовый пример. Ты можешь стилизовать его по своему усмотрению.
const UserNode: React.FC<any> = ({ data, isConnectable }) => { const UserNode: React.FC<any> = ({ data, isConnectable }) => {
return React.createElement('div', { return React.createElement(React.Fragment, null, // Используем React.Fragment для возврата нескольких элементов без лишнего div
className: 'react-flow__node-user'
},
React.createElement(Handle, { React.createElement(Handle, {
type: "source", type: "source",
position: Position.Bottom, position: Position.Bottom,
@ -49,9 +47,7 @@ const UserNode: React.FC<any> = ({ data, isConnectable }) => {
// Простой компонент для узла типа 'llm' // Простой компонент для узла типа 'llm'
const LLMNode: React.FC<any> = ({ data, isConnectable }) => { const LLMNode: React.FC<any> = ({ data, isConnectable }) => {
return React.createElement('div', { return React.createElement(React.Fragment, null,
className: 'react-flow__node-llm'
},
React.createElement(Handle, { React.createElement(Handle, {
type: "source", type: "source",
position: Position.Bottom, position: Position.Bottom,
@ -68,9 +64,7 @@ const LLMNode: React.FC<any> = ({ data, isConnectable }) => {
// Простой компонент для узла типа 'tool' // Простой компонент для узла типа 'tool'
const ToolNode: React.FC<any> = ({ data, isConnectable }) => { const ToolNode: React.FC<any> = ({ data, isConnectable }) => {
return React.createElement('div', { return React.createElement(React.Fragment, null,
className: 'react-flow__node-tool'
},
React.createElement(Handle, { React.createElement(Handle, {
type: "source", type: "source",
position: Position.Bottom, position: Position.Bottom,
@ -87,27 +81,7 @@ const ToolNode: React.FC<any> = ({ data, isConnectable }) => {
// Простой компонент для узла типа 'error' // Простой компонент для узла типа 'error'
const ErrorNode: React.FC<any> = ({ data, isConnectable }) => { const ErrorNode: React.FC<any> = ({ data, isConnectable }) => {
return React.createElement('div', { return React.createElement(React.Fragment, null,
className: 'react-flow__node-error'
},
React.createElement(Handle, {
type: "source",
position: Position.Bottom,
isConnectable: isConnectable,
}),
React.createElement(Handle, {
type: "target",
position: Position.Top,
isConnectable: isConnectable,
}),
data.label
);
};
const AssistantNode: React.FC<any> = ({ data, isConnectable }) => {
return React.createElement('div', {
className: 'react-flow__node-assistant' // Используем CSS класс для узла assistant
},
React.createElement(Handle, { React.createElement(Handle, {
type: "source", type: "source",
position: Position.Bottom, position: Position.Bottom,
@ -128,7 +102,6 @@ const nodeTypes = {
llm: LLMNode, llm: LLMNode,
tool: ToolNode, tool: ToolNode,
error: ErrorNode, error: ErrorNode,
assistant: AssistantNode,
// Если у тебя есть node.type === 'default', то можешь его тоже определить, // Если у тебя есть node.type === 'default', то можешь его тоже определить,
// либо использовать встроенный 'default' React Flow. // либо использовать встроенный 'default' React Flow.
}; };

View File

@ -3,12 +3,14 @@
* прошлых разговоров, начала новых чатов и удаления записей графов. * прошлых разговоров, начала новых чатов и удаления записей графов.
*/ */
import { EventBus } from '../utils/EventBus';
export class HistoryPanel { export class HistoryPanel {
container: HTMLElement; container: HTMLElement;
props: { props: {
graphs: any[]; graphs: any[];
onGraphSelect: (graphId: string) => void; onGraphSelect: (graphId: string) => void;
onNewChat: () => void; eventBus: EventBus;
}; };
graphs: any[]; graphs: any[];
newChatButton: HTMLButtonElement; newChatButton: HTMLButtonElement;
@ -22,7 +24,7 @@ export class HistoryPanel {
props: { props: {
graphs: any[]; graphs: any[];
onGraphSelect: (graphId: string) => void; onGraphSelect: (graphId: string) => void;
onNewChat: () => void; eventBus: EventBus;
} }
) { ) {
this.container = container; this.container = container;
@ -57,9 +59,7 @@ export class HistoryPanel {
setupEventListeners(): void { setupEventListeners(): void {
this.newChatButton.addEventListener('click', () => { this.newChatButton.addEventListener('click', () => {
if (this.props.onNewChat) { this.props.eventBus.emit('new-chat');
this.props.onNewChat();
}
}); });
this.deleteButton.addEventListener('click', () => { this.deleteButton.addEventListener('click', () => {

View File

@ -55,10 +55,10 @@ export class ChatView extends ItemView {
}); });
// Подписываемся на события // Подписываемся на события
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));
} }
async onClose(): Promise<void> { async onClose(): Promise<void> {
if (this.chatPanel) { if (this.chatPanel) {

View File

@ -12,70 +12,70 @@ export const GRAPH_VIEW_TYPE = 'llm-agent-graph-view';
// ----------------------------------------------- Graph View --------------------------------------------------------------- // ----------------------------------------------- Graph View ---------------------------------------------------------------
export class GraphView extends ItemView { export class GraphView extends ItemView {
plugin: LLMAgentPlugin; plugin: LLMAgentPlugin;
graphData: { nodes: any[], edges: any[] }; graphData: { nodes: any[]; edges: any[] };
currentNode: string | null; currentNode: string | null;
graphsList: any[]; graphsList: any[];
selectedGraphId: string | null; selectedGraphId: string | null;
// UI Elements // UI Elements
historyPanel: HistoryPanel | null; historyPanel: HistoryPanel | null;
historyContainer: HTMLDivElement; historyContainer: HTMLDivElement;
graphContainer: HTMLDivElement; graphContainer: HTMLDivElement;
reactRoot: ReactDOM.Root | null; // <-- ИСПРАВЛЕНО: Указываем тип для reactRoot reactRoot: ReactDOM.Root | null; // <-- ИСПРАВЛЕНО: Указываем тип для reactRoot
constructor(leaf: WorkspaceLeaf, plugin: LLMAgentPlugin) { constructor(leaf: WorkspaceLeaf, plugin: LLMAgentPlugin) {
super(leaf); super(leaf);
this.plugin = plugin; this.plugin = plugin;
this.graphData = { nodes: [], edges: [] }; this.graphData = { nodes: [], edges: [] };
this.currentNode = null; this.currentNode = null;
this.graphsList = []; this.graphsList = [];
this.selectedGraphId = null; this.selectedGraphId = null;
this.historyPanel = null; this.historyPanel = null;
this.reactRoot = null; // Инициализируем null this.reactRoot = null; // Инициализируем null
} }
/** /**
* @returns {string} Тип представления. * @returns {string} Тип представления.
*/ */
getViewType(): string { getViewType(): string {
return GRAPH_VIEW_TYPE; return GRAPH_VIEW_TYPE;
} }
/** /**
* @returns {string} Отображаемое имя представления. * @returns {string} Отображаемое имя представления.
*/ */
getDisplayText(): string { getDisplayText(): string {
return 'LLM Agent Graph'; return 'LLM Agent Graph';
} }
/** /**
* @returns {string} Иконка представления. * @returns {string} Иконка представления.
*/ */
getIcon(): string { getIcon(): string {
return 'brain-circuit'; return 'brain-circuit';
} }
/** /**
* Вызывается при открытии представления. Инициализирует UI и загружает данные. * Вызывается при открытии представления. Инициализирует UI и загружает данные.
* @returns {Promise<void>} * @returns {Promise<void>}
*/ */
async onOpen(): Promise<void> { async onOpen(): Promise<void> {
const container = this.containerEl.children[1] as HTMLElement; const container = this.containerEl.children[1] as HTMLElement;
container.empty(); container.empty();
// Создаем основной контейнер с flexbox layout // Создаем основной контейнер с flexbox layout
const mainContainer = container.createDiv('llm-agent-graph-main'); const mainContainer = container.createDiv('llm-agent-graph-main');
mainContainer.style.cssText = ` mainContainer.style.cssText = `
display: flex; display: flex;
height: 100%; height: 100%;
width: 100%; width: 100%;
background-color: var(--background-primary); /* Убедимся, что фон установлен */ background-color: var(--background-primary); /* Убедимся, что фон установлен */
`; `;
// Левая панель для истории // Левая панель для истории
this.historyContainer = mainContainer.createDiv('llm-agent-history-sidebar') as HTMLDivElement; this.historyContainer = mainContainer.createDiv('llm-agent-history-sidebar') as HTMLDivElement;
this.historyContainer.style.cssText = ` this.historyContainer.style.cssText = `
width: 300px; width: 300px;
min-width: 300px; min-width: 300px;
border-right: 1px solid var(--background-modifier-border); /* ИСПРАВЛЕНО: Использование переменной Obsidian */ border-right: 1px solid var(--background-modifier-border); /* ИСПРАВЛЕНО: Использование переменной Obsidian */
@ -83,198 +83,196 @@ export class GraphView extends ItemView {
background-color: var(--background-secondary); /* Убедимся, что фон установлен */ background-color: var(--background-secondary); /* Убедимся, что фон установлен */
`; `;
// Правая панель для графа // Правая панель для графа
this.graphContainer = mainContainer.createDiv('llm-agent-graph-container') as HTMLDivElement; this.graphContainer = mainContainer.createDiv('llm-agent-graph-container') as HTMLDivElement;
this.graphContainer.style.cssText = ` this.graphContainer.style.cssText = `
flex: 1; flex: 1;
position: relative; position: relative;
background-color: var(--background-primary); /* Убедимся, что фон установлен */ background-color: var(--background-primary); /* Убедимся, что фон установлен */
`; `;
// Инициализируем панель истории // Инициализируем панель истории
this.historyPanel = new HistoryPanel(this.historyContainer, { this.historyPanel = new HistoryPanel(this.historyContainer, {
graphs: this.graphsList, graphs: this.graphsList,
onGraphSelect: this.handleGraphSelect.bind(this), onGraphSelect: this.handleGraphSelect.bind(this),
onNewChat: this.handleNewChat.bind(this) eventBus: this.plugin.eventBus
}); });
// Инициализируем React Flow // Инициализируем React Flow
this.initializeGraphPanel(); this.initializeGraphPanel();
// Подписываемся на события // Подписываемся на события
this.plugin.eventBus.on('graph-updated', this.handleGraphUpdate.bind(this)); this.plugin.eventBus.on('graph-updated', this.handleGraphUpdate.bind(this));
this.plugin.eventBus.on('new-chat', this.handleNewChat.bind(this)); this.plugin.eventBus.on('new-chat', this.handleNewChat.bind(this));
// Загружаем список графов // Загружаем список графов
await this.fetchGraphsList(); await this.fetchGraphsList();
} }
/** /**
* Вызывается при закрытии представления. Очищает ресурсы. * Вызывается при закрытии представления. Очищает ресурсы.
* @returns {Promise<void>} * @returns {Promise<void>}
*/ */
async onClose(): Promise<void> { async onClose(): Promise<void> {
if (this.historyPanel) { if (this.historyPanel) {
this.historyPanel.destroy(); this.historyPanel.destroy();
this.historyPanel = null; this.historyPanel = null;
} }
// Отмонтируем React-компонент // Отмонтируем React-компонент
if (this.reactRoot) { if (this.reactRoot) {
this.reactRoot.unmount(); this.reactRoot.unmount();
this.reactRoot = null; this.reactRoot = null;
} }
this.plugin.eventBus.off('graph-updated', this.handleGraphUpdate); this.plugin.eventBus.off('graph-updated', this.handleGraphUpdate);
this.plugin.eventBus.off('new-chat', this.handleNewChat); this.plugin.eventBus.off('new-chat', this.handleNewChat);
} }
// ----------------------------------------------- Graph Panel Integration -------------------------------------------------- // ----------------------------------------------- Graph Panel Integration --------------------------------------------------
/** /**
* Инициализирует GraphPanelComponent (React компонент) в graphContainer. * Инициализирует GraphPanelComponent (React компонент) в graphContainer.
* Использует ReactDOM.createRoot для React 18. * Использует ReactDOM.createRoot для React 18.
*/ */
initializeGraphPanel(): void { initializeGraphPanel(): void {
// Создаем контейнер для React Flow // Создаем контейнер для React Flow
// Убедимся, что id уникален или используем ref для React 18 // Убедимся, что id уникален или используем ref для React 18
this.graphContainer.empty(); // Очищаем контейнер перед добавлением нового элемента this.graphContainer.empty(); // Очищаем контейнер перед добавлением нового элемента
const wrapper = this.graphContainer.createDiv(); const wrapper = this.graphContainer.createDiv();
wrapper.style.cssText = 'width: 100%; height: 100%;'; wrapper.style.cssText = 'width: 100%; height: 100%;';
if (!wrapper) {
console.error('Не удалось создать wrapper для монтирования React Flow.');
return;
}
if (!wrapper) { // Создаем корневой элемент React 18
console.error('Не удалось создать wrapper для монтирования React Flow.'); if (ReactDOM && typeof ReactDOM.createRoot === 'function') {
return; this.reactRoot = ReactDOM.createRoot(wrapper);
} this.renderGraphPanel();
} else {
console.error('ReactDOM.createRoot не определен. Убедитесь, что "react-dom/client" правильно импортирован.');
}
}
// Создаем корневой элемент React 18 /**
if (ReactDOM && typeof ReactDOM.createRoot === 'function') { * Рендерит или обновляет GraphPanelComponent в DOM.
this.reactRoot = ReactDOM.createRoot(wrapper); */
this.renderGraphPanel(); renderGraphPanel(): void {
} else { if (this.reactRoot) {
console.error('ReactDOM.createRoot не определен. Убедитесь, что "react-dom/client" правильно импортирован.'); this.reactRoot.render(
} React.createElement(GraphPanelComponent, {
} graphData: {
nodes: this.graphData.nodes,
edges: this.graphData.edges
},
onNodeClick: this.handleNodeClick.bind(this)
})
);
} else {
console.warn('React root не инициализирован для рендера GraphPanelComponent.');
}
}
// ... (остальные методы API и Event Handlers без изменений)
// ----------------------------------------------- API Methods ---------------------------------------------------------------
/** /**
* Рендерит или обновляет GraphPanelComponent в DOM. * Загружает список всех графов с бэкенда.
*/ * @returns {Promise<void>}
renderGraphPanel(): void { */
if (this.reactRoot) { async fetchGraphsList(): Promise<void> {
this.reactRoot.render( try {
React.createElement(GraphPanelComponent, { const response = await fetch(`${this.plugin.settings.apiBaseUrl}/graphs`);
graphData: { if (!response.ok) {
nodes: this.graphData.nodes, const errorText = await response.text();
edges: this.graphData.edges throw new Error(`Ошибка загрузки списка графов: ${response.status} - ${errorText}`);
}, }
onNodeClick: this.handleNodeClick.bind(this) const data = await response.json();
}) this.graphsList = data;
); if (this.historyPanel) {
} else { this.historyPanel.updateGraphs(this.graphsList);
console.warn('React root не инициализирован для рендера GraphPanelComponent.'); }
} } catch (error) {
} console.error('Ошибка загрузки списка графов:', error);
// ... (остальные методы API и Event Handlers без изменений) // Optionally: show a notice to the user
// ----------------------------------------------- API Methods --------------------------------------------------------------- // new Notice(`Ошибка загрузки списка графов: ${error.message}`);
}
}
/** /**
* Загружает список всех графов с бэкенда. * Загружает данные конкретного графа по его ID.
* @returns {Promise<void>} * @param {string} graphId - ID графа для загрузки.
*/ * @returns {Promise<void>}
async fetchGraphsList(): Promise<void> { */
try { async fetchGraphData(graphId: string): Promise<void> {
const response = await fetch(`${this.plugin.settings.apiBaseUrl}/graphs`); try {
if (!response.ok) { const response = await fetch(`${this.plugin.settings.apiBaseUrl}/graphs/${graphId}`);
const errorText = await response.text(); if (!response.ok) {
throw new Error(`Ошибка загрузки списка графов: ${response.status} - ${errorText}`); const errorText = await response.text();
} throw new Error(`Ошибка загрузки данных графа: ${response.status} - ${errorText}`);
const data = await response.json(); }
this.graphsList = data; const data = await response.json();
if (this.historyPanel) {
this.historyPanel.updateGraphs(this.graphsList);
}
} catch (error) {
console.error('Ошибка загрузки списка графов:', error);
// Optionally: show a notice to the user
// new Notice(`Ошибка загрузки списка графов: ${error.message}`);
}
}
/** this.graphData = {
* Загружает данные конкретного графа по его ID. nodes: data.graph_nodes || [],
* @param {string} graphId - ID графа для загрузки. edges: data.graph_edges || []
* @returns {Promise<void>} };
*/ this.currentNode = data.current_node_id;
async fetchGraphData(graphId: string): Promise<void> {
try {
const response = await fetch(`${this.plugin.settings.apiBaseUrl}/graphs/${graphId}`);
if (!response.ok) {
const errorText = await response.text();
throw new Error(`Ошибка загрузки данных графа: ${response.status} - ${errorText}`);
}
const data = await response.json();
this.graphData = { // Перерендериваем граф
nodes: data.graph_nodes || [], this.renderGraphPanel();
edges: data.graph_edges || [] } catch (error) {
}; console.error('Ошибка загрузки данных графа:', error);
this.currentNode = data.current_node_id; // Optionally: show a notice to the user
// new Notice(`Ошибка загрузки данных графа: ${error.message}`);
}
}
// Перерендериваем граф // ----------------------------------------------- Event Handlers ------------------------------------------------------------
this.renderGraphPanel();
} catch (error) { /**
console.error('Ошибка загрузки данных графа:', error); * Обработчик клика по узлу графа.
// Optionally: show a notice to the user * Устанавливает текущий узел и генерирует событие 'node-selected'.
// new Notice(`Ошибка загрузки данных графа: ${error.message}`); * @param {string} nodeId - ID выбранного узла.
} */
} handleNodeClick(nodeId: string): void {
this.currentNode = nodeId;
this.plugin.eventBus.emit('node-selected', { nodeId });
}
// ----------------------------------------------- Event Handlers ------------------------------------------------------------ /**
* Обработчик обновления данных графа (например, после нового сообщения).
* Обновляет `graphData` и перерендеривает граф.
* @param {{ graphData: { nodes: any[], edges: any[] }, currentNode: string | null }} data - Обновленные данные графа.
*/
handleGraphUpdate(data: { graphData: { nodes: any[]; edges: any[] }; currentNode: string | null }): void {
this.graphData = data.graphData;
this.currentNode = data.currentNode;
this.renderGraphPanel();
}
/** /**
* Обработчик клика по узлу графа. * Обработчик начала нового чата.
* Устанавливает текущий узел и генерирует событие 'node-selected'. * Очищает текущие данные графа и перерендеривает его.
* @param {string} nodeId - ID выбранного узла. */
*/ handleNewChat(): void {
handleNodeClick(nodeId: string): void { this.graphData = { nodes: [], edges: [] };
this.currentNode = nodeId; this.currentNode = null;
this.plugin.eventBus.emit('node-selected', { nodeId }); this.selectedGraphId = null;
} this.renderGraphPanel();
this.fetchGraphsList(); // Обновить список графов, чтобы показать новый пустой чат
}
/** /**
* Обработчик обновления данных графа (например, после нового сообщения). * Обработчик выбора графа из панели истории.
* Обновляет `graphData` и перерендеривает граф. * Устанавливает выбранный граф и загружает его данные.
* @param {{ graphData: { nodes: any[], edges: any[] }, currentNode: string | null }} data - Обновленные данные графа. * @param {string} graphId - ID выбранного графа.
*/ */
handleGraphUpdate(data: { graphData: { nodes: any[], edges: any[] }, currentNode: string | null }): void { async handleGraphSelect(graphId: string): Promise<void> {
this.graphData = data.graphData; this.selectedGraphId = graphId;
this.currentNode = data.currentNode; await this.fetchGraphData(graphId);
this.renderGraphPanel(); // Также сообщаем ChatView, что выбран новый граф и он должен обновить свою историю
} this.plugin.eventBus.emit('graph-selected', { graphId, currentNode: this.currentNode });
}
/**
* Обработчик начала нового чата.
* Очищает текущие данные графа и перерендеривает его.
*/
handleNewChat(): void {
this.graphData = { nodes: [], edges: [] };
this.currentNode = null;
this.selectedGraphId = null;
this.renderGraphPanel();
this.fetchGraphsList(); // Обновить список графов, чтобы показать новый пустой чат
}
/**
* Обработчик выбора графа из панели истории.
* Устанавливает выбранный граф и загружает его данные.
* @param {string} graphId - ID выбранного графа.
*/
async handleGraphSelect(graphId: string): Promise<void> {
this.selectedGraphId = graphId;
await this.fetchGraphData(graphId);
// Также сообщаем ChatView, что выбран новый граф и он должен обновить свою историю
this.plugin.eventBus.emit('graph-selected', { graphId, currentNode: this.currentNode });
}
} }