work on separation / streaming
This commit is contained in:
parent
b0d802241b
commit
826b9222d3
9
main.ts
9
main.ts
|
|
@ -2,6 +2,7 @@ import { App, Editor, MarkdownView, Modal, Notice, Plugin, PluginSettingTab, Set
|
||||||
import { EventBus } from './src/utils/EventBus';
|
import { EventBus } from './src/utils/EventBus';
|
||||||
import { GraphView, GRAPH_VIEW_TYPE } from './src/views/GraphView';
|
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 'src/css/styles.css'; // Путь к твоему новому CSS файлу
|
import 'src/css/styles.css'; // Путь к твоему новому CSS файлу
|
||||||
|
|
||||||
|
|
@ -32,12 +33,16 @@ const DEFAULT_SETTINGS: LLMAgentSettings = {
|
||||||
export default class LLMAgentPlugin extends Plugin {
|
export default class LLMAgentPlugin extends Plugin {
|
||||||
settings: LLMAgentSettings;
|
settings: LLMAgentSettings;
|
||||||
eventBus: EventBus;
|
eventBus: EventBus;
|
||||||
|
webSocketService: WebSocketService;
|
||||||
|
|
||||||
async onload() {
|
async onload() {
|
||||||
await this.loadSettings();
|
await this.loadSettings();
|
||||||
|
|
||||||
this.eventBus = new EventBus();
|
this.eventBus = new EventBus();
|
||||||
|
|
||||||
|
this.webSocketService = new WebSocketService(this.eventBus, this.settings.apiBaseUrl);
|
||||||
|
this.webSocketService.connect();
|
||||||
|
|
||||||
// Регистрируем кастомные views
|
// Регистрируем кастомные views
|
||||||
this.registerView(GRAPH_VIEW_TYPE, (leaf) => new GraphView(leaf, this));
|
this.registerView(GRAPH_VIEW_TYPE, (leaf) => new GraphView(leaf, this));
|
||||||
this.registerView(CHAT_VIEW_TYPE, (leaf) => new ChatView(leaf, this));
|
this.registerView(CHAT_VIEW_TYPE, (leaf) => new ChatView(leaf, this));
|
||||||
|
|
@ -74,6 +79,10 @@ export default class LLMAgentPlugin extends Plugin {
|
||||||
}
|
}
|
||||||
|
|
||||||
onunload() {
|
onunload() {
|
||||||
|
if (this.webSocketService) {
|
||||||
|
this.webSocketService.disconnect();
|
||||||
|
}
|
||||||
|
|
||||||
console.log('LLM Agent plugin выгружен');
|
console.log('LLM Agent plugin выгружен');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
133
package-lock.json
generated
133
package-lock.json
generated
|
|
@ -8,6 +8,9 @@
|
||||||
"name": "obsidian-sample-plugin",
|
"name": "obsidian-sample-plugin",
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"socket.io-client": "^4.8.1"
|
||||||
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/dagre": "^0.7.52",
|
"@types/dagre": "^0.7.52",
|
||||||
"@types/node": "^16.11.6",
|
"@types/node": "^16.11.6",
|
||||||
|
|
@ -646,6 +649,11 @@
|
||||||
"react-dom": ">=17"
|
"react-dom": ">=17"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@socket.io/component-emitter": {
|
||||||
|
"version": "3.1.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/@socket.io/component-emitter/-/component-emitter-3.1.2.tgz",
|
||||||
|
"integrity": "sha512-9BCxFwvbGg/RsZK9tjXd8s4UcwR0MWeFQ1XEKIQVVvAGJyINdrqKMcTRyLoK8Rse1GjzLV9cwjWV1olXRWEXVA=="
|
||||||
|
},
|
||||||
"node_modules/@types/codemirror": {
|
"node_modules/@types/codemirror": {
|
||||||
"version": "5.60.8",
|
"version": "5.60.8",
|
||||||
"resolved": "https://registry.npmjs.org/@types/codemirror/-/codemirror-5.60.8.tgz",
|
"resolved": "https://registry.npmjs.org/@types/codemirror/-/codemirror-5.60.8.tgz",
|
||||||
|
|
@ -1539,6 +1547,42 @@
|
||||||
"node": ">=6.0.0"
|
"node": ">=6.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/engine.io-client": {
|
||||||
|
"version": "6.6.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/engine.io-client/-/engine.io-client-6.6.3.tgz",
|
||||||
|
"integrity": "sha512-T0iLjnyNWahNyv/lcjS2y4oE358tVS/SYQNxYXGAJ9/GLgH4VCvOQ/mhTjqU88mLZCQgiG8RIegFHYCdVC+j5w==",
|
||||||
|
"dependencies": {
|
||||||
|
"@socket.io/component-emitter": "~3.1.0",
|
||||||
|
"debug": "~4.3.1",
|
||||||
|
"engine.io-parser": "~5.2.1",
|
||||||
|
"ws": "~8.17.1",
|
||||||
|
"xmlhttprequest-ssl": "~2.1.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/engine.io-client/node_modules/debug": {
|
||||||
|
"version": "4.3.7",
|
||||||
|
"resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz",
|
||||||
|
"integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==",
|
||||||
|
"dependencies": {
|
||||||
|
"ms": "^2.1.3"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=6.0"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"supports-color": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/engine.io-parser": {
|
||||||
|
"version": "5.2.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/engine.io-parser/-/engine.io-parser-5.2.3.tgz",
|
||||||
|
"integrity": "sha512-HqD3yTBfnBxIrbnM1DoD6Pcq8NECnh8d4As1Qgh0z5Gg3jRRIqijury0CL3ghu/edArpUYiYqQiDUQBIs4np3Q==",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=10.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/esbuild": {
|
"node_modules/esbuild": {
|
||||||
"version": "0.17.3",
|
"version": "0.17.3",
|
||||||
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.17.3.tgz",
|
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.17.3.tgz",
|
||||||
|
|
@ -2292,8 +2336,7 @@
|
||||||
"node_modules/ms": {
|
"node_modules/ms": {
|
||||||
"version": "2.1.3",
|
"version": "2.1.3",
|
||||||
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
|
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
|
||||||
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
|
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="
|
||||||
"dev": true
|
|
||||||
},
|
},
|
||||||
"node_modules/natural-compare": {
|
"node_modules/natural-compare": {
|
||||||
"version": "1.4.0",
|
"version": "1.4.0",
|
||||||
|
|
@ -2648,6 +2691,64 @@
|
||||||
"node": ">=8"
|
"node": ">=8"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/socket.io-client": {
|
||||||
|
"version": "4.8.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/socket.io-client/-/socket.io-client-4.8.1.tgz",
|
||||||
|
"integrity": "sha512-hJVXfu3E28NmzGk8o1sHhN3om52tRvwYeidbj7xKy2eIIse5IoKX3USlS6Tqt3BHAtflLIkCQBkzVrEEfWUyYQ==",
|
||||||
|
"dependencies": {
|
||||||
|
"@socket.io/component-emitter": "~3.1.0",
|
||||||
|
"debug": "~4.3.2",
|
||||||
|
"engine.io-client": "~6.6.1",
|
||||||
|
"socket.io-parser": "~4.2.4"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=10.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/socket.io-client/node_modules/debug": {
|
||||||
|
"version": "4.3.7",
|
||||||
|
"resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz",
|
||||||
|
"integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==",
|
||||||
|
"dependencies": {
|
||||||
|
"ms": "^2.1.3"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=6.0"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"supports-color": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/socket.io-parser": {
|
||||||
|
"version": "4.2.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-4.2.4.tgz",
|
||||||
|
"integrity": "sha512-/GbIKmo8ioc+NIWIhwdecY0ge+qVBSMdgxGygevmdHj24bsfgtCmcUUcQ5ZzcylGFHsN3k4HB4Cgkl96KVnuew==",
|
||||||
|
"dependencies": {
|
||||||
|
"@socket.io/component-emitter": "~3.1.0",
|
||||||
|
"debug": "~4.3.1"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=10.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/socket.io-parser/node_modules/debug": {
|
||||||
|
"version": "4.3.7",
|
||||||
|
"resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz",
|
||||||
|
"integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==",
|
||||||
|
"dependencies": {
|
||||||
|
"ms": "^2.1.3"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=6.0"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"supports-color": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/strip-ansi": {
|
"node_modules/strip-ansi": {
|
||||||
"version": "6.0.1",
|
"version": "6.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
|
||||||
|
|
@ -2838,6 +2939,34 @@
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"peer": true
|
"peer": true
|
||||||
},
|
},
|
||||||
|
"node_modules/ws": {
|
||||||
|
"version": "8.17.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/ws/-/ws-8.17.1.tgz",
|
||||||
|
"integrity": "sha512-6XQFvXTkbfUOZOKKILFG1PDK2NDQs4azKQl26T0YS5CxqWLgXajbPZ+h4gZekJyRqFU8pvnbAbbs/3TgRPy+GQ==",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=10.0.0"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"bufferutil": "^4.0.1",
|
||||||
|
"utf-8-validate": ">=5.0.2"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"bufferutil": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"utf-8-validate": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/xmlhttprequest-ssl": {
|
||||||
|
"version": "2.1.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/xmlhttprequest-ssl/-/xmlhttprequest-ssl-2.1.2.tgz",
|
||||||
|
"integrity": "sha512-TEU+nJVUUnA4CYJFLvK5X9AOeH4KvDvhIfm0vV1GaQRtchnG0hgK5p8hw/xjv8cunWYCsiPCSDzObPyhEwq3KQ==",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=0.4.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/yocto-queue": {
|
"node_modules/yocto-queue": {
|
||||||
"version": "0.1.0",
|
"version": "0.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz",
|
||||||
|
|
|
||||||
|
|
@ -27,5 +27,8 @@
|
||||||
"reactflow": "^11.11.3",
|
"reactflow": "^11.11.3",
|
||||||
"tslib": "^2.4.0",
|
"tslib": "^2.4.0",
|
||||||
"typescript": "^5.4.5"
|
"typescript": "^5.4.5"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"socket.io-client": "^4.8.1"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -923,6 +923,21 @@ export class ChatPanel {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Кнопка "Регенерировать"
|
||||||
|
const regenerateButton = toolsetContainer.createEl('button', {
|
||||||
|
cls: 'text-token-text-secondary hover:bg-token-bg-secondary rounded-lg ml-1',
|
||||||
|
attr: {
|
||||||
|
'aria-label': 'Регенерировать сообщение',
|
||||||
|
'data-testid': 'regenerate-turn-action-button',
|
||||||
|
'data-state': 'closed'
|
||||||
|
}
|
||||||
|
});
|
||||||
|
const regenerateIconContainer = regenerateButton.createSpan({ cls: 'flex items-center justify-center touch:w-10 h-8 w-8' });
|
||||||
|
setIcon(regenerateIconContainer, 'refresh-cw');
|
||||||
|
regenerateButton.addEventListener('click', async () => {
|
||||||
|
this.props.plugin.eventBus.emit('regenerate-message', { graphId, nodeId });
|
||||||
|
});
|
||||||
|
|
||||||
// Кнопка "Попробовать снова"
|
// Кнопка "Попробовать снова"
|
||||||
/*const retryButton = toolsetContainer.createEl('button', {
|
/*const retryButton = toolsetContainer.createEl('button', {
|
||||||
cls: 'text-token-text-secondary hover:bg-token-bg-secondary rounded-lg',
|
cls: 'text-token-text-secondary hover:bg-token-bg-secondary rounded-lg',
|
||||||
|
|
|
||||||
|
|
@ -45,6 +45,7 @@ interface BaseNodeWrapperData {
|
||||||
title_generated?: boolean;
|
title_generated?: boolean;
|
||||||
isSelected: boolean;
|
isSelected: boolean;
|
||||||
nodeId: string;
|
nodeId: string;
|
||||||
|
graphId: string;
|
||||||
onNodeDelete: (nodeId: string) => Promise<void>;
|
onNodeDelete: (nodeId: string) => Promise<void>;
|
||||||
app: App;
|
app: App;
|
||||||
}
|
}
|
||||||
|
|
@ -66,59 +67,83 @@ interface BaseNodeWrapperProps {
|
||||||
*/
|
*/
|
||||||
const BaseNodeWrapper: React.FC<BaseNodeWrapperProps> = ({ data, isConnectable, children }) => {
|
const BaseNodeWrapper: React.FC<BaseNodeWrapperProps> = ({ data, isConnectable, children }) => {
|
||||||
// ----------------------------------------------- Styles --------------------------------------------------------------------
|
// ----------------------------------------------- Styles --------------------------------------------------------------------
|
||||||
// Стили для кнопки удаления. Вынесены для переиспользования.
|
// Стили для кнопки удаления. Вынесены для переиспользования.
|
||||||
const deleteButtonStyle: React.CSSProperties = {
|
const deleteButtonStyle: React.CSSProperties = {
|
||||||
position: 'absolute',
|
position: 'absolute',
|
||||||
bottom: '-30px', // Размещаем под узлом
|
bottom: '-30px', // Размещаем под узлом
|
||||||
left: '5px',
|
left: '5px',
|
||||||
background: 'transparent',
|
background: 'transparent',
|
||||||
border: 'none',
|
border: 'none',
|
||||||
cursor: 'pointer',
|
cursor: 'pointer',
|
||||||
fontSize: '1em',
|
fontSize: '1em',
|
||||||
color: 'var(--text-muted)', // Используем цвет текста Obsidian
|
color: 'var(--text-muted)', // Используем цвет текста Obsidian
|
||||||
padding: '0 5px',
|
padding: '0 5px',
|
||||||
opacity: data.isSelected ? 1 : 0, // Показываем только если узел выделен
|
opacity: data.isSelected ? 1 : 0, // Показываем только если узел выделен
|
||||||
transition: 'opacity 0.2s ease-in-out',
|
transition: 'opacity 0.2s ease-in-out'
|
||||||
};
|
};
|
||||||
|
|
||||||
// ----------------------------------------------- Event Handlers ------------------------------------------------------------
|
// ----------------------------------------------- Event Handlers ------------------------------------------------------------
|
||||||
/**
|
/**
|
||||||
* Обработчик удаления узла.
|
* Обработчик удаления узла.
|
||||||
* @param {React.MouseEvent} event - Событие клика.
|
* @param {React.MouseEvent} event - Событие клика.
|
||||||
*/
|
*/
|
||||||
const handleNodeDelete = async (event: React.MouseEvent) => {
|
const handleNodeDelete = async (event: React.MouseEvent) => {
|
||||||
event.stopPropagation(); // Предотвращаем срабатывание onNodeClick на родительском узле
|
event.stopPropagation(); // Предотвращаем срабатывание onNodeClick на родительском узле
|
||||||
|
|
||||||
const confirmed = await Dialog.confirm(
|
const confirmed = await Dialog.confirm(data.app, 'Вы уверены, что хотите удалить этот узел?');
|
||||||
data.app,
|
|
||||||
'Вы уверены, что хотите удалить этот узел?'
|
|
||||||
);
|
|
||||||
if (confirmed) {
|
if (confirmed) {
|
||||||
await data.onNodeDelete(data.nodeId);
|
await data.onNodeDelete(data.nodeId);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleRegenerate = async (event: React.MouseEvent) => {
|
||||||
|
event.stopPropagation();
|
||||||
|
// Используем EventBus вместо workspace.trigger
|
||||||
|
const eventBus = (data.app as any).plugins?.plugins?.['llm-agent-plugin']?.eventBus;
|
||||||
|
if (eventBus) {
|
||||||
|
eventBus.emit('regenerate-message', {
|
||||||
|
graphId: data.graphId,
|
||||||
|
nodeId: data.nodeId
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
// ----------------------------------------------- Render --------------------------------------------------------------------
|
// ----------------------------------------------- Render --------------------------------------------------------------------
|
||||||
return React.createElement(
|
return React.createElement(
|
||||||
React.Fragment,
|
React.Fragment,
|
||||||
null,
|
null,
|
||||||
React.createElement(Handle, {
|
React.createElement(Handle, {
|
||||||
type: 'source',
|
type: 'source',
|
||||||
position: Position.Bottom,
|
position: Position.Bottom,
|
||||||
isConnectable: isConnectable
|
isConnectable: isConnectable
|
||||||
}),
|
}),
|
||||||
React.createElement(Handle, {
|
React.createElement(Handle, {
|
||||||
type: 'target',
|
type: 'target',
|
||||||
position: Position.Top,
|
position: Position.Top,
|
||||||
isConnectable: isConnectable
|
isConnectable: isConnectable
|
||||||
}),
|
}),
|
||||||
children, // Здесь будет отображаться специфическое содержимое узла
|
children, // Здесь будет отображаться специфическое содержимое узла
|
||||||
data.isSelected && React.createElement('button', {
|
data.isSelected && React.createElement('div', {
|
||||||
style: deleteButtonStyle,
|
style: {
|
||||||
onClick: handleNodeDelete,
|
position: 'absolute',
|
||||||
'aria-label': 'Удалить узел',
|
bottom: '-30px',
|
||||||
className: 'node-delete-button' // Добавляем класс для стилизации
|
left: '5px',
|
||||||
}, '🗑️') // Можно использовать SVG иконку
|
display: 'flex',
|
||||||
|
gap: '5px'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
React.createElement('button', {
|
||||||
|
style: deleteButtonStyle,
|
||||||
|
onClick: handleNodeDelete,
|
||||||
|
'aria-label': 'Удалить узел',
|
||||||
|
className: 'node-delete-button'
|
||||||
|
}, '🗑️'),
|
||||||
|
React.createElement('button', {
|
||||||
|
style: deleteButtonStyle,
|
||||||
|
onClick: handleRegenerate,
|
||||||
|
'aria-label': 'Регенерировать',
|
||||||
|
className: 'node-regenerate-button'
|
||||||
|
}, '🔄')
|
||||||
|
)
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -294,7 +319,14 @@ interface GraphPanelProps {
|
||||||
* @param {GraphPanelProps} props - Свойства компонента.
|
* @param {GraphPanelProps} props - Свойства компонента.
|
||||||
* @returns {React.ReactElement} Элемент React.
|
* @returns {React.ReactElement} Элемент React.
|
||||||
*/
|
*/
|
||||||
export const GraphPanelComponent: React.FC<GraphPanelProps> = ({ graphData, onNodeClick, onNodeSelectionChange, onNodeDelete, selectedNodeIds, app }) => {
|
export const GraphPanelComponent: React.FC<GraphPanelProps> = ({
|
||||||
|
graphData,
|
||||||
|
onNodeClick,
|
||||||
|
onNodeSelectionChange,
|
||||||
|
onNodeDelete,
|
||||||
|
selectedNodeIds,
|
||||||
|
app
|
||||||
|
}) => {
|
||||||
// ----------------------------------------------- React Hooks ---------------------------------------------------------------
|
// ----------------------------------------------- React Hooks ---------------------------------------------------------------
|
||||||
// Инициализируем состояния для узлов и рёбер React Flow
|
// Инициализируем состояния для узлов и рёбер React Flow
|
||||||
const [nodes, setNodes, onNodesChange] = useNodesState([]);
|
const [nodes, setNodes, onNodesChange] = useNodesState([]);
|
||||||
|
|
@ -313,16 +345,14 @@ export const GraphPanelComponent: React.FC<GraphPanelProps> = ({ graphData, onNo
|
||||||
label: node.data?.label || `Node ${node.id}`,
|
label: node.data?.label || `Node ${node.id}`,
|
||||||
title: node.data?.title,
|
title: node.data?.title,
|
||||||
title_generated: node.data?.title_generated || false,
|
title_generated: node.data?.title_generated || false,
|
||||||
// Передаем колбэк onNodeDelete в данные узла, чтобы он был доступен в CustomNode
|
|
||||||
onNodeDelete: onNodeDelete,
|
onNodeDelete: onNodeDelete,
|
||||||
isSelected: selectedNodeIds.has(node.id), // Передаем состояние выделения
|
isSelected: selectedNodeIds.has(node.id),
|
||||||
// Добавляем graphId и nodeId для кнопки удаления
|
|
||||||
graphId: (node.data && node.data.message && node.data.message.graph_id) ? node.data.message.graph_id : 'unknown',
|
graphId: (node.data && node.data.message && node.data.message.graph_id) ? node.data.message.graph_id : 'unknown',
|
||||||
nodeId: node.id,
|
nodeId: node.id,
|
||||||
app: app
|
app: app
|
||||||
},
|
},
|
||||||
position: { x: 0, y: 0 },
|
position: { x: 0, y: 0 },
|
||||||
className: selectedNodeIds.has(node.id) ? 'selected-node' : '', // Применяем класс для выделения
|
className: selectedNodeIds.has(node.id) ? 'selected-node' : '',
|
||||||
}));
|
}));
|
||||||
|
|
||||||
const initialEdgesForLayout = graphData.edges.map((edge) => ({
|
const initialEdgesForLayout = graphData.edges.map((edge) => ({
|
||||||
|
|
@ -364,43 +394,46 @@ export const GraphPanelComponent: React.FC<GraphPanelProps> = ({ graphData, onNo
|
||||||
const nodeClickHandler = React.useCallback(
|
const nodeClickHandler = React.useCallback(
|
||||||
(event: React.MouseEvent, node: any) => {
|
(event: React.MouseEvent, node: any) => {
|
||||||
if (event.shiftKey) {
|
if (event.shiftKey) {
|
||||||
// Если зажат Shift, переключаем выделение
|
// Если зажат Shift, переключаем выделение
|
||||||
onNodeSelectionChange(node.id, !selectedNodeIds.has(node.id));
|
onNodeSelectionChange(node.id, !selectedNodeIds.has(node.id));
|
||||||
} else {
|
} else {
|
||||||
// Иначе, это обычный клик по узлу
|
// Иначе, это обычный клик по узлу
|
||||||
onNodeClick(node.id);
|
onNodeClick(node.id);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[onNodeClick, onNodeSelectionChange, selectedNodeIds]
|
[onNodeClick, onNodeSelectionChange, selectedNodeIds]
|
||||||
);
|
);
|
||||||
|
|
||||||
// Добавляем обработчик для ReactFlow, чтобы отслеживать клики по пустому пространству
|
// Добавляем обработчик для ReactFlow, чтобы отслеживать клики по пустому пространству
|
||||||
const onPaneClick = React.useCallback((event: React.MouseEvent) => {
|
const onPaneClick = React.useCallback(
|
||||||
// Проверяем, был ли клик по пустому пространству (не по узлу или ребру)
|
(event: React.MouseEvent) => {
|
||||||
const target = event.target as HTMLElement;
|
// Проверяем, был ли клик по пустому пространству (не по узлу или ребру)
|
||||||
if (!target.closest('.react-flow__node') && !target.closest('.react-flow__edge')) {
|
const target = event.target as HTMLElement;
|
||||||
// Если клик был не по узлу и не по ребру, сбрасываем выделение
|
if (!target.closest('.react-flow__node') && !target.closest('.react-flow__edge')) {
|
||||||
if (selectedNodeIds.size > 0) {
|
// Если клик был не по узлу и не по ребру, сбрасываем выделение
|
||||||
// Вызываем колбэк для сброса всех выделений
|
if (selectedNodeIds.size > 0) {
|
||||||
selectedNodeIds.forEach(nodeId => onNodeSelectionChange(nodeId, false));
|
// Вызываем колбэк для сброса всех выделений
|
||||||
}
|
selectedNodeIds.forEach((nodeId) => onNodeSelectionChange(nodeId, false));
|
||||||
}
|
}
|
||||||
}, [selectedNodeIds, onNodeSelectionChange]);
|
}
|
||||||
|
},
|
||||||
|
[selectedNodeIds, onNodeSelectionChange]
|
||||||
|
);
|
||||||
|
|
||||||
// Обработчик для клавиатуры (Escape)
|
// Обработчик для клавиатуры (Escape)
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
const handleKeyDown = (event: KeyboardEvent) => {
|
const handleKeyDown = (event: KeyboardEvent) => {
|
||||||
if (event.key === 'Escape') {
|
if (event.key === 'Escape') {
|
||||||
if (selectedNodeIds.size > 0) {
|
if (selectedNodeIds.size > 0) {
|
||||||
selectedNodeIds.forEach(nodeId => onNodeSelectionChange(nodeId, false));
|
selectedNodeIds.forEach((nodeId) => onNodeSelectionChange(nodeId, false));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
document.addEventListener('keydown', handleKeyDown);
|
document.addEventListener('keydown', handleKeyDown);
|
||||||
return () => {
|
return () => {
|
||||||
document.removeEventListener('keydown', handleKeyDown);
|
document.removeEventListener('keydown', handleKeyDown);
|
||||||
};
|
};
|
||||||
}, [selectedNodeIds, onNodeSelectionChange]);
|
}, [selectedNodeIds, onNodeSelectionChange]);
|
||||||
|
|
||||||
// ----------------------------------------------- Render --------------------------------------------------------------------
|
// ----------------------------------------------- Render --------------------------------------------------------------------
|
||||||
return React.createElement(
|
return React.createElement(
|
||||||
|
|
@ -419,7 +452,7 @@ export const GraphPanelComponent: React.FC<GraphPanelProps> = ({ graphData, onNo
|
||||||
onEdgesChange, // Обработчик изменения рёбер
|
onEdgesChange, // Обработчик изменения рёбер
|
||||||
onConnect, // Обработчик подключения
|
onConnect, // Обработчик подключения
|
||||||
onNodeClick: nodeClickHandler, // Обработчик клика по узлу
|
onNodeClick: nodeClickHandler, // Обработчик клика по узлу
|
||||||
onPaneClick: onPaneClick, // Обработчик клика по панели
|
onPaneClick: onPaneClick, // Обработчик клика по панели
|
||||||
fitView: true, // Автоматически центрирует и масштабирует граф при загрузке
|
fitView: true, // Автоматически центрирует и масштабирует граф при загрузке
|
||||||
attributionPosition: 'bottom-right', // Позиция атрибуции React Flow
|
attributionPosition: 'bottom-right', // Позиция атрибуции React Flow
|
||||||
nodeTypes,
|
nodeTypes,
|
||||||
|
|
|
||||||
85
src/utils/WebSocketService.ts
Normal file
85
src/utils/WebSocketService.ts
Normal file
|
|
@ -0,0 +1,85 @@
|
||||||
|
import { io, Socket } from 'socket.io-client';
|
||||||
|
import { EventBus } from './EventBus';
|
||||||
|
|
||||||
|
export class WebSocketService {
|
||||||
|
private socket: Socket | null = null;
|
||||||
|
private eventBus: EventBus;
|
||||||
|
private apiBaseUrl: string;
|
||||||
|
|
||||||
|
constructor(eventBus: EventBus, apiBaseUrl: string) {
|
||||||
|
this.eventBus = eventBus;
|
||||||
|
this.apiBaseUrl = apiBaseUrl;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Подключается к WebSocket серверу
|
||||||
|
*/
|
||||||
|
connect(): void {
|
||||||
|
if (this.socket?.connected) {
|
||||||
|
console.log('WebSocket уже подключен');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Извлекаем базовый URL без /api
|
||||||
|
const wsUrl = this.apiBaseUrl.replace('/api', '');
|
||||||
|
|
||||||
|
this.socket = io(wsUrl, {
|
||||||
|
transports: ['websocket', 'polling'],
|
||||||
|
reconnection: true,
|
||||||
|
reconnectionDelay: 1000,
|
||||||
|
reconnectionDelayMax: 5000,
|
||||||
|
reconnectionAttempts: 5
|
||||||
|
});
|
||||||
|
|
||||||
|
this.setupEventHandlers();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Настраивает обработчики WebSocket событий
|
||||||
|
*/
|
||||||
|
private setupEventHandlers(): void {
|
||||||
|
if (!this.socket) return;
|
||||||
|
|
||||||
|
this.socket.on('connect', () => {
|
||||||
|
console.log('✅ WebSocket подключен');
|
||||||
|
});
|
||||||
|
|
||||||
|
this.socket.on('disconnect', (reason) => {
|
||||||
|
console.log('❌ WebSocket отключен:', reason);
|
||||||
|
});
|
||||||
|
|
||||||
|
this.socket.on('connect_error', (error) => {
|
||||||
|
console.error('Ошибка подключения WebSocket:', error);
|
||||||
|
});
|
||||||
|
|
||||||
|
// ----------------------------------------------- Title Update Events ---------------------------------------------------------------
|
||||||
|
|
||||||
|
this.socket.on('graph_title_updated', (data: { graph_id: string; title: string }) => {
|
||||||
|
console.log('📊 Получено обновление заголовка графа:', data);
|
||||||
|
this.eventBus.emit('ws-graph-title-updated', data);
|
||||||
|
});
|
||||||
|
|
||||||
|
this.socket.on('node_title_updated', (data: { graph_id: string; node_id: string; title: string }) => {
|
||||||
|
console.log('📝 Получено обновление заголовка узла:', data);
|
||||||
|
this.eventBus.emit('ws-node-title-updated', data);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Отключается от WebSocket сервера
|
||||||
|
*/
|
||||||
|
disconnect(): void {
|
||||||
|
if (this.socket) {
|
||||||
|
this.socket.disconnect();
|
||||||
|
this.socket = null;
|
||||||
|
console.log('WebSocket отключен');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Проверяет статус подключения
|
||||||
|
*/
|
||||||
|
isConnected(): boolean {
|
||||||
|
return this.socket?.connected ?? false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -3,7 +3,7 @@
|
||||||
* отправляет запросы LLM-агенту и обрабатывает ответы, обновляя состояние чата и графа.
|
* отправляет запросы LLM-агенту и обрабатывает ответы, обновляя состояние чата и графа.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { ItemView, WorkspaceLeaf } from 'obsidian';
|
import { ItemView, Notice, WorkspaceLeaf } from 'obsidian';
|
||||||
import { ChatPanel } from '../components/ChatPanel';
|
import { ChatPanel } from '../components/ChatPanel';
|
||||||
import LLMAgentPlugin from 'main';
|
import LLMAgentPlugin from 'main';
|
||||||
|
|
||||||
|
|
@ -69,7 +69,9 @@ 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));
|
||||||
|
this.plugin.eventBus.on('regenerate-message', this.handleRegenerateMessage.bind(this)); // Добавлено
|
||||||
}
|
}
|
||||||
|
|
||||||
async onClose(): Promise<void> {
|
async onClose(): Promise<void> {
|
||||||
if (this.chatPanel) {
|
if (this.chatPanel) {
|
||||||
this.chatPanel.destroy();
|
this.chatPanel.destroy();
|
||||||
|
|
@ -78,6 +80,7 @@ export class ChatView extends ItemView {
|
||||||
this.plugin.eventBus.off('node-selected', this.handleNodeSelected);
|
this.plugin.eventBus.off('node-selected', this.handleNodeSelected);
|
||||||
this.plugin.eventBus.off('new-chat', this.handleNewChat);
|
this.plugin.eventBus.off('new-chat', this.handleNewChat);
|
||||||
this.plugin.eventBus.off('graph-selected', this.handleGraphSelected);
|
this.plugin.eventBus.off('graph-selected', this.handleGraphSelected);
|
||||||
|
this.plugin.eventBus.off('regenerate-message', this.handleRegenerateMessage);
|
||||||
}
|
}
|
||||||
|
|
||||||
// ----------------------------------------------- API Methods ---------------------------------------------------------------
|
// ----------------------------------------------- API Methods ---------------------------------------------------------------
|
||||||
|
|
@ -145,62 +148,42 @@ export class ChatView extends ItemView {
|
||||||
|
|
||||||
async handleSendMessage(message: string, selectedModel?: string): Promise<void> {
|
async handleSendMessage(message: string, selectedModel?: string): Promise<void> {
|
||||||
try {
|
try {
|
||||||
const response = await fetch(`${this.plugin.settings.apiBaseUrl}/chat`, {
|
// Шаг 1: Создаём узел пользователя
|
||||||
|
const sendResponse = await fetch(`${this.plugin.settings.apiBaseUrl}/chat/send`, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: {
|
headers: { 'Content-Type': 'application/json' },
|
||||||
'Content-Type': 'application/json'
|
|
||||||
},
|
|
||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
message: message,
|
message: message,
|
||||||
graph_id: this.selectedGraphId,
|
graph_id: this.selectedGraphId,
|
||||||
parent_node_id: this.currentNode,
|
parent_node_id: this.currentNode
|
||||||
system_prompt: this.plugin.settings.systemPrompt,
|
|
||||||
model: selectedModel || 'gemini-2.5-flash'
|
|
||||||
})
|
})
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!response.ok) {
|
if (!sendResponse.ok) {
|
||||||
// Пытаемся прочитать ошибку из тела ответа, если это возможно
|
throw new Error(`Ошибка при отправке сообщения: ${sendResponse.status}`);
|
||||||
const errorData = await response.json().catch(() => ({ message: response.statusText }));
|
|
||||||
throw new Error(`HTTP ошибка при отправке сообщения! Статус: ${response.status} - ${errorData.message}`);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const result = await response.json();
|
const sendResult = await sendResponse.json();
|
||||||
|
this.selectedGraphId = sendResult.graph_id;
|
||||||
|
const userNodeId = sendResult.node_id;
|
||||||
|
|
||||||
// Обновляем graph_id и current_node_id всегда
|
// Обновляем граф с новым узлом пользователя
|
||||||
this.selectedGraphId = result.graph_id;
|
this.plugin.eventBus.emit('graph-selected', {
|
||||||
const newCurrentNodeId = result.graph_visualization_data.current_node_id;
|
graphId: this.selectedGraphId,
|
||||||
this.currentNode = newCurrentNodeId;
|
currentNode: userNodeId
|
||||||
|
});
|
||||||
|
|
||||||
// Сначала обновим GraphView, чтобы он запросил последнюю версию графа
|
// Шаг 2: Запускаем стриминг ответа
|
||||||
// Это предотвратит состояние гонки, когда разные запросы возвращают частичные графы.
|
if (this.selectedGraphId) {
|
||||||
// Вместо того, чтобы отправлять частичные данные, мы запрашиваем полный граф у бэкенда.
|
// Проверка на null
|
||||||
this.plugin.eventBus.emit('graph-selected', { graphId: this.selectedGraphId, currentNode: this.currentNode });
|
await this.streamLLMResponse(this.selectedGraphId, userNodeId, selectedModel);
|
||||||
|
} else {
|
||||||
// TODO: сделать аккуратнее - устанавливаем таймер на 30 секунд для обновления заголовков
|
throw new Error('Graph ID не определён после отправки сообщения');
|
||||||
this.titleUpdateTimer = setTimeout(() => {
|
|
||||||
console.log('Обновление графа для получения сгенерированных заголовков...');
|
|
||||||
if (this.selectedGraphId && this.currentNode) {
|
|
||||||
this.plugin.eventBus.emit('graph-selected', {
|
|
||||||
graphId: this.selectedGraphId,
|
|
||||||
currentNode: this.currentNode
|
|
||||||
});
|
|
||||||
}
|
|
||||||
this.titleUpdateTimer = null;
|
|
||||||
}, 20000); // 20 секунд, эмпирически
|
|
||||||
|
|
||||||
// Затем обновим ChatView, загрузив историю до нового current_node_id
|
|
||||||
if (newCurrentNodeId) {
|
|
||||||
await this.fetchMessagesFromRootToNode(newCurrentNodeId);
|
|
||||||
} else if (this.selectedGraphId) {
|
|
||||||
// Если newCurrentNodeId по какой-то причине не вернулся, но graph_id есть,
|
|
||||||
// пытаемся загрузить историю по последнему известному узлу или просто весь граф (чтобы отобразить пустую историю, если граф новый).
|
|
||||||
// В этом случае, если current_node_id отсутствует, historyPanel должна очиститься.
|
|
||||||
this.currentNode = null; // Сбрасываем, если no current_node_id
|
|
||||||
this.fetchMessagesFromRootToNode(''); // Передаем пустую строку или null, чтобы очистить историю
|
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error: any) {
|
||||||
|
// Указываем тип any для error
|
||||||
console.error('Ошибка отправки сообщения:', error);
|
console.error('Ошибка отправки сообщения:', error);
|
||||||
|
new Notice(`Ошибка: ${error?.message || error}`, 5000);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -240,4 +223,140 @@ export class ChatView extends ItemView {
|
||||||
this.currentNode = data.currentNode; // Может быть пустым, но это не проблема
|
this.currentNode = data.currentNode; // Может быть пустым, но это не проблема
|
||||||
await this.fetchMessagesFromRootToNode(data.currentNode);
|
await this.fetchMessagesFromRootToNode(data.currentNode);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async streamLLMResponse(graphId: string, userNodeId: string, selectedModel?: string): Promise<void> {
|
||||||
|
const params = new URLSearchParams({
|
||||||
|
graph_id: graphId,
|
||||||
|
user_node_id: userNodeId,
|
||||||
|
system_prompt: this.plugin.settings.systemPrompt || '',
|
||||||
|
model: selectedModel || this.plugin.settings.defaultModel
|
||||||
|
});
|
||||||
|
|
||||||
|
const response = await fetch(`${this.plugin.settings.apiBaseUrl}/chat/stream`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({
|
||||||
|
graph_id: graphId,
|
||||||
|
user_node_id: userNodeId,
|
||||||
|
system_prompt: this.plugin.settings.systemPrompt,
|
||||||
|
model: selectedModel || this.plugin.settings.defaultModel
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(`Ошибка стриминга: ${response.status}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const reader = response.body?.getReader();
|
||||||
|
if (!reader) {
|
||||||
|
throw new Error('Response body is null');
|
||||||
|
}
|
||||||
|
|
||||||
|
const decoder = new TextDecoder();
|
||||||
|
let assistantNodeId: string | null = null;
|
||||||
|
let accumulatedContent = '';
|
||||||
|
|
||||||
|
try {
|
||||||
|
while (true) {
|
||||||
|
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('graph-selected', {
|
||||||
|
graphId: graphId,
|
||||||
|
currentNode: assistantNodeId
|
||||||
|
});
|
||||||
|
} 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);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
} else if (data.type === 'done') {
|
||||||
|
this.currentNode = assistantNodeId;
|
||||||
|
this.plugin.eventBus.emit('graph-selected', {
|
||||||
|
graphId: graphId,
|
||||||
|
currentNode: assistantNodeId
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
reader.releaseLock();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
updateStreamingMessage(nodeId: string, content: string): void {
|
||||||
|
const lastMessage = this.chatHistory[this.chatHistory.length - 1];
|
||||||
|
if (lastMessage && lastMessage.node_id === nodeId) {
|
||||||
|
lastMessage.content = content;
|
||||||
|
} else {
|
||||||
|
this.chatHistory.push({
|
||||||
|
role: 'assistant',
|
||||||
|
content: content,
|
||||||
|
node_id: nodeId,
|
||||||
|
graph_id: this.selectedGraphId || ''
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.chatPanel) {
|
||||||
|
this.chatPanel.updateHistory(this.chatHistory);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async handleRegenerateMessage(data: { graphId: string; nodeId: string }): Promise<void> {
|
||||||
|
try {
|
||||||
|
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,
|
||||||
|
system_prompt: this.plugin.settings.systemPrompt,
|
||||||
|
model: this.plugin.settings.defaultModel
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(`Ошибка регенерации: ${response.status}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = await response.json();
|
||||||
|
const newNodeId = result.new_node_id;
|
||||||
|
const parentNodeId = result.parent_node_id;
|
||||||
|
|
||||||
|
// Запускаем стриминг для нового узла
|
||||||
|
if (data.graphId) {
|
||||||
|
await this.streamLLMResponse(data.graphId, parentNodeId, this.plugin.settings.defaultModel);
|
||||||
|
}
|
||||||
|
} catch (error: any) {
|
||||||
|
console.error('Ошибка регенерации:', error);
|
||||||
|
new Notice(`Ошибка регенерации: ${error?.message || error}`, 5000);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -120,6 +120,10 @@ export class GraphView extends ItemView {
|
||||||
this.plugin.eventBus.on('graph-selected', this.handleGraphSelected.bind(this));
|
this.plugin.eventBus.on('graph-selected', this.handleGraphSelected.bind(this));
|
||||||
this.plugin.eventBus.on('delete-node', this.handleDeleteNodeFromChat.bind(this)); // Слушаем событие из ChatPanel
|
this.plugin.eventBus.on('delete-node', this.handleDeleteNodeFromChat.bind(this)); // Слушаем событие из ChatPanel
|
||||||
|
|
||||||
|
// WebSocket Event Handlers
|
||||||
|
this.plugin.eventBus.on('ws-graph-title-updated', this.handleGraphTitleUpdated.bind(this));
|
||||||
|
this.plugin.eventBus.on('ws-node-title-updated', this.handleNodeTitleUpdated.bind(this));
|
||||||
|
|
||||||
// Загружаем список графов
|
// Загружаем список графов
|
||||||
await this.fetchGraphsList();
|
await this.fetchGraphsList();
|
||||||
}
|
}
|
||||||
|
|
@ -143,21 +147,25 @@ export class GraphView extends ItemView {
|
||||||
this.plugin.eventBus.off('new-chat', this.handleNewChat);
|
this.plugin.eventBus.off('new-chat', this.handleNewChat);
|
||||||
this.plugin.eventBus.off('graph-selected', this.handleGraphSelected);
|
this.plugin.eventBus.off('graph-selected', this.handleGraphSelected);
|
||||||
this.plugin.eventBus.off('delete-node', this.handleDeleteNodeFromChat);
|
this.plugin.eventBus.off('delete-node', this.handleDeleteNodeFromChat);
|
||||||
|
|
||||||
|
// WebSocket Event Cleanup
|
||||||
|
this.plugin.eventBus.off('ws-graph-title-updated', this.handleGraphTitleUpdated);
|
||||||
|
this.plugin.eventBus.off('ws-node-title-updated', this.handleNodeTitleUpdated);
|
||||||
}
|
}
|
||||||
|
|
||||||
// ----------------------------------------------- Custom Title Update ---------------------------------------------------
|
// ----------------------------------------------- Custom Title Update ---------------------------------------------------
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Обновляет текстовое содержимое заголовка представления.
|
* Обновляет текстовое содержимое заголовка представления.
|
||||||
* Этот метод должен быть вызван для динамического изменения заголовка
|
* Этот метод должен быть вызван для динамического изменения заголовка
|
||||||
* после первоначальной отрисовки представления.
|
* после первоначальной отрисовки представления.
|
||||||
*/
|
*/
|
||||||
updateTitle(): void {
|
updateTitle(): void {
|
||||||
const titleElement = this.containerEl.querySelector('.view-header-title');
|
const titleElement = this.containerEl.querySelector('.view-header-title');
|
||||||
if (titleElement) {
|
if (titleElement) {
|
||||||
titleElement.textContent = this.selectedGraphTitle || 'LLM Agent Graph';
|
titleElement.textContent = this.selectedGraphTitle || 'LLM Agent Graph';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ----------------------------------------------- History Panel Toggle --------------------------------------------------
|
// ----------------------------------------------- History Panel Toggle --------------------------------------------------
|
||||||
|
|
||||||
|
|
@ -300,7 +308,7 @@ export class GraphView extends ItemView {
|
||||||
this.currentNode = null;
|
this.currentNode = null;
|
||||||
this.selectedNodeIds.clear(); // Очищаем выделения
|
this.selectedNodeIds.clear(); // Очищаем выделения
|
||||||
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 });
|
||||||
return;
|
return;
|
||||||
|
|
@ -321,7 +329,7 @@ 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.renderGraphPanel();
|
this.renderGraphPanel();
|
||||||
|
|
||||||
|
|
@ -334,7 +342,7 @@ export class GraphView extends ItemView {
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Ошибка загрузки данных графа:', error);
|
console.error('Ошибка загрузки данных графа:', error);
|
||||||
this.selectedGraphTitle = null;
|
this.selectedGraphTitle = null;
|
||||||
this.updateTitle(); // Обновляем заголовок
|
this.updateTitle(); // Обновляем заголовок
|
||||||
// Optionally: show a notice to the user
|
// Optionally: show a notice to the user
|
||||||
// new Notice(`Ошибка загрузки данных графа: ${error.message}`);
|
// new Notice(`Ошибка загрузки данных графа: ${error.message}`);
|
||||||
}
|
}
|
||||||
|
|
@ -413,12 +421,12 @@ export class GraphView extends ItemView {
|
||||||
this.graphData = { nodes: [], edges: [] };
|
this.graphData = { nodes: [], edges: [] };
|
||||||
this.currentNode = null;
|
this.currentNode = null;
|
||||||
this.selectedGraphId = null;
|
this.selectedGraphId = null;
|
||||||
this.selectedNodeIds.clear(); // Очищаем выделения
|
this.selectedNodeIds.clear(); // Очищаем выделения
|
||||||
this.selectedGraphTitle = null; // Сброс заголовка при новом чате
|
this.selectedGraphTitle = null; // Сброс заголовка при новом чате
|
||||||
this.updateTitle(); // Обновляем заголовок
|
this.updateTitle(); // Обновляем заголовок
|
||||||
this.renderGraphPanel();
|
this.renderGraphPanel();
|
||||||
this.fetchGraphsList(); // Обновить список графов, чтобы показать новый пустой чат
|
this.fetchGraphsList(); // Обновить список графов, чтобы показать новый пустой чат
|
||||||
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 });
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -437,7 +445,7 @@ export class GraphView extends ItemView {
|
||||||
* Обработчик события удаления узла из ChatPanel.
|
* Обработчик события удаления узла из ChatPanel.
|
||||||
* @param {{ graphId: string, nodeId: string }} data - Данные об удаляемом узле.
|
* @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) {
|
||||||
await this.handleDeleteNode(data.nodeId);
|
await this.handleDeleteNode(data.nodeId);
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -459,7 +467,7 @@ export class GraphView extends ItemView {
|
||||||
// Необходимо пройтись по nodes в graphData, чтобы получить актуальные данные сообщений
|
// Необходимо пройтись по nodes в graphData, чтобы получить актуальные данные сообщений
|
||||||
const orderedNodeIds = Array.from(this.selectedNodeIds); // Сохраняем порядок
|
const orderedNodeIds = Array.from(this.selectedNodeIds); // Сохраняем порядок
|
||||||
for (const nodeId of orderedNodeIds) {
|
for (const nodeId of orderedNodeIds) {
|
||||||
const node = this.graphData.nodes.find(n => n.id === nodeId);
|
const node = this.graphData.nodes.find((n) => n.id === nodeId);
|
||||||
if (node && node.data && node.data.message && node.data.message.content) {
|
if (node && node.data && node.data.message && node.data.message.content) {
|
||||||
messagesToCopy.push(node.data.message.content);
|
messagesToCopy.push(node.data.message.content);
|
||||||
}
|
}
|
||||||
|
|
@ -496,8 +504,7 @@ export class GraphView extends ItemView {
|
||||||
this.plugin.app,
|
this.plugin.app,
|
||||||
`Вы уверены, что хотите удалить ${this.selectedNodeIds.size} выделенных узлов?`
|
`Вы уверены, что хотите удалить ${this.selectedNodeIds.size} выделенных узлов?`
|
||||||
);
|
);
|
||||||
if (!confirmed)
|
if (!confirmed) return;
|
||||||
return;
|
|
||||||
|
|
||||||
new Notice(`Начинаю удаление ${this.selectedNodeIds.size} узлов...`, 2000);
|
new Notice(`Начинаю удаление ${this.selectedNodeIds.size} узлов...`, 2000);
|
||||||
const nodesToDelete = Array.from(this.selectedNodeIds); // Сохраняем порядок выделения
|
const nodesToDelete = Array.from(this.selectedNodeIds); // Сохраняем порядок выделения
|
||||||
|
|
@ -525,4 +532,46 @@ export class GraphView extends ItemView {
|
||||||
await this.fetchGraphData(this.selectedGraphId); // Полностью обновляем граф и чат-панель
|
await this.fetchGraphData(this.selectedGraphId); // Полностью обновляем граф и чат-панель
|
||||||
new Notice('Операция удаления завершена.', 2000);
|
new Notice('Операция удаления завершена.', 2000);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ----------------------------------------------- WebSocket Event Handlers ---------------------------------------------------------------
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Обработчик обновления заголовка графа через WebSocket
|
||||||
|
*/
|
||||||
|
handleGraphTitleUpdated(data: { graph_id: string; title: string }): void {
|
||||||
|
// Обновляем только если это текущий граф
|
||||||
|
if (this.selectedGraphId === data.graph_id) {
|
||||||
|
this.selectedGraphTitle = data.title;
|
||||||
|
this.updateTitle();
|
||||||
|
|
||||||
|
// Также обновляем в списке истории
|
||||||
|
if (this.historyPanel) {
|
||||||
|
this.fetchGraphsList(); // Перезагружаем список графов
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Обработчик обновления заголовка узла через WebSocket
|
||||||
|
*/
|
||||||
|
handleNodeTitleUpdated(data: { graph_id: string; node_id: string; title: string }): void {
|
||||||
|
// Обновляем только если это узел текущего графа
|
||||||
|
if (this.selectedGraphId !== data.graph_id) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Находим и обновляем узел в graphData
|
||||||
|
const nodeIndex = this.graphData.nodes.findIndex((n) => n.id === data.node_id);
|
||||||
|
if (nodeIndex !== -1) {
|
||||||
|
// Обновляем заголовок и флаг генерации
|
||||||
|
this.graphData.nodes[nodeIndex].data.title = data.title;
|
||||||
|
this.graphData.nodes[nodeIndex].data.title_generated = true;
|
||||||
|
|
||||||
|
// Перерисовываем граф с обновленными данными
|
||||||
|
// React Flow автоматически обновит только измененный узел
|
||||||
|
this.renderGraphPanel();
|
||||||
|
|
||||||
|
console.log(`✅ Обновлен заголовок узла ${data.node_id}: "${data.title}"`);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
Loading…
Reference in New Issue
Block a user