Bug fixes to render react-flow using esbuild bundler instead of loading from cdn

This commit is contained in:
dimitrievgs 2025-09-14 19:03:33 +03:00
parent bcd152e81f
commit 04a19b2508
23 changed files with 42189 additions and 785 deletions

View File

@ -1,49 +1,72 @@
import esbuild from "esbuild";
import process from "process";
import builtins from "builtin-modules";
import esbuild from 'esbuild'
import process from 'process'
import builtins from 'builtin-modules'
import fs from 'fs'
import path from 'path'
const banner =
`/*
const banner = `/*
THIS IS A GENERATED/BUNDLED FILE BY ESBUILD
if you want to view the source, please visit the github repository of this plugin
*/
`;
`
const prod = (process.argv[2] === "production");
const prod = process.argv[2] === 'production'
const renameCssPlugin = () => ({
name: 'rename-css',
setup (build) {
build.onEnd(() => {
const outDir = path.dirname(build.initialOptions.outfile || 'main.js')
const mainCss = path.join(outDir, 'main.css')
const stylesCss = path.join(outDir, 'styles.css')
if (fs.existsSync(mainCss)) {
fs.renameSync(mainCss, stylesCss)
console.log(`Renamed ${mainCss} to ${stylesCss}`)
}
})
}
})
const context = await esbuild.context({
banner: {
js: banner,
js: banner
},
entryPoints: ["main.ts"],
entryPoints: ['main.ts'],
bundle: true,
external: [
"obsidian",
"electron",
"@codemirror/autocomplete",
"@codemirror/collab",
"@codemirror/commands",
"@codemirror/language",
"@codemirror/lint",
"@codemirror/search",
"@codemirror/state",
"@codemirror/view",
"@lezer/common",
"@lezer/highlight",
"@lezer/lr",
...builtins],
format: "cjs",
target: "es2018",
logLevel: "info",
sourcemap: prod ? false : "inline",
'obsidian',
'electron',
'@codemirror/autocomplete',
'@codemirror/collab',
'@codemirror/commands',
'@codemirror/language',
'@codemirror/lint',
'@codemirror/search',
'@codemirror/state',
'@codemirror/view',
'@lezer/common',
'@lezer/highlight',
'@lezer/lr',
...builtins
],
format: 'cjs',
target: 'es2019', // Обновляем таргет до es2019
logLevel: 'info',
sourcemap: prod ? false : 'inline',
treeShaking: true,
outfile: "main.js",
outfile: 'main.js',
minify: prod,
});
// ДОБАВЛЕНО: Загрузчик для CSS файлов
loader: {
'.css': 'css'
},
plugins: [renameCssPlugin()]
})
if (prod) {
await context.rebuild();
process.exit(0);
await context.rebuild()
process.exit(0)
} else {
await context.watch();
await context.watch()
}

39637
main.js

File diff suppressed because one or more lines are too long

View File

@ -3,6 +3,8 @@ import { EventBus } from './src/utils/EventBus';
import { GraphView, GRAPH_VIEW_TYPE } from './src/views/GraphView';
import { ChatView, CHAT_VIEW_TYPE } from './src/views/ChatView';
import 'src/css/styles.css'; // Путь к твоему новому CSS файлу
// ----------------------------------------------- Settings ---------------------------------------------------------------
interface LLMAgentSettings {

270
old/src/App.css Normal file
View File

@ -0,0 +1,270 @@
/* Общие стили контейнера приложения */
.app-container {
display: flex;
height: 100vh; /* Полная высота окна просмотра */
font-family: Arial, sans-serif;
background-color: #f0f0f0; /* Светлый фон для всего приложения */
}
/* Стили для HistoryPanel */
.history-panel {
width: 280px; /* Ширина боковой панели */
background-color: #202123; /* Темный фон, как в ChatGPT */
color: #ececec; /* Светлый текст */
padding: 10px;
box-shadow: 2px 0 5px rgba(0, 0, 0, 0.2); /* Тень для выделения */
display: flex;
flex-direction: column;
overflow-y: auto; /* Прокрутка, если история длинная */
flex-shrink: 0; /* Не позволяет панели сжиматься */
}
.history-header {
display: flex;
justify-content: space-between;
align-items: center;
padding-bottom: 15px;
border-bottom: 1px solid #3e3e40; /* Разделитель */
margin-bottom: 15px;
}
.history-panel h2 {
font-size: 1.1em;
color: #f0f0f0;
margin: 0;
}
.new-chat-button {
background-color: #3e3e40;
color: #ececec;
border: none;
padding: 8px 12px;
border-radius: 5px;
cursor: pointer;
font-size: 0.9em;
transition: background-color 0.2s ease;
}
.new-chat-button:hover {
background-color: #555558;
}
.chat-list {
list-style: none;
padding: 0;
margin: 0;
}
.chat-list-item {
padding: 10px 15px;
margin-bottom: 5px;
border-radius: 5px;
cursor: pointer;
background-color: #2d2e30; /* Фон элемента списка */
transition: background-color 0.2s ease, color 0.2s ease;
display: flex; /* Используем flexbox для внутреннего содержимого */
align-items: center;
min-height: 36px; /* Минимальная высота элемента */
}
.chat-list-item:hover {
background-color: #3e3e40; /* Фон при наведении */
color: #ffffff;
}
.chat-list-item.selected {
background-color: #4a4a50; /* Фон для выбранного элемента (если будет реализовано) */
}
.chat-item-content {
display: flex;
justify-content: space-between;
align-items: center;
width: 100%;
}
.chat-item-text {
flex-grow: 1; /* Позволяет тексту занимать все доступное пространство */
min-width: 0; /* Важно для работы truncate */
}
.chat-item-text .truncate {
white-space: nowrap; /* Запрещает перенос строки */
overflow: hidden; /* Скрывает содержимое, которое не помещается */
text-overflow: ellipsis; /* Добавляет многоточие для скрытого текста */
display: block; /* Важно для truncate */
}
.chat-item-actions {
margin-left: 10px;
flex-shrink: 0; /* Не позволяет кнопкам сжиматься */
}
.chat-item-options-button {
background: none;
border: none;
color: #777777; /* Цвет иконки */
cursor: pointer;
padding: 5px;
border-radius: 3px;
transition: color 0.2s ease, background-color 0.2s ease;
display: flex; /* Для центрирования SVG */
align-items: center;
justify-content: center;
}
.chat-item-options-button:hover {
color: #ffffff;
background-color: #555558;
}
/* Стили для GraphPanel */
.graph-panel {
flex-grow: 1;
border-right: 1px solid #ccc;
}
/* Стили для ChatPanel */
.chat-panel {
width: 300px; /* Фиксированная ширина для ChatPanel */
flex-shrink: 0; /* Не позволяет панели сжиматься */
display: flex;
flex-direction: column;
background-color: #343541; /* Темный фон для панели чата */
color: #e0e0e0;
padding: 20px;
}
.chat-history {
flex-grow: 1;
overflow-y: auto;
padding-right: 10px; /* Для отступа от скроллбара */
padding-bottom: 10px; /* Сохраняем ваш padding-bottom */
}
.message {
margin-bottom: 15px; /* Увеличиваем отступ */
padding: 10px 15px; /* Увеличиваем padding */
border-radius: 8px; /* Скругляем углы */
max-width: 70%; /* Ограничение ширины сообщения */
word-break: break-word; /* Добавлено: Перенос слов, если строка слишком длинная */
}
.message img {
max-width: 100%; /* Максимальная ширина изображения - 100% от родительского элемента */
height: auto; /* Автоматическая высота для сохранения пропорций */
display: block; /* Убирает отступы снизу у inline элементов */
margin-bottom: 5px; /* Небольшой отступ снизу каждого изображения */
}
.message.user {
background-color: #444654; /* Измененный фон */
align-self: flex-end; /* Сообщения пользователя справа */
margin-left: auto; /* Сдвигает сообщение вправо */
text-align: left; /* Изменено: текст выравнивается по левому краю внутри блока */
}
.message.assistant {
background-color: #4e4f5a; /* Измененный фон */
align-self: flex-start; /* Сообщения ассистента слева */
margin-right: auto; /* Сдвигает сообщение влево */
text-align: left; /* Сохраняем ваше выравнивание */
}
.chat-input-form {
display: flex;
margin-top: 20px; /* Добавляем отступ сверху */
gap: 10px; /* Промежуток между элементами формы */
}
/* Стили для CommandInput */
.command-input {
position: relative;
flex-grow: 1; /* Позволяет input занимать доступное пространство */
}
.command-input input {
width: 100%;
padding: 10px 15px; /* Увеличенный padding */
border: 1px solid #555; /* Темная рамка */
border-radius: 5px;
background-color: #444654; /* Темный фон */
color: #e0e0e0; /* Светлый текст */
font-size: 1em;
outline: none; /* Убираем стандартный outline */
}
.command-input input::placeholder {
color: #999; /* Цвет placeholder */
}
.chat-input-form button {
background-color: #1a73e8; /* Синяя кнопка */
color: white;
border: none;
padding: 10px 15px; /* Увеличенный padding */
border-radius: 5px;
cursor: pointer;
transition: background-color 0.2s ease;
}
.chat-input-form button:hover {
background-color: #0d47a1; /* Темнее при наведении */
}
.react-flow-root {
width: 100%;
height: 100%;
}
.command-input .suggestions {
list-style: none;
padding: 0;
margin: 0;
position: absolute;
bottom: 100%; /* Меню будет расположено выше элемента, к которому оно прикреплено */
left: 0;
width: 100%;
background-color: white;
border: 1px solid #ccc;
border-bottom: none; /* Убираем нижнюю границу, так как теперь верхняя часть будет примыкать к полю ввода */
border-radius: 5px 5px 0 0; /* Верхние углы будут скруглены, нижние - прямые */
box-shadow: 0 -2px 5px rgba(0, 0, 0, 0.1); /* Тень сверху */
z-index: 1;
}
.command-input .suggestions li {
padding: 5px;
cursor: pointer;
}
.command-input .suggestions li:hover {
background-color: #f0f0f0;
}
/* Стили для контекстного меню */
.context-menu {
position: absolute;
background-color: #fff;
border: 1px solid #ccc;
border-radius: 5px;
box-shadow: 2px 2px 5px rgba(0, 0, 0, 0.2);
z-index: 1000;
padding: 5px 0;
}
.context-menu button {
display: block;
width: 100%;
padding: 8px 15px;
text-align: left;
background: none;
border: none;
cursor: pointer;
transition: background-color 0.2s ease;
}
.context-menu button:hover {
background-color: #f0f0f0;
}

165
old/src/App.js Normal file
View File

@ -0,0 +1,165 @@
import React, { useState, useEffect } from "react";
import ChatPanel from "./components/ChatPanel";
import GraphPanel from "./components/GraphPanel";
import HistoryPanel from "./components/HistoryPanel";
import "./App.css";
function App() {
const [graphData, setGraphData] = useState({ nodes: [], edges: [] });
const [chatHistory, setChatHistory] = useState([]);
const [currentNode, setCurrentNode] = useState(null);
const [graphsList, setGraphsList] = useState([]);
const [selectedGraphId, setSelectedGraphId] = useState(null);
const API_BASE_URL = "http://localhost:5000/api"; // Укажи базовый URL для API
useEffect(() => {
fetchGraphsList();
if (selectedGraphId) {
fetchGraphData(selectedGraphId);
}
}, [selectedGraphId]);
const fetchGraphsList = async () => {
try {
const response = await fetch(`${API_BASE_URL}/graphs`); // Используй базовый URL
const data = await response.json();
setGraphsList(data);
} catch (error) {
console.error("Error fetching graphs:", error);
}
};
const fetchGraphData = async (graphId) => {
try {
const response = await fetch(`${API_BASE_URL}/graphs/${graphId}`); // Используй базовый URL
if (!response.ok) {
// Если статус ответа не 200-299, выбрасываем ошибку
if (response.status === 404) {
console.error(`Graph with id ${graphId} not found.`);
// Здесь можно добавить логику для отображения сообщения пользователю
// Например, установить состояние ошибки и отобразить сообщение в UI
// setErrorMessage(`Graph with id ${graphId} not found.`);
}
throw new Error(`HTTP error! status: ${response.status}`);
}
const data = await response.json();
console.log("Fetched graph data:", data); // Добавлено для отладки
setGraphData({
nodes: data.graph_nodes.map((node) => ({
id: node.id,
data: { label: node.data.label },
})),
edges: data.graph_edges.map((edge) => ({
id: edge.id,
source: edge.source,
target: edge.target,
})),
});
setChatHistory(data.messages);
} catch (error) {
console.error("Error fetching graph data:", error);
}
};
const handleSendMessage = async (message) => {
try {
const response = await fetch(`${API_BASE_URL}/chat`, {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
message: message,
graph_id: selectedGraphId,
parent_node_id: currentNode,
}),
});
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const result = await response.json();
console.log("Result from API:", result);
setGraphData({
nodes: result.graph_visualization_data.nodes.map((node) => ({
id: node.id,
data: { label: node.data.label },
})),
edges: result.graph_visualization_data.edges.map((edge) => ({
id: edge.id,
source: edge.source,
target: edge.target,
})),
});
// Обновляем выбранный ID графа
setSelectedGraphId(result.graph_id);
// Устанавливаем текущий узел на тот, который был последним в ответе агента
const newCurrentNodeId = result.graph_visualization_data.current_node_id;
setCurrentNode(newCurrentNodeId);
// Всегда загружаем сообщения от корня до нового текущего узла
// Это гарантирует, что в чате отображается только текущая ветка
if (newCurrentNodeId) {
await fetchMessagesFromRootToNode(newCurrentNodeId);
} else {
// Если почему-то current_node_id не пришел (что не должно быть при успешном ответе),
// используем полную историю, хотя это менее предпочтительно.
setChatHistory(result.messages);
}
} catch (error) {
console.error("Error sending message:", error);
}
};
// Новая функция для получения сообщений от корня до выбранной ноды
const fetchMessagesFromRootToNode = async (nodeId) => {
try {
const response = await fetch(`${API_BASE_URL}/messages/${selectedGraphId}/${nodeId}`); // Используй базовый URL
const data = await response.json();
setChatHistory(data); // Обновляем историю чата только сообщениями от корня до выбранной ноды
} catch (error) {
console.error("Error fetching messages:", error);
}
};
const handleNodeClick = (nodeId) => {
setCurrentNode(nodeId);
fetchMessagesFromRootToNode(nodeId); // Получаем сообщения при клике на ноду
};
const handleGraphSelect = (graphId) => {
setSelectedGraphId(graphId);
};
// Новая функция для создания нового чата
const handleNewChat = () => {
setGraphData({ nodes: [], edges: [] }); // Очищаем данные графа
setChatHistory([]); // Очищаем историю чата
setCurrentNode(null); // Сбрасываем текущий выбранный узел
setSelectedGraphId(null); // Сбрасываем выбранный ID графа (чтобы бэкенд создал новый)
};
return (
<div className="app-container">
{/* Передаем handleNewChat в HistoryPanel */}
<HistoryPanel
graphs={graphsList}
onGraphSelect={handleGraphSelect}
onNewChat={handleNewChat}
/>
<GraphPanel graphData={graphData} onNodeClick={handleNodeClick} />
<ChatPanel chatHistory={chatHistory} onSendMessage={handleSendMessage} />
</div>
);
}
export default App;

View File

@ -0,0 +1,71 @@
import React, { useState, useEffect } from 'react';
import ReactMarkdown from 'react-markdown';
import remarkGfm from 'remark-gfm';
import rehypeRaw from 'rehype-raw'; // Импортируем rehype-raw для обработки сырого HTML
import CommandInput from './CommandInput';
function ChatPanel({ chatHistory, onSendMessage }) {
const [message, setMessage] = useState('');
const handleSubmit = (event) => {
event.preventDefault(); // Предотвращаем стандартное действие отправки формы, которое перезагружает страницу
if (message.trim()) { // Проверяем, что сообщение не пустое
onSendMessage(message); // Вызываем функцию отправки сообщения, переданную из родителя
setMessage(''); // Очищаем поле ввода после отправки
}
};
useEffect(() => {
const chatHistoryDiv = document.querySelector('.chat-history');
if (chatHistoryDiv) {
chatHistoryDiv.scrollTop = chatHistoryDiv.scrollHeight;
}
}, [chatHistory]);
return (
<div className="chat-panel">
<div className="chat-history">
{chatHistory.map((msg, index) => (
<div key={index} className={`message ${msg.role}`}>
{/* Добавлено: обработка текста с Markdown */}
<ReactMarkdown
remarkPlugins={[remarkGfm]} // remarkHtml больше не нужен здесь
rehypePlugins={[rehypeRaw]} // Добавляем rehypeRaw для парсинга и отображения сырого HTML
components={{
a: ({ node, ...props }) => (
<a {...props} target="_blank" rel="noopener noreferrer">
{props.children}
</a>
),
img: ({ node, ...props }) => (
// Переопределяем стили изображения для адаптивности
<img
{...props}
style={{
...props.style, // Сохраняем другие инлайн-стили, переданные из бэкенда (например, max-height)
maxWidth: '100%', // Гарантируем, что изображение не выходит за пределы ширины родителя
height: 'auto' // Сохраняем пропорции изображения
}}
/>
)
}}
children={msg.content} // Явное указание children
/>
</div>
))}
</div>
<form onSubmit={handleSubmit} className="chat-input-form">
<CommandInput
value={message} // Передаем текущее значение поля ввода
onChange={(e) => setMessage(e.target.value)} // Обновляем состояние при изменении поля ввода
// onSendMessage больше НЕ передается сюда,
// так как CommandInput не должен напрямую отправлять сообщения.
// Отправка происходит через onSubmit формы.
/>
<button type="submit">Отправить</button>
</form>
</div>
);
}
export default ChatPanel;

View File

@ -0,0 +1,57 @@
import React, { useState, useEffect } from 'react';
const CommandInput = ({ value, onChange, /* onSendMessage удален из пропсов */ }) => { // onSendMessage больше не передается как проп
const [suggestions, setSuggestions] = useState([]);
const commands = ['/help', '/imagine', '/analyze', '/subtitles_meet', '/subtitles_teams', '/summarize', '/chat'];
useEffect(() => {
if (value.startsWith('/')) {
const filteredSuggestions = commands.filter(command =>
command.startsWith(value.toLowerCase())
);
setSuggestions(filteredSuggestions);
} else {
setSuggestions([]);
}
}, [value]);
const handleSuggestionClick = (suggestion) => {
onChange({ target: { value: suggestion } });
setSuggestions([]);
};
const handleKeyDown = (event) => {
if (event.key === 'Enter') {
if (suggestions.length > 0) {
// Если есть предложения, выбираем первое (или другое, если у вас есть логика выделения)
handleSuggestionClick(suggestions[0]);
// Предотвращаем стандартное поведение (отправку формы), так как мы обработали Enter
event.preventDefault();
}
// Если предложений нет, то Enter должен просто вызвать стандартную отправку формы.
// Ничего дополнительно делать здесь не нужно, т.к. родительская форма обработает submit.
}
};
return (
<div className="command-input">
<input
type="text"
value={value}
onChange={onChange}
onKeyDown={handleKeyDown}
/>
{suggestions.length > 0 && (
<ul className="suggestions">
{suggestions.map((suggestion, index) => (
<li key={index} onClick={() => handleSuggestionClick(suggestion)}>
{suggestion}
</li>
))}
</ul>
)}
</div>
);
};
export default CommandInput;

View File

@ -0,0 +1,132 @@
import React, { useCallback, useEffect } from 'react';
import ReactFlow, {
addEdge,
useNodesState,
useEdgesState,
Background,
Controls,
ReactFlowProvider
} from 'reactflow';
import 'reactflow/dist/style.css';
import dagre from 'dagre'; // Импортируем библиотеку dagre
// Инициализируем объект Dagre Graph
const dagreGraph = new dagre.graphlib.Graph().setDefaultEdgeLabel(() => ({}));
// Определяем размеры узлов для корректного расположения
const nodeWidth = 172; // Ширина узла, может потребоваться корректировка
const nodeHeight = 36; // Высота узла, может потребоваться корректировка
/**
* Применяет алгоритм расположения Dagre к узлам и рёбрам графа.
* @param {Array} nodes Массив узлов React Flow.
* @param {Array} edges Массив рёбер React Flow.
* @param {string} direction Направление графа ('TB' - сверху вниз, 'LR' - слева направо).
* @returns {Object} Объект с обновлёнными узлами и рёбрами, содержащими позиции.
*/
const getLayoutedElements = (nodes, edges, direction = 'TB') => {
const isHorizontal = direction === 'LR'; // Проверяем, горизонтальное ли расположение
dagreGraph.setGraph({ rankdir: direction }); // Устанавливаем направление графа
// Добавляем узлы в граф Dagre с их размерами
nodes.forEach((node) => {
dagreGraph.setNode(node.id, { width: nodeWidth, height: nodeHeight });
});
// Добавляем рёбра в граф Dagre
edges.forEach((edge) => {
dagreGraph.setEdge(edge.source, edge.target);
});
// Выполняем расчёт расположения графа
dagre.layout(dagreGraph);
// Обновляем позиции узлов React Flow на основе расчётов Dagre
const newNodes = nodes.map((node) => {
const nodeWithPosition = dagreGraph.node(node.id); // Получаем данные узла из Dagre
// Смещаем позицию узла Dagre (якорь в центре) к верхнему левому углу,
// чтобы она соответствовала точке привязки узла React Flow (верхний левый угол).
return {
...node,
targetPosition: isHorizontal ? 'left' : 'top',
sourcePosition: isHorizontal ? 'right' : 'bottom',
position: {
x: nodeWithPosition.x - nodeWidth / 2,
y: nodeWithPosition.y - nodeHeight / 2,
},
};
});
return { nodes: newNodes, edges }; // Возвращаем обновлённые узлы и рёбра
};
const GraphPanel = ({ graphData, onNodeClick }) => {
// Инициализируем состояния узлов и рёбер пустыми массивами
const [nodes, setNodes, onNodesChange] = useNodesState([]);
const [edges, setEdges, onEdgesChange] = useEdgesState([]);
// Используем useEffect для применения Dagre layout при изменении данных графа
useEffect(() => {
// Проверяем, есть ли данные для построения графа
if (graphData.nodes.length > 0 || graphData.edges.length > 0) {
console.log("Applying Dagre layout with data:", graphData);
// Преобразуем входящие данные в формат, необходимый для Dagre и React Flow
const initialNodesForLayout = graphData.nodes.map(node => ({
id: node.id,
data: { label: node.data.label },
position: { x: 0, y: 0 } // Временная позиция, будет перезаписана Dagre
}));
const initialEdgesForLayout = graphData.edges.map(edge => ({
id: edge.id,
source: edge.source,
target: edge.target,
}));
// Применяем Dagre layout
const { nodes: layoutedNodes, edges: layoutedEdges } = getLayoutedElements(
initialNodesForLayout,
initialEdgesForLayout
);
// Обновляем состояния узлов и рёбер React Flow
setNodes(layoutedNodes);
setEdges(layoutedEdges);
} else {
// Если данных нет, очищаем граф
setNodes([]);
setEdges([]);
}
}, [graphData.nodes, graphData.edges, setNodes, setEdges]); // Зависимости: изменения в узлах и рёбрах графа
const onConnect = useCallback(
(params) => setEdges((eds) => addEdge(params, eds)),
[setEdges]
);
const nodeClickHandler = useCallback((event, node) => {
onNodeClick(node.id);
}, [onNodeClick]);
return (
<div className="graph-panel">
<ReactFlowProvider>
<ReactFlow
nodes={nodes}
edges={edges}
onNodesChange={onNodesChange}
onEdgesChange={onEdgesChange}
onConnect={onConnect}
onNodeClick={nodeClickHandler}
fitView // Автоматически центрирует и масштабирует граф
attributionPosition="top-right"
>
<Controls /> {/* Добавляем элементы управления */}
<Background variant="dots" gap={12} size={1} /> {/* Добавляем фон */}
</ReactFlow>
</ReactFlowProvider>
</div>
);
};
export default GraphPanel;

View File

@ -0,0 +1,127 @@
import React, { useState, useRef, useEffect } from "react";
function HistoryPanel({ graphs, onGraphSelect, onNewChat }) {
const [contextMenu, setContextMenu] = useState({
visible: false,
x: 0,
y: 0,
graphId: null,
});
const menuRef = useRef(null);
useEffect(() => {
function handleClickOutside(event) {
if (menuRef.current && !menuRef.current.contains(event.target)) {
setContextMenu((prev) => ({ ...prev, visible: false }));
}
}
document.addEventListener("mousedown", handleClickOutside);
return () => {
document.removeEventListener("mousedown", handleClickOutside);
};
}, [menuRef]);
const handleContextMenu = (e, graphId) => {
e.preventDefault();
setContextMenu({
visible: true,
x: e.clientX,
y: e.clientY,
graphId: graphId,
});
};
const handleDeleteGraph = async (graphId) => {
try {
const response = await fetch(
`http://localhost:5000/api/graphs/${graphId}`,
{
// Укажи URL для удаления графа
method: "DELETE",
}
);
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
// После успешного удаления обновите список графов
// setGraphs(graphs.filter(graph => graph.id !== graphId));
window.location.reload(); // Простейший способ обновить список графов
setContextMenu({ ...contextMenu, visible: false });
} catch (error) {
console.error("Error deleting graph:", error);
// TODO: Обработка ошибок удаления графа
}
};
return (
<div className="history-panel">
<div className="history-header">
<h2>История диалогов</h2>
<button className="new-chat-button" onClick={onNewChat}>
+ Новый чат
</button>
</div>
<ul className="chat-list">
{graphs.map((graph) => (
<li
key={graph.id}
className="chat-list-item"
onClick={() => onGraphSelect(graph.id)}
>
<div className="chat-item-content">
<div className="chat-item-text">
<span className="truncate">
{graph.first_message
? graph.first_message
: `Граф ${graph.id}`}
</span>
</div>
<div className="chat-item-actions">
<button
className="chat-item-options-button"
aria-label="Открыть параметры обсуждения"
onClick={(e) => {
e.stopPropagation(); // Предотвращаем выполнение родительского onClick
handleContextMenu(e, graph.id); // Открываем контекстное меню
}}
>
<svg
width="20"
height="20"
viewBox="0 0 20 20"
fill="currentColor"
xmlns="http://www.w3.org/2000/svg"
>
<path d="M15.498 8.50159C16.3254 8.50159 16.9959 9.17228 16.9961 9.99963C16.9961 10.8271 16.3256 11.4987 15.498 11.4987C14.6705 11.4987 14 10.8271 14 9.99963C14.0002 9.17228 14.6706 8.50159 15.498 8.50159Z"></path>
<path d="M4.49805 8.50159C5.32544 8.50159 5.99689 9.17228 5.99707 9.99963C5.99707 10.8271 5.32555 11.4987 4.49805 11.4987C3.67069 11.4985 3 10.827 3 9.99963C3.00018 9.17239 3.6708 8.50176 4.49805 8.50159Z"></path>
<path d="M10.0003 8.50159C10.8276 8.50176 11.4982 9.17239 11.4984 9.99963C11.4984 10.827 10.8277 11.4985 10.0003 11.4987C9.17283 11.4987 8.50131 10.8271 8.50131 9.99963C8.50149 9.17228 9.17294 8.50159 10.0003 8.50159Z"></path>
</svg>
</button>
</div>
</div>
</li>
))}
</ul>
{contextMenu.visible && (
<div
className="context-menu"
style={{
top: contextMenu.y,
left: contextMenu.x,
}}
ref={menuRef}
>
<button onClick={() => handleDeleteGraph(contextMenu.graphId)}>
Удалить
</button>
</div>
)}
</div>
);
}
export default HistoryPanel;

19
old/src/index.css Normal file
View File

@ -0,0 +1,19 @@
body {
margin: 0;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
code {
font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New',
monospace;
}
/* Общие стили для всего приложения */
.app-container {
display: flex;
height: 100vh;
}

11
old/src/index.js Normal file
View File

@ -0,0 +1,11 @@
import React from 'react';
import ReactDOM from 'react-dom/client';
import './index.css';
import App from './App';
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(
<React.StrictMode>
<App />
</React.StrictMode>
);

658
package-lock.json generated
View File

@ -9,14 +9,21 @@
"version": "1.0.0",
"license": "MIT",
"devDependencies": {
"@types/dagre": "^0.7.52",
"@types/node": "^16.11.6",
"@types/react": "^18.3.3",
"@types/react-dom": "^18.3.7",
"@typescript-eslint/eslint-plugin": "5.29.0",
"@typescript-eslint/parser": "5.29.0",
"builtin-modules": "3.3.0",
"dagre": "^0.8.5",
"esbuild": "0.17.3",
"obsidian": "latest",
"react": "^18.3.1",
"react-dom": "^18.3.1",
"reactflow": "^11.11.3",
"tslib": "^2.4.0",
"typescript": "4.7.4"
"typescript": "^5.4.5"
}
},
"node_modules/@codemirror/state": {
@ -537,6 +544,108 @@
"node": ">= 8"
}
},
"node_modules/@reactflow/background": {
"version": "11.3.14",
"resolved": "https://registry.npmjs.org/@reactflow/background/-/background-11.3.14.tgz",
"integrity": "sha512-Gewd7blEVT5Lh6jqrvOgd4G6Qk17eGKQfsDXgyRSqM+CTwDqRldG2LsWN4sNeno6sbqVIC2fZ+rAUBFA9ZEUDA==",
"dev": true,
"dependencies": {
"@reactflow/core": "11.11.4",
"classcat": "^5.0.3",
"zustand": "^4.4.1"
},
"peerDependencies": {
"react": ">=17",
"react-dom": ">=17"
}
},
"node_modules/@reactflow/controls": {
"version": "11.2.14",
"resolved": "https://registry.npmjs.org/@reactflow/controls/-/controls-11.2.14.tgz",
"integrity": "sha512-MiJp5VldFD7FrqaBNIrQ85dxChrG6ivuZ+dcFhPQUwOK3HfYgX2RHdBua+gx+40p5Vw5It3dVNp/my4Z3jF0dw==",
"dev": true,
"dependencies": {
"@reactflow/core": "11.11.4",
"classcat": "^5.0.3",
"zustand": "^4.4.1"
},
"peerDependencies": {
"react": ">=17",
"react-dom": ">=17"
}
},
"node_modules/@reactflow/core": {
"version": "11.11.4",
"resolved": "https://registry.npmjs.org/@reactflow/core/-/core-11.11.4.tgz",
"integrity": "sha512-H4vODklsjAq3AMq6Np4LE12i1I4Ta9PrDHuBR9GmL8uzTt2l2jh4CiQbEMpvMDcp7xi4be0hgXj+Ysodde/i7Q==",
"dev": true,
"dependencies": {
"@types/d3": "^7.4.0",
"@types/d3-drag": "^3.0.1",
"@types/d3-selection": "^3.0.3",
"@types/d3-zoom": "^3.0.1",
"classcat": "^5.0.3",
"d3-drag": "^3.0.0",
"d3-selection": "^3.0.0",
"d3-zoom": "^3.0.0",
"zustand": "^4.4.1"
},
"peerDependencies": {
"react": ">=17",
"react-dom": ">=17"
}
},
"node_modules/@reactflow/minimap": {
"version": "11.7.14",
"resolved": "https://registry.npmjs.org/@reactflow/minimap/-/minimap-11.7.14.tgz",
"integrity": "sha512-mpwLKKrEAofgFJdkhwR5UQ1JYWlcAAL/ZU/bctBkuNTT1yqV+y0buoNVImsRehVYhJwffSWeSHaBR5/GJjlCSQ==",
"dev": true,
"dependencies": {
"@reactflow/core": "11.11.4",
"@types/d3-selection": "^3.0.3",
"@types/d3-zoom": "^3.0.1",
"classcat": "^5.0.3",
"d3-selection": "^3.0.0",
"d3-zoom": "^3.0.0",
"zustand": "^4.4.1"
},
"peerDependencies": {
"react": ">=17",
"react-dom": ">=17"
}
},
"node_modules/@reactflow/node-resizer": {
"version": "2.2.14",
"resolved": "https://registry.npmjs.org/@reactflow/node-resizer/-/node-resizer-2.2.14.tgz",
"integrity": "sha512-fwqnks83jUlYr6OHcdFEedumWKChTHRGw/kbCxj0oqBd+ekfs+SIp4ddyNU0pdx96JIm5iNFS0oNrmEiJbbSaA==",
"dev": true,
"dependencies": {
"@reactflow/core": "11.11.4",
"classcat": "^5.0.4",
"d3-drag": "^3.0.0",
"d3-selection": "^3.0.0",
"zustand": "^4.4.1"
},
"peerDependencies": {
"react": ">=17",
"react-dom": ">=17"
}
},
"node_modules/@reactflow/node-toolbar": {
"version": "1.3.14",
"resolved": "https://registry.npmjs.org/@reactflow/node-toolbar/-/node-toolbar-1.3.14.tgz",
"integrity": "sha512-rbynXQnH/xFNu4P9H+hVqlEUafDCkEoCy0Dg9mG22Sg+rY/0ck6KkrAQrYrTgXusd+cEJOMK0uOOFCK2/5rSGQ==",
"dev": true,
"dependencies": {
"@reactflow/core": "11.11.4",
"classcat": "^5.0.3",
"zustand": "^4.4.1"
},
"peerDependencies": {
"react": ">=17",
"react-dom": ">=17"
}
},
"node_modules/@types/codemirror": {
"version": "5.60.8",
"resolved": "https://registry.npmjs.org/@types/codemirror/-/codemirror-5.60.8.tgz",
@ -546,12 +655,277 @@
"@types/tern": "*"
}
},
"node_modules/@types/d3": {
"version": "7.4.3",
"resolved": "https://registry.npmjs.org/@types/d3/-/d3-7.4.3.tgz",
"integrity": "sha512-lZXZ9ckh5R8uiFVt8ogUNf+pIrK4EsWrx2Np75WvF/eTpJ0FMHNhjXk8CKEx/+gpHbNQyJWehbFaTvqmHWB3ww==",
"dev": true,
"dependencies": {
"@types/d3-array": "*",
"@types/d3-axis": "*",
"@types/d3-brush": "*",
"@types/d3-chord": "*",
"@types/d3-color": "*",
"@types/d3-contour": "*",
"@types/d3-delaunay": "*",
"@types/d3-dispatch": "*",
"@types/d3-drag": "*",
"@types/d3-dsv": "*",
"@types/d3-ease": "*",
"@types/d3-fetch": "*",
"@types/d3-force": "*",
"@types/d3-format": "*",
"@types/d3-geo": "*",
"@types/d3-hierarchy": "*",
"@types/d3-interpolate": "*",
"@types/d3-path": "*",
"@types/d3-polygon": "*",
"@types/d3-quadtree": "*",
"@types/d3-random": "*",
"@types/d3-scale": "*",
"@types/d3-scale-chromatic": "*",
"@types/d3-selection": "*",
"@types/d3-shape": "*",
"@types/d3-time": "*",
"@types/d3-time-format": "*",
"@types/d3-timer": "*",
"@types/d3-transition": "*",
"@types/d3-zoom": "*"
}
},
"node_modules/@types/d3-array": {
"version": "3.2.2",
"resolved": "https://registry.npmjs.org/@types/d3-array/-/d3-array-3.2.2.tgz",
"integrity": "sha512-hOLWVbm7uRza0BYXpIIW5pxfrKe0W+D5lrFiAEYR+pb6w3N2SwSMaJbXdUfSEv+dT4MfHBLtn5js0LAWaO6otw==",
"dev": true
},
"node_modules/@types/d3-axis": {
"version": "3.0.6",
"resolved": "https://registry.npmjs.org/@types/d3-axis/-/d3-axis-3.0.6.tgz",
"integrity": "sha512-pYeijfZuBd87T0hGn0FO1vQ/cgLk6E1ALJjfkC0oJ8cbwkZl3TpgS8bVBLZN+2jjGgg38epgxb2zmoGtSfvgMw==",
"dev": true,
"dependencies": {
"@types/d3-selection": "*"
}
},
"node_modules/@types/d3-brush": {
"version": "3.0.6",
"resolved": "https://registry.npmjs.org/@types/d3-brush/-/d3-brush-3.0.6.tgz",
"integrity": "sha512-nH60IZNNxEcrh6L1ZSMNA28rj27ut/2ZmI3r96Zd+1jrZD++zD3LsMIjWlvg4AYrHn/Pqz4CF3veCxGjtbqt7A==",
"dev": true,
"dependencies": {
"@types/d3-selection": "*"
}
},
"node_modules/@types/d3-chord": {
"version": "3.0.6",
"resolved": "https://registry.npmjs.org/@types/d3-chord/-/d3-chord-3.0.6.tgz",
"integrity": "sha512-LFYWWd8nwfwEmTZG9PfQxd17HbNPksHBiJHaKuY1XeqscXacsS2tyoo6OdRsjf+NQYeB6XrNL3a25E3gH69lcg==",
"dev": true
},
"node_modules/@types/d3-color": {
"version": "3.1.3",
"resolved": "https://registry.npmjs.org/@types/d3-color/-/d3-color-3.1.3.tgz",
"integrity": "sha512-iO90scth9WAbmgv7ogoq57O9YpKmFBbmoEoCHDB2xMBY0+/KVrqAaCDyCE16dUspeOvIxFFRI+0sEtqDqy2b4A==",
"dev": true
},
"node_modules/@types/d3-contour": {
"version": "3.0.6",
"resolved": "https://registry.npmjs.org/@types/d3-contour/-/d3-contour-3.0.6.tgz",
"integrity": "sha512-BjzLgXGnCWjUSYGfH1cpdo41/hgdWETu4YxpezoztawmqsvCeep+8QGfiY6YbDvfgHz/DkjeIkkZVJavB4a3rg==",
"dev": true,
"dependencies": {
"@types/d3-array": "*",
"@types/geojson": "*"
}
},
"node_modules/@types/d3-delaunay": {
"version": "6.0.4",
"resolved": "https://registry.npmjs.org/@types/d3-delaunay/-/d3-delaunay-6.0.4.tgz",
"integrity": "sha512-ZMaSKu4THYCU6sV64Lhg6qjf1orxBthaC161plr5KuPHo3CNm8DTHiLw/5Eq2b6TsNP0W0iJrUOFscY6Q450Hw==",
"dev": true
},
"node_modules/@types/d3-dispatch": {
"version": "3.0.7",
"resolved": "https://registry.npmjs.org/@types/d3-dispatch/-/d3-dispatch-3.0.7.tgz",
"integrity": "sha512-5o9OIAdKkhN1QItV2oqaE5KMIiXAvDWBDPrD85e58Qlz1c1kI/J0NcqbEG88CoTwJrYe7ntUCVfeUl2UJKbWgA==",
"dev": true
},
"node_modules/@types/d3-drag": {
"version": "3.0.7",
"resolved": "https://registry.npmjs.org/@types/d3-drag/-/d3-drag-3.0.7.tgz",
"integrity": "sha512-HE3jVKlzU9AaMazNufooRJ5ZpWmLIoc90A37WU2JMmeq28w1FQqCZswHZ3xR+SuxYftzHq6WU6KJHvqxKzTxxQ==",
"dev": true,
"dependencies": {
"@types/d3-selection": "*"
}
},
"node_modules/@types/d3-dsv": {
"version": "3.0.7",
"resolved": "https://registry.npmjs.org/@types/d3-dsv/-/d3-dsv-3.0.7.tgz",
"integrity": "sha512-n6QBF9/+XASqcKK6waudgL0pf/S5XHPPI8APyMLLUHd8NqouBGLsU8MgtO7NINGtPBtk9Kko/W4ea0oAspwh9g==",
"dev": true
},
"node_modules/@types/d3-ease": {
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/@types/d3-ease/-/d3-ease-3.0.2.tgz",
"integrity": "sha512-NcV1JjO5oDzoK26oMzbILE6HW7uVXOHLQvHshBUW4UMdZGfiY6v5BeQwh9a9tCzv+CeefZQHJt5SRgK154RtiA==",
"dev": true
},
"node_modules/@types/d3-fetch": {
"version": "3.0.7",
"resolved": "https://registry.npmjs.org/@types/d3-fetch/-/d3-fetch-3.0.7.tgz",
"integrity": "sha512-fTAfNmxSb9SOWNB9IoG5c8Hg6R+AzUHDRlsXsDZsNp6sxAEOP0tkP3gKkNSO/qmHPoBFTxNrjDprVHDQDvo5aA==",
"dev": true,
"dependencies": {
"@types/d3-dsv": "*"
}
},
"node_modules/@types/d3-force": {
"version": "3.0.10",
"resolved": "https://registry.npmjs.org/@types/d3-force/-/d3-force-3.0.10.tgz",
"integrity": "sha512-ZYeSaCF3p73RdOKcjj+swRlZfnYpK1EbaDiYICEEp5Q6sUiqFaFQ9qgoshp5CzIyyb/yD09kD9o2zEltCexlgw==",
"dev": true
},
"node_modules/@types/d3-format": {
"version": "3.0.4",
"resolved": "https://registry.npmjs.org/@types/d3-format/-/d3-format-3.0.4.tgz",
"integrity": "sha512-fALi2aI6shfg7vM5KiR1wNJnZ7r6UuggVqtDA+xiEdPZQwy/trcQaHnwShLuLdta2rTymCNpxYTiMZX/e09F4g==",
"dev": true
},
"node_modules/@types/d3-geo": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/@types/d3-geo/-/d3-geo-3.1.0.tgz",
"integrity": "sha512-856sckF0oP/diXtS4jNsiQw/UuK5fQG8l/a9VVLeSouf1/PPbBE1i1W852zVwKwYCBkFJJB7nCFTbk6UMEXBOQ==",
"dev": true,
"dependencies": {
"@types/geojson": "*"
}
},
"node_modules/@types/d3-hierarchy": {
"version": "3.1.7",
"resolved": "https://registry.npmjs.org/@types/d3-hierarchy/-/d3-hierarchy-3.1.7.tgz",
"integrity": "sha512-tJFtNoYBtRtkNysX1Xq4sxtjK8YgoWUNpIiUee0/jHGRwqvzYxkq0hGVbbOGSz+JgFxxRu4K8nb3YpG3CMARtg==",
"dev": true
},
"node_modules/@types/d3-interpolate": {
"version": "3.0.4",
"resolved": "https://registry.npmjs.org/@types/d3-interpolate/-/d3-interpolate-3.0.4.tgz",
"integrity": "sha512-mgLPETlrpVV1YRJIglr4Ez47g7Yxjl1lj7YKsiMCb27VJH9W8NVM6Bb9d8kkpG/uAQS5AmbA48q2IAolKKo1MA==",
"dev": true,
"dependencies": {
"@types/d3-color": "*"
}
},
"node_modules/@types/d3-path": {
"version": "3.1.1",
"resolved": "https://registry.npmjs.org/@types/d3-path/-/d3-path-3.1.1.tgz",
"integrity": "sha512-VMZBYyQvbGmWyWVea0EHs/BwLgxc+MKi1zLDCONksozI4YJMcTt8ZEuIR4Sb1MMTE8MMW49v0IwI5+b7RmfWlg==",
"dev": true
},
"node_modules/@types/d3-polygon": {
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/@types/d3-polygon/-/d3-polygon-3.0.2.tgz",
"integrity": "sha512-ZuWOtMaHCkN9xoeEMr1ubW2nGWsp4nIql+OPQRstu4ypeZ+zk3YKqQT0CXVe/PYqrKpZAi+J9mTs05TKwjXSRA==",
"dev": true
},
"node_modules/@types/d3-quadtree": {
"version": "3.0.6",
"resolved": "https://registry.npmjs.org/@types/d3-quadtree/-/d3-quadtree-3.0.6.tgz",
"integrity": "sha512-oUzyO1/Zm6rsxKRHA1vH0NEDG58HrT5icx/azi9MF1TWdtttWl0UIUsjEQBBh+SIkrpd21ZjEv7ptxWys1ncsg==",
"dev": true
},
"node_modules/@types/d3-random": {
"version": "3.0.3",
"resolved": "https://registry.npmjs.org/@types/d3-random/-/d3-random-3.0.3.tgz",
"integrity": "sha512-Imagg1vJ3y76Y2ea0871wpabqp613+8/r0mCLEBfdtqC7xMSfj9idOnmBYyMoULfHePJyxMAw3nWhJxzc+LFwQ==",
"dev": true
},
"node_modules/@types/d3-scale": {
"version": "4.0.9",
"resolved": "https://registry.npmjs.org/@types/d3-scale/-/d3-scale-4.0.9.tgz",
"integrity": "sha512-dLmtwB8zkAeO/juAMfnV+sItKjlsw2lKdZVVy6LRr0cBmegxSABiLEpGVmSJJ8O08i4+sGR6qQtb6WtuwJdvVw==",
"dev": true,
"dependencies": {
"@types/d3-time": "*"
}
},
"node_modules/@types/d3-scale-chromatic": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/@types/d3-scale-chromatic/-/d3-scale-chromatic-3.1.0.tgz",
"integrity": "sha512-iWMJgwkK7yTRmWqRB5plb1kadXyQ5Sj8V/zYlFGMUBbIPKQScw+Dku9cAAMgJG+z5GYDoMjWGLVOvjghDEFnKQ==",
"dev": true
},
"node_modules/@types/d3-selection": {
"version": "3.0.11",
"resolved": "https://registry.npmjs.org/@types/d3-selection/-/d3-selection-3.0.11.tgz",
"integrity": "sha512-bhAXu23DJWsrI45xafYpkQ4NtcKMwWnAC/vKrd2l+nxMFuvOT3XMYTIj2opv8vq8AO5Yh7Qac/nSeP/3zjTK0w==",
"dev": true
},
"node_modules/@types/d3-shape": {
"version": "3.1.7",
"resolved": "https://registry.npmjs.org/@types/d3-shape/-/d3-shape-3.1.7.tgz",
"integrity": "sha512-VLvUQ33C+3J+8p+Daf+nYSOsjB4GXp19/S/aGo60m9h1v6XaxjiT82lKVWJCfzhtuZ3yD7i/TPeC/fuKLLOSmg==",
"dev": true,
"dependencies": {
"@types/d3-path": "*"
}
},
"node_modules/@types/d3-time": {
"version": "3.0.4",
"resolved": "https://registry.npmjs.org/@types/d3-time/-/d3-time-3.0.4.tgz",
"integrity": "sha512-yuzZug1nkAAaBlBBikKZTgzCeA+k1uy4ZFwWANOfKw5z5LRhV0gNA7gNkKm7HoK+HRN0wX3EkxGk0fpbWhmB7g==",
"dev": true
},
"node_modules/@types/d3-time-format": {
"version": "4.0.3",
"resolved": "https://registry.npmjs.org/@types/d3-time-format/-/d3-time-format-4.0.3.tgz",
"integrity": "sha512-5xg9rC+wWL8kdDj153qZcsJ0FWiFt0J5RB6LYUNZjwSnesfblqrI/bJ1wBdJ8OQfncgbJG5+2F+qfqnqyzYxyg==",
"dev": true
},
"node_modules/@types/d3-timer": {
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/@types/d3-timer/-/d3-timer-3.0.2.tgz",
"integrity": "sha512-Ps3T8E8dZDam6fUyNiMkekK3XUsaUEik+idO9/YjPtfj2qruF8tFBXS7XhtE4iIXBLxhmLjP3SXpLhVf21I9Lw==",
"dev": true
},
"node_modules/@types/d3-transition": {
"version": "3.0.9",
"resolved": "https://registry.npmjs.org/@types/d3-transition/-/d3-transition-3.0.9.tgz",
"integrity": "sha512-uZS5shfxzO3rGlu0cC3bjmMFKsXv+SmZZcgp0KD22ts4uGXp5EVYGzu/0YdwZeKmddhcAccYtREJKkPfXkZuCg==",
"dev": true,
"dependencies": {
"@types/d3-selection": "*"
}
},
"node_modules/@types/d3-zoom": {
"version": "3.0.8",
"resolved": "https://registry.npmjs.org/@types/d3-zoom/-/d3-zoom-3.0.8.tgz",
"integrity": "sha512-iqMC4/YlFCSlO8+2Ii1GGGliCAY4XdeG748w5vQUbevlbDu0zSjH/+jojorQVBK/se0j6DUFNPBGSqD3YWYnDw==",
"dev": true,
"dependencies": {
"@types/d3-interpolate": "*",
"@types/d3-selection": "*"
}
},
"node_modules/@types/dagre": {
"version": "0.7.53",
"resolved": "https://registry.npmjs.org/@types/dagre/-/dagre-0.7.53.tgz",
"integrity": "sha512-f4gkWqzPZvYmKhOsDnhq/R8mO4UMcKdxZo+i5SCkOU1wvGeHJeUXGIHeE9pnwGyPMDof1Vx5ZQo4nxpeg2TTVQ==",
"dev": true
},
"node_modules/@types/estree": {
"version": "1.0.8",
"resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz",
"integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==",
"dev": true
},
"node_modules/@types/geojson": {
"version": "7946.0.16",
"resolved": "https://registry.npmjs.org/@types/geojson/-/geojson-7946.0.16.tgz",
"integrity": "sha512-6C8nqWur3j98U6+lXDfTUWIfgvZU+EumvpHKcYjujKH7woYyLj2sUmff0tRhrqM7BohUw7Pz3ZB1jj2gW9Fvmg==",
"dev": true
},
"node_modules/@types/json-schema": {
"version": "7.0.15",
"resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz",
@ -564,6 +938,31 @@
"integrity": "sha512-OTcgaiwfGFBKacvfwuHzzn1KLxH/er8mluiy8/uM3sGXHaRe73RrSIj01jow9t4kJEW633Ov+cOexXeiApTyAw==",
"dev": true
},
"node_modules/@types/prop-types": {
"version": "15.7.15",
"resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.15.tgz",
"integrity": "sha512-F6bEyamV9jKGAFBEmlQnesRPGOQqS2+Uwi0Em15xenOxHaf2hv6L8YCVn3rPdPJOiJfPiCnLIRyvwVaqMY3MIw==",
"dev": true
},
"node_modules/@types/react": {
"version": "18.3.24",
"resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.24.tgz",
"integrity": "sha512-0dLEBsA1kI3OezMBF8nSsb7Nk19ZnsyE1LLhB8r27KbgU5H4pvuqZLdtE+aUkJVoXgTVuA+iLIwmZ0TuK4tx6A==",
"dev": true,
"dependencies": {
"@types/prop-types": "*",
"csstype": "^3.0.2"
}
},
"node_modules/@types/react-dom": {
"version": "18.3.7",
"resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.3.7.tgz",
"integrity": "sha512-MEe3UeoENYVFXzoXEWsvcpg6ZvlrFNlOQ7EOsvhI3CfAXwzPfO8Qwuxd40nepsYKqyyVQnTdEfv68q91yLcKrQ==",
"dev": true,
"peerDependencies": {
"@types/react": "^18.0.0"
}
},
"node_modules/@types/tern": {
"version": "0.23.9",
"resolved": "https://registry.npmjs.org/@types/tern/-/tern-0.23.9.tgz",
@ -915,6 +1314,12 @@
"url": "https://github.com/chalk/chalk?sponsor=1"
}
},
"node_modules/classcat": {
"version": "5.0.5",
"resolved": "https://registry.npmjs.org/classcat/-/classcat-5.0.5.tgz",
"integrity": "sha512-JhZUT7JFcQy/EzW605k/ktHtncoo9vnyW/2GspNYwFlN1C/WmjuV/xtS04e9SOkL2sTdw0VAZ2UGCcQ9lR6p6w==",
"dev": true
},
"node_modules/color-convert": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
@ -964,6 +1369,127 @@
"node": ">= 8"
}
},
"node_modules/csstype": {
"version": "3.1.3",
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz",
"integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==",
"dev": true
},
"node_modules/d3-color": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/d3-color/-/d3-color-3.1.0.tgz",
"integrity": "sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA==",
"dev": true,
"engines": {
"node": ">=12"
}
},
"node_modules/d3-dispatch": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/d3-dispatch/-/d3-dispatch-3.0.1.tgz",
"integrity": "sha512-rzUyPU/S7rwUflMyLc1ETDeBj0NRuHKKAcvukozwhshr6g6c5d8zh4c2gQjY2bZ0dXeGLWc1PF174P2tVvKhfg==",
"dev": true,
"engines": {
"node": ">=12"
}
},
"node_modules/d3-drag": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/d3-drag/-/d3-drag-3.0.0.tgz",
"integrity": "sha512-pWbUJLdETVA8lQNJecMxoXfH6x+mO2UQo8rSmZ+QqxcbyA3hfeprFgIT//HW2nlHChWeIIMwS2Fq+gEARkhTkg==",
"dev": true,
"dependencies": {
"d3-dispatch": "1 - 3",
"d3-selection": "3"
},
"engines": {
"node": ">=12"
}
},
"node_modules/d3-ease": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/d3-ease/-/d3-ease-3.0.1.tgz",
"integrity": "sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w==",
"dev": true,
"engines": {
"node": ">=12"
}
},
"node_modules/d3-interpolate": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/d3-interpolate/-/d3-interpolate-3.0.1.tgz",
"integrity": "sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g==",
"dev": true,
"dependencies": {
"d3-color": "1 - 3"
},
"engines": {
"node": ">=12"
}
},
"node_modules/d3-selection": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/d3-selection/-/d3-selection-3.0.0.tgz",
"integrity": "sha512-fmTRWbNMmsmWq6xJV8D19U/gw/bwrHfNXxrIN+HfZgnzqTHp9jOmKMhsTUjXOJnZOdZY9Q28y4yebKzqDKlxlQ==",
"dev": true,
"engines": {
"node": ">=12"
}
},
"node_modules/d3-timer": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/d3-timer/-/d3-timer-3.0.1.tgz",
"integrity": "sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA==",
"dev": true,
"engines": {
"node": ">=12"
}
},
"node_modules/d3-transition": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/d3-transition/-/d3-transition-3.0.1.tgz",
"integrity": "sha512-ApKvfjsSR6tg06xrL434C0WydLr7JewBB3V+/39RMHsaXTOG0zmt/OAXeng5M5LBm0ojmxJrpomQVZ1aPvBL4w==",
"dev": true,
"dependencies": {
"d3-color": "1 - 3",
"d3-dispatch": "1 - 3",
"d3-ease": "1 - 3",
"d3-interpolate": "1 - 3",
"d3-timer": "1 - 3"
},
"engines": {
"node": ">=12"
},
"peerDependencies": {
"d3-selection": "2 - 3"
}
},
"node_modules/d3-zoom": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/d3-zoom/-/d3-zoom-3.0.0.tgz",
"integrity": "sha512-b8AmV3kfQaqWAuacbPuNbL6vahnOJflOhexLzMMNLga62+/nh0JzvJ0aO/5a5MVgUFGS7Hu1P9P03o3fJkDCyw==",
"dev": true,
"dependencies": {
"d3-dispatch": "1 - 3",
"d3-drag": "2 - 3",
"d3-interpolate": "1 - 3",
"d3-selection": "2 - 3",
"d3-transition": "2 - 3"
},
"engines": {
"node": ">=12"
}
},
"node_modules/dagre": {
"version": "0.8.5",
"resolved": "https://registry.npmjs.org/dagre/-/dagre-0.8.5.tgz",
"integrity": "sha512-/aTqmnRta7x7MCCpExk7HQL2O4owCT2h8NT//9I1OQ9vt29Pa0BzSAkR5lwFUcQ7491yVi/3CXU9jQ5o0Mn2Sw==",
"dev": true,
"dependencies": {
"graphlib": "^2.1.8",
"lodash": "^4.17.15"
}
},
"node_modules/debug": {
"version": "4.4.1",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz",
@ -1493,6 +2019,15 @@
"dev": true,
"peer": true
},
"node_modules/graphlib": {
"version": "2.1.8",
"resolved": "https://registry.npmjs.org/graphlib/-/graphlib-2.1.8.tgz",
"integrity": "sha512-jcLLfkpoVGmH7/InMC/1hIvOPSUh38oJtGhvrOFGzioE1DZ+0YW16RgmOJhHiuWTvGiJQ9Z1Ik43JvkRPRvE+A==",
"dev": true,
"dependencies": {
"lodash": "^4.17.15"
}
},
"node_modules/has-flag": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
@ -1605,6 +2140,12 @@
"dev": true,
"peer": true
},
"node_modules/js-tokens": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
"integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==",
"dev": true
},
"node_modules/js-yaml": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz",
@ -1679,6 +2220,12 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/lodash": {
"version": "4.17.21",
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz",
"integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==",
"dev": true
},
"node_modules/lodash.merge": {
"version": "4.6.2",
"resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz",
@ -1686,6 +2233,18 @@
"dev": true,
"peer": true
},
"node_modules/loose-envify": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz",
"integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==",
"dev": true,
"dependencies": {
"js-tokens": "^3.0.0 || ^4.0.0"
},
"bin": {
"loose-envify": "cli.js"
}
},
"node_modules/merge2": {
"version": "1.4.1",
"resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz",
@ -1921,6 +2480,49 @@
}
]
},
"node_modules/react": {
"version": "18.3.1",
"resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz",
"integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==",
"dev": true,
"dependencies": {
"loose-envify": "^1.1.0"
},
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/react-dom": {
"version": "18.3.1",
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz",
"integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==",
"dev": true,
"dependencies": {
"loose-envify": "^1.1.0",
"scheduler": "^0.23.2"
},
"peerDependencies": {
"react": "^18.3.1"
}
},
"node_modules/reactflow": {
"version": "11.11.4",
"resolved": "https://registry.npmjs.org/reactflow/-/reactflow-11.11.4.tgz",
"integrity": "sha512-70FOtJkUWH3BAOsN+LU9lCrKoKbtOPnz2uq0CV2PLdNSwxTXOhCbsZr50GmZ+Rtw3jx8Uv7/vBFtCGixLfd4Og==",
"dev": true,
"dependencies": {
"@reactflow/background": "11.3.14",
"@reactflow/controls": "11.2.14",
"@reactflow/core": "11.11.4",
"@reactflow/minimap": "11.7.14",
"@reactflow/node-resizer": "2.2.14",
"@reactflow/node-toolbar": "1.3.14"
},
"peerDependencies": {
"react": ">=17",
"react-dom": ">=17"
}
},
"node_modules/regexpp": {
"version": "3.2.0",
"resolved": "https://registry.npmjs.org/regexpp/-/regexpp-3.2.0.tgz",
@ -1993,6 +2595,15 @@
"queue-microtask": "^1.2.2"
}
},
"node_modules/scheduler": {
"version": "0.23.2",
"resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.2.tgz",
"integrity": "sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==",
"dev": true,
"dependencies": {
"loose-envify": "^1.1.0"
}
},
"node_modules/semver": {
"version": "7.7.2",
"resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz",
@ -2156,16 +2767,16 @@
}
},
"node_modules/typescript": {
"version": "4.7.4",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-4.7.4.tgz",
"integrity": "sha512-C0WQT0gezHuw6AdY1M2jxUO83Rjf0HP7Sk1DtXj6j1EwkQNZrHAg2XPWlq62oqEhYvONq5pkC2Y9oPljWToLmQ==",
"version": "5.9.2",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.2.tgz",
"integrity": "sha512-CWBzXQrc/qOkhidw1OzBTQuYRbfyxDXJMVJ1XNwUHGROVmuaeiEm3OslpZ1RV96d7SKKjZKrSJu3+t/xlw3R9A==",
"dev": true,
"bin": {
"tsc": "bin/tsc",
"tsserver": "bin/tsserver"
},
"engines": {
"node": ">=4.2.0"
"node": ">=14.17"
}
},
"node_modules/uri-js": {
@ -2178,6 +2789,15 @@
"punycode": "^2.1.0"
}
},
"node_modules/use-sync-external-store": {
"version": "1.5.0",
"resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.5.0.tgz",
"integrity": "sha512-Rb46I4cGGVBmjamjphe8L/UnvJD+uPPtTkNvX5mZgqdbavhI4EbgIWJiIHXJ8bc/i9EQGPRh4DwEURJ552Do0A==",
"dev": true,
"peerDependencies": {
"react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
}
},
"node_modules/w3c-keyname": {
"version": "2.2.8",
"resolved": "https://registry.npmjs.org/w3c-keyname/-/w3c-keyname-2.2.8.tgz",
@ -2230,6 +2850,34 @@
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/zustand": {
"version": "4.5.7",
"resolved": "https://registry.npmjs.org/zustand/-/zustand-4.5.7.tgz",
"integrity": "sha512-CHOUy7mu3lbD6o6LJLfllpjkzhHXSBlX8B9+qPddUsIfeF5S/UZ5q0kmCsnRqT1UHFQZchNFDDzMbQsuesHWlw==",
"dev": true,
"dependencies": {
"use-sync-external-store": "^1.2.2"
},
"engines": {
"node": ">=12.7.0"
},
"peerDependencies": {
"@types/react": ">=16.8",
"immer": ">=9.0.6",
"react": ">=16.8"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
},
"immer": {
"optional": true
},
"react": {
"optional": true
}
}
}
}
}

View File

@ -4,21 +4,28 @@
"description": "This is a sample plugin for Obsidian (https://obsidian.md)",
"main": "main.js",
"scripts": {
"dev": "node esbuild.config.mjs",
"build": "tsc -noEmit -skipLibCheck && node esbuild.config.mjs production",
"dev": "node esbuild.config.mjs && move main.css styles.css",
"build": "tsc -noEmit -skipLibCheck && node esbuild.config.mjs production && move main.css styles.css",
"version": "node version-bump.mjs && git add manifest.json versions.json"
},
"keywords": [],
"author": "",
"license": "MIT",
"devDependencies": {
"@types/dagre": "^0.7.52",
"@types/node": "^16.11.6",
"@types/react": "^18.3.3",
"@types/react-dom": "^18.3.7",
"@typescript-eslint/eslint-plugin": "5.29.0",
"@typescript-eslint/parser": "5.29.0",
"builtin-modules": "3.3.0",
"dagre": "^0.8.5",
"esbuild": "0.17.3",
"obsidian": "latest",
"react": "^18.3.1",
"react-dom": "^18.3.1",
"reactflow": "^11.11.3",
"tslib": "^2.4.0",
"typescript": "4.7.4"
"typescript": "^5.4.5"
}
}

View File

@ -1,3 +1,8 @@
/*
* Управляет пользовательским интерфейсом чата, включая ввод сообщений,
* отображение истории и подсказки команд для взаимодействия с пользователем.
*/
import { ChatHistoryItem } from './models/ChatHistoryItem';
export class ChatPanel {
@ -120,7 +125,7 @@ export class ChatPanel {
historyContainer.innerHTML = this.chatHistory
.map(
(msg) => `
<div class="message ${msg.role}">
<div class="message ${msg.role} message-type-${msg.type || 'default'}">
<div class="message-content">${this.processMessageContent(msg.content)}</div>
</div>
`

View File

@ -1,138 +1,302 @@
export class GraphPanel {
container: HTMLElement;
props: {
graphData: { nodes: any[]; edges: any[] };
onNodeClick: (nodeId: string) => void;
};
nodes: any[];
edges: any[];
// ----------------------------------------------- React & React Flow Imports ---------------------------------------------------
import * as React from 'react';
import * as ReactDOM from 'react-dom/client'; // <-- ИСПРАВЛЕНО: для createRoot нужен 'react-dom/client'
import ReactFlow, {
addEdge,
useNodesState,
useEdgesState,
Background,
Controls,
ReactFlowProvider, // <-- ИСПРАВЛЕНО: импортируем прямо ReactFlowProvider без переименования
Connection,
Edge,
BackgroundVariant // <-- ИСПРАВЛЕНО: используем BackgroundVariant для типизации
} from 'reactflow';
constructor(
container: HTMLElement,
props: {
graphData: { nodes: any[]; edges: any[] };
onNodeClick: (nodeId: string) => void;
}
) {
this.container = container;
this.props = props;
this.nodes = [];
this.edges = [];
// <-- ИСПРАВЛЕНО: Это ключевой момент. Убедитесь, что esbuild настроен на обработку CSS импортов.
// Обычно, при использовании esbuild для React-проектов, вы импортируете CSS прямо в JS/TS файл.
// esbuild должен быть настроен для копирования или встраивания этого CSS.
// Если стили не применяются, проверьте конфигурацию esbuild (например, через плагин esbuild-plugin-css-bundle).
import 'reactflow/dist/style.css'; // Импортируем стили ReactFlow
this.init();
}
// Если вы используете какие-либо кастомные ноды, импортируйте их здесь
// import CustomNode from './CustomNode';
// const nodeTypes = { custom: CustomNode };
init(): void {
// Создаем структуру для React Flow
this.container.innerHTML = `
<div class="llm-agent-graph-panel">
<div id="reactflow-container" style="width: 100%; height: 100%;"></div>
</div>
`;
// ----------------------------------------------- Custom Nodes -----------------------------------------------------------------
// Загружаем React Flow динамически (если доступен)
this.initReactFlow();
}
import { Handle, Position } from 'reactflow';
async initReactFlow(): Promise<void> {
try {
// В среде Obsidian нужно будет подключить React Flow через CDN или bundler
// Для демонстрации создаем упрощенную визуализацию
this.createSimpleGraph();
} catch (error) {
console.error('Ошибка инициализации React Flow:', error);
this.createFallbackGraph();
}
}
// Простой компонент для узла типа 'user'
// Это базовый пример. Ты можешь стилизовать его по своему усмотрению.
const UserNode: React.FC<any> = ({ data, isConnectable }) => {
return React.createElement('div', {
className: 'react-flow__node-user'
},
React.createElement(Handle, {
type: "source",
position: Position.Bottom,
isConnectable: isConnectable,
}),
React.createElement(Handle, {
type: "target",
position: Position.Top,
isConnectable: isConnectable,
}),
data.label
);
};
createSimpleGraph(): void {
const container = this.container.querySelector('#reactflow-container') as HTMLElement;
container.innerHTML = `
<div class="simple-graph">
<div class="graph-nodes" id="graph-nodes"></div>
<svg class="graph-edges" id="graph-edges"></svg>
</div>
`;
// Простой компонент для узла типа 'llm'
const LLMNode: React.FC<any> = ({ data, isConnectable }) => {
return React.createElement('div', {
className: 'react-flow__node-llm'
},
React.createElement(Handle, {
type: "source",
position: Position.Bottom,
isConnectable: isConnectable,
}),
React.createElement(Handle, {
type: "target",
position: Position.Top,
isConnectable: isConnectable,
}),
data.label
);
};
this.renderNodes();
this.renderEdges();
}
// Простой компонент для узла типа 'tool'
const ToolNode: React.FC<any> = ({ data, isConnectable }) => {
return React.createElement('div', {
className: 'react-flow__node-tool'
},
React.createElement(Handle, {
type: "source",
position: Position.Bottom,
isConnectable: isConnectable,
}),
React.createElement(Handle, {
type: "target",
position: Position.Top,
isConnectable: isConnectable,
}),
data.label
);
};
renderNodes(): void {
const nodesContainer = this.container.querySelector('#graph-nodes') as HTMLElement;
nodesContainer.innerHTML = '';
// Простой компонент для узла типа 'error'
const ErrorNode: React.FC<any> = ({ data, isConnectable }) => {
return React.createElement('div', {
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
);
};
this.nodes.forEach((node, index) => {
const nodeEl = document.createElement('div');
nodeEl.className = `graph-node node-${node.type || 'default'}`;
nodeEl.style.cssText = `
position: absolute;
left: ${50 + index * 150}px;
top: ${50 + Math.floor(index / 5) * 80}px;
padding: 8px 12px;
border: 2px solid #333;
border-radius: 4px;
background: #fff;
cursor: pointer;
max-width: 120px;
word-wrap: break-word;
`;
nodeEl.textContent = node.data?.label || `Node ${node.id}`;
nodeEl.dataset.nodeId = node.id;
const AssistantNode: React.FC<any> = ({ data, isConnectable }) => {
return React.createElement('div', {
className: 'react-flow__node-assistant' // Используем CSS класс для узла assistant
},
React.createElement(Handle, {
type: "source",
position: Position.Bottom,
isConnectable: isConnectable,
}),
React.createElement(Handle, {
type: "target",
position: Position.Top,
isConnectable: isConnectable,
}),
data.label
);
};
nodeEl.addEventListener('click', () => {
this.handleNodeClick(node.id);
});
// Регистрируем кастомные типы узлов
const nodeTypes = {
user: UserNode,
llm: LLMNode,
tool: ToolNode,
error: ErrorNode,
assistant: AssistantNode,
// Если у тебя есть node.type === 'default', то можешь его тоже определить,
// либо использовать встроенный 'default' React Flow.
};
nodesContainer.appendChild(nodeEl);
});
}
// ----------------------------------------------- Dagre Layout -----------------------------------------------------------------
import dagre from 'dagre'; // Импортируем библиотеку dagre
renderEdges(): void {
const edgesContainer = this.container.querySelector('#graph-edges') as HTMLElement;
edgesContainer.innerHTML = '';
// Упрощенная отрисовка связей - в реальной реализации нужно будет вычислять позиции
}
createFallbackGraph(): void {
const container = this.container.querySelector('#reactflow-container') as HTMLElement;
container.innerHTML = `
<div class="fallback-graph">
<p>Упрощенное представление графа</p>
<div id="nodes-list"></div>
</div>
`;
this.renderFallbackNodes();
}
renderFallbackNodes(): void {
const nodesList = this.container.querySelector('#nodes-list') as HTMLElement;
nodesList.innerHTML = '';
this.nodes.forEach((node) => {
const nodeEl = document.createElement('div');
nodeEl.className = 'fallback-node';
nodeEl.textContent = `${node.id}: ${node.data?.label || 'No label'}`;
nodeEl.addEventListener('click', () => this.handleNodeClick(node.id));
nodesList.appendChild(nodeEl);
});
}
updateData(graphData: { nodes: any[]; edges: any[] }): void {
this.nodes = graphData.nodes || [];
this.edges = graphData.edges || [];
this.renderNodes();
this.renderEdges();
}
handleNodeClick(nodeId: string): void {
if (this.props.onNodeClick) {
this.props.onNodeClick(nodeId);
}
}
destroy(): void {
// Очистка ресурсов
this.container.innerHTML = '';
}
// Типы для Dagre (были уже корректны)
interface DagreNode {
x: number;
y: number;
width: number;
height: number;
}
// Инициализируем объект Dagre Graph
const dagreGraph = new dagre.graphlib.Graph().setDefaultEdgeLabel(() => ({}));
// Определяем размеры узлов для корректного расположения.
// Может потребоваться корректировка в зависимости от реального размера узла в CSS.
const nodeWidth = 172;
const nodeHeight = 36;
/**
* Применяет алгоритм расположения Dagre к узлам и рёбрам графа.
* @param {Array<any>} nodes - Массив узлов React Flow.
* @param {Array<any>} edges - Массив рёбер React Flow.
* @param {'TB' | 'LR'} [direction='TB'] - Направление графа ('TB' - сверху вниз, 'LR' - слева направо).
* @returns {{nodes: any[], edges: any[]}} Объект с обновлёнными узлами и рёбрами, содержащими позиции.
*/
const getLayoutedElements = (nodes: any[], edges: any[], direction: 'TB' | 'LR' = 'TB') => {
const isHorizontal = direction === 'LR'; // Проверяем, горизонтальное ли расположение
// ДОБАВЛЕНО: ranksep и nodesep для лучшего отступа между узлами
dagreGraph.setGraph({ rankdir: direction, ranksep: 100, nodesep: 50 });
// Добавляем узлы в граф Dagre с их размерами
nodes.forEach((node) => {
// Убедимся, что узел имеет уникальный id и необходимые размеры для Dagre
dagreGraph.setNode(node.id, { width: nodeWidth, height: nodeHeight });
});
// Добавляем рёбра в граф Dagre
edges.forEach((edge) => {
// Dagre ожидает id, source, target
dagreGraph.setEdge(edge.source, edge.target);
});
// Выполняем расчёт расположения графа
dagre.layout(dagreGraph);
// Обновляем позиции узлов React Flow на основе расчётов Dagre
const newNodes = nodes.map((node) => {
const nodeWithPosition: DagreNode = dagreGraph.node(node.id); // Получаем данные узла из Dagre
// Смещаем позицию узла Dagre (якорь в центре) к верхнему левому углу,
// чтобы она соответствовала точке привязки узла React Flow (верхний левый угол).
return {
...node,
// Обновляем targetPosition и sourcePosition в зависимости от направления для корректного отображения ручек
targetPosition: isHorizontal ? 'left' : 'top',
sourcePosition: isHorizontal ? 'right' : 'bottom',
position: {
x: nodeWithPosition.x - nodeWidth / 2, // Корректировка X
y: nodeWithPosition.y - nodeHeight / 2, // Корректировка Y
},
};
});
return { nodes: newNodes, edges }; // Возвращаем обновлённые узлы и рёбра
};
/**
* @typedef {object} GraphPanelProps
* @property {{ nodes: any[]; edges: any[]; }} graphData - Данные графа для отображения (узлы и ребра).
* @property {(nodeId: string) => void} onNodeClick - Обработчик клика по узлу.
*/
interface GraphPanelProps {
graphData: { nodes: any[]; edges: any[]; };
onNodeClick: (nodeId: string) => void;
}
/**
* GraphPanelComponent - React компонент для отображения графа диалогов с использованием React Flow и Dagre.
* @param {GraphPanelProps} props - Свойства компонента.
* @returns {React.ReactElement} Элемент React.
*/
export const GraphPanelComponent: React.FC<GraphPanelProps> = ({ graphData, onNodeClick }) => {
// ----------------------------------------------- React Hooks ---------------------------------------------------------------
// Инициализируем состояния для узлов и рёбер React Flow
const [nodes, setNodes, onNodesChange] = useNodesState([]);
const [edges, setEdges, onEdgesChange] = useEdgesState([]);
// Используем useEffect для применения Dagre layout при изменении данных графа
React.useEffect(() => {
// Проверяем, есть ли данные для построения графа
if (graphData.nodes.length > 0 || graphData.edges.length > 0) {
console.log("Applying Dagre layout with data:", graphData);
// Преобразуем входящие данные в формат, необходимый для Dagre и React Flow
// Убеждаемся, что каждый узел имеет данные.label для отображения
const initialNodesForLayout = graphData.nodes.map(node => ({
id: node.id,
// ДОБАВЛЕНО: используем node.type, если он есть, иначе 'default'
type: node.type || 'default',
data: { label: node.data?.label || `Node ${node.id}` },
position: { x: 0, y: 0 } // Временная позиция, будет перезаписана Dagre
}));
const initialEdgesForLayout = graphData.edges.map(edge => ({
id: edge.id,
source: edge.source,
target: edge.target,
// Для рёбер можно добавить тип 'default' или ваш кастомный
type: edge.type || 'default',
}));
// Применяем Dagre layout
const { nodes: layoutedNodes, edges: layoutedEdges } = getLayoutedElements(
initialNodesForLayout,
initialEdgesForLayout
);
// Обновляем состояния узлов и рёбер React Flow
setNodes(layoutedNodes);
setEdges(layoutedEdges);
} else {
// Если данных нет, очищаем граф
setNodes([]);
setEdges([]);
}
}, [graphData.nodes, graphData.edges]); // Зависимости: изменения в узлах и рёбрах графа
// ----------------------------------------------- Event Handlers ------------------------------------------------------------
/**
* Обработчик добавления нового соединения между узлами (если разрешено).
* @param {Connection | Edge} params - Параметры нового соединения.
*/
const onConnect = React.useCallback(
(params: Connection | Edge) => setEdges((eds) => addEdge(params, eds)),
[setEdges]
);
/**
* Обработчик клика по узлу.
* @param {React.MouseEvent} event - Объект события клика.
* @param {any} node - Данные узла, по которому был клик.
*/
const nodeClickHandler = React.useCallback((event: React.MouseEvent, node: any) => {
onNodeClick(node.id);
}, [onNodeClick]);
// ----------------------------------------------- Render --------------------------------------------------------------------
return React.createElement('div', { className: 'graph-panel', style: { width: '100%', height: '100%' } },
// Оборачиваем ReactFlowProvider вокруг ReactFlow для доступа к контексту React Flow
React.createElement(ReactFlowProvider, null,
React.createElement(ReactFlow, {
nodes,
edges,
onNodesChange, // Обработчик изменения узлов
onEdgesChange, // Обработчик изменения рёбер
onConnect, // Обработчик подключения
onNodeClick: nodeClickHandler, // Обработчик клика по узлу
fitView: true, // Автоматически центрирует и масштабирует граф при загрузке
attributionPosition: 'bottom-right', // Позиция атрибуции React Flow
nodeTypes, // <-- ИСПРАВЛЕНО: Передаем зарегистрированные типы узлов
},
// Компоненты Controls и Background являются дочерними для ReactFlow
React.createElement(Controls, null),
React.createElement(Background, { variant: BackgroundVariant.Dots, gap: 12, size: 1 })
)
)
);
};

View File

@ -1,3 +1,8 @@
/*
* Управляет и отображает историю графов чата. Предоставляет функционал для выбора
* прошлых разговоров, начала новых чатов и удаления записей графов.
*/
export class HistoryPanel {
container: HTMLElement;
props: {

View File

@ -1,4 +1,10 @@
/*
* Определяет интерфейс для элемента истории чата, включающего роль участника
* (пользователь/агент) и содержимое сообщения.
*/
export interface ChatHistoryItem {
role: string;
content: string;
}
type?: 'user' | 'llm' | 'tool' | 'error' | 'default';
}

104
src/css/styles.css Normal file
View File

@ -0,0 +1,104 @@
/* src/styles.css или куда ты добавляешь стили */
:root {
/* Цвета сообщений чата */
--llm-user-border-color: #78B0E3;
--llm-user-background-color: #C5E1FF;
--llm-llm-border-color: #A8D8AD;
--llm-llm-background-color: #DBF5DD;
--llm-tool-border-color: #FFD180;
--llm-tool-background-color: #FFF0C9;
--llm-error-border-color: #FF6666;
--llm-error-background-color: #FFCCCC;
--llm-assistant-border-color: #B0B0B0; /* Нейтральный серый для ассистента */
--llm-assistant-background-color: #E0E0E0; /* Светло-серый фон */
/* Общие стили узлов графа */
--node-padding: 10px;
--node-border-radius: 5px;
--node-font-size: 14px;
--node-font-weight: bold;
--node-text-color: #333;
--node-text-align: center;
}
.llm-agent-chat-panel .message-type-user .message-content {
border: 1px solid var(--llm-user-border-color); /* Цвет для user */
background-color: var(--llm-user-background-color); /* Светло-голубой фон для user */
border-radius: 5px;
padding: 10px;
}
.llm-agent-chat-panel .message-type-llm .message-content {
border: 1px solid var(--llm-llm-border-color); /* Цвет для llm */
background-color: var(--llm-llm-background-color); /* Светло-зеленый фон для llm */
border-radius: 5px;
padding: 10px;
}
.llm-agent-chat-panel .message-type-tool .message-content {
border: 1px solid var(--llm-tool-border-color); /* Цвет для tool */
background-color: var(--llm-tool-background-color); /* Светло-оранжевый фон для tool */
border-radius: 5px;
padding: 10px;
}
.llm-agent-chat-panel .message-type-error .message-content {
border: 1px solid var(--llm-error-border-color); /* Красный цвет для error */
background-color: var(--llm-error-background-color); /* Светло-красный фон для error */
border-radius: 5px;
padding: 10px;
}
.llm-agent-chat-panel .message-type-assistant .message-content {
border: 1px solid var(--llm-assistant-border-color); /* Цвет для assistant */
background-color: var(--llm-assistant-background-color); /* Светло-серый фон для assistant */
border-radius: 5px;
padding: 10px;
}
/* Общие стили для сообщений */
.llm-agent-chat-panel .message {
margin-bottom: 10px;
display: flex; /* Для выравнивания по сторонам */
}
/* Стили для узлов графа */
.react-flow__node-default,
.react-flow__node-user,
.react-flow__node-llm,
.react-flow__node-tool,
.react-flow__node-error,
.react-flow__node-assistant {
padding: var(--node-padding);
border-radius: var(--node-border-radius);
text-align: var(--node-text-align);
font-size: var(--node-font-size);
font-weight: var(--node-font-weight);
color: var(--node-text-color);
}
.react-flow__node-user {
border: 1px solid var(--llm-user-border-color); /* Цвет для user */
background-color: var(--llm-user-background-color); /* Светло-голубой фон для user */
}
.react-flow__node-llm {
border: 1px solid var(--llm-llm-border-color); /* Цвет для llm */
background-color: var(--llm-llm-background-color); /* Светло-зеленый фон для llm */
}
.react-flow__node-tool {
border: 1px solid var(--llm-tool-border-color); /* Цвет для tool */
background-color: var(--llm-tool-background-color); /* Светло-оранжевый фон для tool */
}
.react-flow__node-error {
border: 1px solid var(--llm-error-border-color); /* Красный цвет для error */
background-color: var(--llm-error-background-color); /* Светло-красный фон для error */
}
.react-flow__node-assistant {
border: 1px solid var(--llm-assistant-border-color); /* Нейтральный серый для ассистента */
background-color: var(--llm-assistant-background-color); /* Светло-серый фон */
}

View File

@ -1,4 +1,7 @@
// ----------------------------------------------- Event Bus ---------------------------------------------------------------
/*
* EventBus предоставляет централизованный механизм для подписки и публикации событий
* между различными компонентами приложения, обеспечивая их слабую связанность.
*/
export class EventBus {
private events: { [key: string]: ((data?: any) => void)[] } = {};

View File

@ -1,6 +1,10 @@
/*
* ChatView управляет пользовательским интерфейсом чата, отображает историю сообщений,
* отправляет запросы LLM-агенту и обрабатывает ответы, обновляя состояние чата и графа.
*/
import { ItemView, WorkspaceLeaf } from 'obsidian';
import { ChatPanel } from '../components/ChatPanel';
import { HistoryPanel } from '../components/HistoryPanel';
import LLMAgentPlugin from 'main';
export const CHAT_VIEW_TYPE = 'llm-agent-chat-view';
@ -10,23 +14,18 @@ export const CHAT_VIEW_TYPE = 'llm-agent-chat-view';
export class ChatView extends ItemView {
plugin: LLMAgentPlugin;
chatHistory: any[];
graphsList: any[];
selectedGraphId: string | null;
currentNode: string | null;
chatContainer: HTMLDivElement;
historyContainer: HTMLDivElement;
chatPanel: ChatPanel | null;
historyPanel: HistoryPanel | null;
constructor(leaf: WorkspaceLeaf, plugin: LLMAgentPlugin) {
super(leaf);
this.plugin = plugin;
this.chatHistory = [];
this.graphsList = [];
this.selectedGraphId = null;
this.currentNode = null;
this.chatPanel = null;
this.historyPanel = null;
}
getViewType(): string {
@ -45,31 +44,20 @@ export class ChatView extends ItemView {
const container = this.containerEl.children[1] as HTMLElement;
container.empty();
// Создаем контейнеры для React компонентов
// Создаем только контейнер для чата (без истории)
this.chatContainer = container.createDiv() as HTMLDivElement;
this.chatContainer.addClass('llm-agent-chat-container');
this.historyContainer = container.createDiv() as HTMLDivElement;
this.historyContainer.addClass('llm-agent-history-container');
// Инициализируем React компоненты
// Инициализируем только чат панель
this.chatPanel = new ChatPanel(this.chatContainer, {
chatHistory: this.chatHistory,
onSendMessage: this.handleSendMessage.bind(this)
});
this.historyPanel = new HistoryPanel(this.historyContainer, {
graphs: this.graphsList,
onGraphSelect: this.handleGraphSelect.bind(this),
onNewChat: this.handleNewChat.bind(this)
});
// Подписываемся на события
this.plugin.eventBus.on('node-selected', this.handleNodeSelected.bind(this));
this.plugin.eventBus.on('new-chat', this.handleNewChat.bind(this));
// Загружаем список графов
this.fetchGraphsList();
this.plugin.eventBus.on('graph-selected', this.handleGraphSelected.bind(this));
}
async onClose(): Promise<void> {
@ -77,145 +65,143 @@ export class ChatView extends ItemView {
this.chatPanel.destroy();
this.chatPanel = null;
}
if (this.historyPanel) {
this.historyPanel.destroy();
this.historyPanel = null;
}
this.plugin.eventBus.off('node-selected', this.handleNodeSelected);
this.plugin.eventBus.off('new-chat', this.handleNewChat);
this.plugin.eventBus.off('graph-selected', this.handleGraphSelected);
}
// ----------------------------------------------- API Methods ---------------------------------------------------------------
async fetchGraphsList(): Promise<void> {
try {
const response = await fetch(`${this.plugin.settings.apiBaseUrl}/graphs`);
const data = await response.json();
this.graphsList = data;
if (this.historyPanel) {
this.historyPanel.updateGraphs(this.graphsList);
}
} catch (error) {
console.error('Ошибка загрузки списка графов:', error);
}
}
async fetchGraphData(graphId: string): Promise<void> {
try {
const response = await fetch(`${this.plugin.settings.apiBaseUrl}/graphs/${graphId}`);
if (!response.ok) {
if (response.status === 404) {
console.error(`Граф с ID ${graphId} не найден.`);
}
throw new Error(`HTTP ошибка! статус: ${response.status}`);
}
const data = await response.json();
// Обновляем граф
this.plugin.eventBus.emit('graph-updated', {
graphData: {
nodes: data.graph_nodes || [],
edges: data.graph_edges || []
},
currentNode: data.current_node_id
});
// Обновляем историю чата
this.chatHistory = data.messages || [];
if (this.chatPanel) {
this.chatPanel.updateHistory(this.chatHistory);
}
} catch (error) {
console.error('Ошибка загрузки данных графа:', error);
}
}
async fetchMessagesFromRootToNode(nodeId: string): Promise<void> {
try {
const response = await fetch(`${this.plugin.settings.apiBaseUrl}/messages/${this.selectedGraphId}/${nodeId}`);
const data = await response.json();
this.chatHistory = data;
if (this.chatPanel) {
this.chatPanel.updateHistory(this.chatHistory);
}
} catch (error) {
console.error('Ошибка загрузки сообщений:', error);
}
}
/**
* Загружает историю сообщений от корня до указанного узла графа.
* Обновляет `chatHistory` и отображение в `chatPanel`.
* @param {string} nodeId - ID конечного узла для загрузки истории.
* @returns {Promise<void>}
*/
async fetchMessagesFromRootToNode(nodeId: string): Promise<void> {
try {
// Убедимся, что selectedGraphId установлен
if (!this.selectedGraphId) {
console.warn('selectedGraphId не установлен. Невозможно загрузить сообщения.');
return;
}
const response = await fetch(`${this.plugin.settings.apiBaseUrl}/messages/${this.selectedGraphId}/${nodeId}`);
if (!response.ok) {
const errorText = await response.text();
throw new Error(`Ошибка загрузки сообщений: ${response.status} - ${errorText}`);
}
const data = await response.json();
// ИСПРАВЛЕНО: Убедиться, что type передается, если это возможно
this.chatHistory = data.map((msg: any) => ({
...msg,
type: msg.type || msg.role // Используем type, если есть, иначе role как fallback
})) || [];
if (this.chatPanel) {
this.chatPanel.updateHistory(this.chatHistory);
}
} catch (error) {
console.error('Ошибка загрузки сообщений:', error);
// Optionally: show a notice to the user
// new Notice(`Ошибка загрузки сообщений: ${error.message}`);
}
}
// ----------------------------------------------- Event Handlers ---------------------------------------------------------------
async handleSendMessage(message: string): Promise<void> {
try {
const response = await fetch(`${this.plugin.settings.apiBaseUrl}/chat`, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
message: message,
graph_id: this.selectedGraphId,
parent_node_id: this.currentNode
})
});
async handleSendMessage(message: string): Promise<void> {
try {
const response = await fetch(`${this.plugin.settings.apiBaseUrl}/chat`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
message: message,
graph_id: this.selectedGraphId,
parent_node_id: this.currentNode,
}),
});
if (!response.ok) {
throw new Error(`HTTP ошибка! статус: ${response.status}`);
}
if (!response.ok) {
throw new Error(`HTTP ошибка! статус: ${response.status}`);
}
const result = await response.json();
const result = await response.json();
this.selectedGraphId = result.graph_id;
const newCurrentNodeId = result.graph_visualization_data.current_node_id;
this.currentNode = newCurrentNodeId;
// Обновляем выбранный ID графа
this.selectedGraphId = result.graph_id;
// Обновляем граф
this.plugin.eventBus.emit('graph-updated', {
graphData: {
nodes: result.graph_visualization_data.nodes || [],
edges: result.graph_visualization_data.edges || []
},
currentNode: newCurrentNodeId
});
// Устанавливаем текущий узел
const newCurrentNodeId = result.graph_visualization_data.current_node_id;
this.currentNode = newCurrentNodeId;
// Загружаем сообщения
if (newCurrentNodeId) {
await this.fetchMessagesFromRootToNode(newCurrentNodeId);
} else {
// ИСПРАВЛЕНО: Убедиться, что type передается, если это возможно
this.chatHistory = result.messages.map((msg: any) => ({
...msg,
type: msg.type || msg.role // Используем type, если есть, иначе role как fallback
})) || [];
if (this.chatPanel) {
this.chatPanel.updateHistory(this.chatHistory);
}
}
// Обновляем граф
this.plugin.eventBus.emit('graph-updated', {
graphData: {
nodes: result.graph_visualization_data.nodes || [],
edges: result.graph_visualization_data.edges || []
},
currentNode: newCurrentNodeId
});
} catch (error) {
console.error('Ошибка отправки сообщения:', error);
}
}
// Загружаем сообщения от корня до нового текущего узла
if (newCurrentNodeId) {
await this.fetchMessagesFromRootToNode(newCurrentNodeId);
} else {
this.chatHistory = result.messages || [];
if (this.chatPanel) {
this.chatPanel.updateHistory(this.chatHistory);
}
}
/**
* Обрабатывает выбор узла на графе. Загружает историю сообщений до выбранного узла
* и обновляет панель чата.
* @param {{ nodeId: string }} data - Объект, содержащий ID выбранного узла.
*/
handleNodeSelected(data: { nodeId: string }): void {
this.currentNode = data.nodeId;
if (this.selectedGraphId) {
this.fetchMessagesFromRootToNode(data.nodeId);
}
}
// Обновляем список графов
this.fetchGraphsList();
} catch (error) {
console.error('Ошибка отправки сообщения:', error);
}
}
/**
* Обрабатывает событие начала нового чата. Сбрасывает текущее состояние чата
* и очищает отображение истории.
*/
handleNewChat(): void {
this.chatHistory = [];
this.currentNode = null;
this.selectedGraphId = null;
if (this.chatPanel) {
this.chatPanel.updateHistory(this.chatHistory);
}
}
handleNodeSelected(data: { nodeId: string }): void {
this.currentNode = data.nodeId;
if (this.selectedGraphId) {
this.fetchMessagesFromRootToNode(data.nodeId);
}
}
handleGraphSelect(graphId: string): void {
this.selectedGraphId = graphId;
this.fetchGraphData(graphId);
}
handleNewChat(): void {
this.chatHistory = [];
this.currentNode = null;
this.selectedGraphId = null;
if (this.chatPanel) {
this.chatPanel.updateHistory(this.chatHistory);
}
}
/**
* Обрабатывает выбор графа из `GraphView`. Устанавливает выбранный граф и обновляет
* историю чата, загружая сообщения, относящиеся к этому графу.
* @param {{ graphId: string; currentNode: string | null }} data - Объект, содержащий ID выбранного графа и текущий узел.
* @returns {Promise<void>}
*/
async handleGraphSelected(data: { graphId: string; currentNode: string | null }): Promise<void> {
this.selectedGraphId = data.graphId;
this.currentNode = data.currentNode;
if (this.currentNode) {
await this.fetchMessagesFromRootToNode(this.currentNode);
} else {
// Если текущего узла нет (например, пустой граф), очищаем историю
this.chatHistory = [];
if (this.chatPanel) {
this.chatPanel.updateHistory(this.chatHistory);
}
}
}
}

View File

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

File diff suppressed because one or more lines are too long

View File

@ -1,24 +1,27 @@
{
"compilerOptions": {
"baseUrl": ".",
"inlineSourceMap": true,
"inlineSources": true,
"module": "ESNext",
"target": "ES6",
"allowJs": true,
"noImplicitAny": true,
"moduleResolution": "node",
"importHelpers": true,
"isolatedModules": true,
"strictNullChecks": true,
"lib": [
"DOM",
"ES5",
"ES6",
"ES7"
]
},
"include": [
"**/*.ts"
]
"compilerOptions": {
"baseUrl": ".",
"inlineSourceMap": true,
"inlineSources": true,
"module": "ESNext",
"target": "ES2019", // Обновляем таргет до ES2019
"allowJs": true,
"noImplicitAny": true,
"moduleResolution": "node",
"importHelpers": true,
"isolatedModules": true,
"strictNullChecks": true,
"lib": [
"DOM",
"ESNext" // Используем ESNext для более полного набора API
],
"jsx": "react-jsx", // Включаем поддержку JSX
"esModuleInterop": true, // Включаем для лучшей совместимости импортов
"skipLibCheck": true, // Пропускаем проверку типов в библиотеках
"forceConsistentCasingInFileNames": true // Обеспечиваем единообразие в именах файлов
},
"include": [
"**/*.ts",
"**/*.tsx" // Добавляем поддержку TSX файлов
]
}