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 esbuild from 'esbuild'
import process from "process"; import process from 'process'
import builtins from "builtin-modules"; 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 THIS IS A GENERATED/BUNDLED FILE BY ESBUILD
if you want to view the source, please visit the github repository of this plugin 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({ const context = await esbuild.context({
banner: { banner: {
js: banner, js: banner
}, },
entryPoints: ["main.ts"], entryPoints: ['main.ts'],
bundle: true, bundle: true,
external: [ external: [
"obsidian", 'obsidian',
"electron", 'electron',
"@codemirror/autocomplete", '@codemirror/autocomplete',
"@codemirror/collab", '@codemirror/collab',
"@codemirror/commands", '@codemirror/commands',
"@codemirror/language", '@codemirror/language',
"@codemirror/lint", '@codemirror/lint',
"@codemirror/search", '@codemirror/search',
"@codemirror/state", '@codemirror/state',
"@codemirror/view", '@codemirror/view',
"@lezer/common", '@lezer/common',
"@lezer/highlight", '@lezer/highlight',
"@lezer/lr", '@lezer/lr',
...builtins], ...builtins
format: "cjs", ],
target: "es2018", format: 'cjs',
logLevel: "info", target: 'es2019', // Обновляем таргет до es2019
sourcemap: prod ? false : "inline", logLevel: 'info',
sourcemap: prod ? false : 'inline',
treeShaking: true, treeShaking: true,
outfile: "main.js", outfile: 'main.js',
minify: prod, minify: prod,
}); // ДОБАВЛЕНО: Загрузчик для CSS файлов
loader: {
'.css': 'css'
},
plugins: [renameCssPlugin()]
})
if (prod) { if (prod) {
await context.rebuild(); await context.rebuild()
process.exit(0); process.exit(0)
} else { } 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 { GraphView, GRAPH_VIEW_TYPE } from './src/views/GraphView';
import { ChatView, CHAT_VIEW_TYPE } from './src/views/ChatView'; import { ChatView, CHAT_VIEW_TYPE } from './src/views/ChatView';
import 'src/css/styles.css'; // Путь к твоему новому CSS файлу
// ----------------------------------------------- Settings --------------------------------------------------------------- // ----------------------------------------------- Settings ---------------------------------------------------------------
interface LLMAgentSettings { 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", "version": "1.0.0",
"license": "MIT", "license": "MIT",
"devDependencies": { "devDependencies": {
"@types/dagre": "^0.7.52",
"@types/node": "^16.11.6", "@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/eslint-plugin": "5.29.0",
"@typescript-eslint/parser": "5.29.0", "@typescript-eslint/parser": "5.29.0",
"builtin-modules": "3.3.0", "builtin-modules": "3.3.0",
"dagre": "^0.8.5",
"esbuild": "0.17.3", "esbuild": "0.17.3",
"obsidian": "latest", "obsidian": "latest",
"react": "^18.3.1",
"react-dom": "^18.3.1",
"reactflow": "^11.11.3",
"tslib": "^2.4.0", "tslib": "^2.4.0",
"typescript": "4.7.4" "typescript": "^5.4.5"
} }
}, },
"node_modules/@codemirror/state": { "node_modules/@codemirror/state": {
@ -537,6 +544,108 @@
"node": ">= 8" "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": { "node_modules/@types/codemirror": {
"version": "5.60.8", "version": "5.60.8",
"resolved": "https://registry.npmjs.org/@types/codemirror/-/codemirror-5.60.8.tgz", "resolved": "https://registry.npmjs.org/@types/codemirror/-/codemirror-5.60.8.tgz",
@ -546,12 +655,277 @@
"@types/tern": "*" "@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": { "node_modules/@types/estree": {
"version": "1.0.8", "version": "1.0.8",
"resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz",
"integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==",
"dev": true "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": { "node_modules/@types/json-schema": {
"version": "7.0.15", "version": "7.0.15",
"resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz",
@ -564,6 +938,31 @@
"integrity": "sha512-OTcgaiwfGFBKacvfwuHzzn1KLxH/er8mluiy8/uM3sGXHaRe73RrSIj01jow9t4kJEW633Ov+cOexXeiApTyAw==", "integrity": "sha512-OTcgaiwfGFBKacvfwuHzzn1KLxH/er8mluiy8/uM3sGXHaRe73RrSIj01jow9t4kJEW633Ov+cOexXeiApTyAw==",
"dev": true "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": { "node_modules/@types/tern": {
"version": "0.23.9", "version": "0.23.9",
"resolved": "https://registry.npmjs.org/@types/tern/-/tern-0.23.9.tgz", "resolved": "https://registry.npmjs.org/@types/tern/-/tern-0.23.9.tgz",
@ -915,6 +1314,12 @@
"url": "https://github.com/chalk/chalk?sponsor=1" "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": { "node_modules/color-convert": {
"version": "2.0.1", "version": "2.0.1",
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
@ -964,6 +1369,127 @@
"node": ">= 8" "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": { "node_modules/debug": {
"version": "4.4.1", "version": "4.4.1",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz", "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz",
@ -1493,6 +2019,15 @@
"dev": true, "dev": true,
"peer": 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": { "node_modules/has-flag": {
"version": "4.0.0", "version": "4.0.0",
"resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
@ -1605,6 +2140,12 @@
"dev": true, "dev": true,
"peer": 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": { "node_modules/js-yaml": {
"version": "4.1.0", "version": "4.1.0",
"resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz",
@ -1679,6 +2220,12 @@
"url": "https://github.com/sponsors/sindresorhus" "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": { "node_modules/lodash.merge": {
"version": "4.6.2", "version": "4.6.2",
"resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz",
@ -1686,6 +2233,18 @@
"dev": true, "dev": true,
"peer": 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": { "node_modules/merge2": {
"version": "1.4.1", "version": "1.4.1",
"resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", "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": { "node_modules/regexpp": {
"version": "3.2.0", "version": "3.2.0",
"resolved": "https://registry.npmjs.org/regexpp/-/regexpp-3.2.0.tgz", "resolved": "https://registry.npmjs.org/regexpp/-/regexpp-3.2.0.tgz",
@ -1993,6 +2595,15 @@
"queue-microtask": "^1.2.2" "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": { "node_modules/semver": {
"version": "7.7.2", "version": "7.7.2",
"resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz", "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz",
@ -2156,16 +2767,16 @@
} }
}, },
"node_modules/typescript": { "node_modules/typescript": {
"version": "4.7.4", "version": "5.9.2",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-4.7.4.tgz", "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.2.tgz",
"integrity": "sha512-C0WQT0gezHuw6AdY1M2jxUO83Rjf0HP7Sk1DtXj6j1EwkQNZrHAg2XPWlq62oqEhYvONq5pkC2Y9oPljWToLmQ==", "integrity": "sha512-CWBzXQrc/qOkhidw1OzBTQuYRbfyxDXJMVJ1XNwUHGROVmuaeiEm3OslpZ1RV96d7SKKjZKrSJu3+t/xlw3R9A==",
"dev": true, "dev": true,
"bin": { "bin": {
"tsc": "bin/tsc", "tsc": "bin/tsc",
"tsserver": "bin/tsserver" "tsserver": "bin/tsserver"
}, },
"engines": { "engines": {
"node": ">=4.2.0" "node": ">=14.17"
} }
}, },
"node_modules/uri-js": { "node_modules/uri-js": {
@ -2178,6 +2789,15 @@
"punycode": "^2.1.0" "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": { "node_modules/w3c-keyname": {
"version": "2.2.8", "version": "2.2.8",
"resolved": "https://registry.npmjs.org/w3c-keyname/-/w3c-keyname-2.2.8.tgz", "resolved": "https://registry.npmjs.org/w3c-keyname/-/w3c-keyname-2.2.8.tgz",
@ -2230,6 +2850,34 @@
"funding": { "funding": {
"url": "https://github.com/sponsors/sindresorhus" "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)", "description": "This is a sample plugin for Obsidian (https://obsidian.md)",
"main": "main.js", "main": "main.js",
"scripts": { "scripts": {
"dev": "node esbuild.config.mjs", "dev": "node esbuild.config.mjs && move main.css styles.css",
"build": "tsc -noEmit -skipLibCheck && node esbuild.config.mjs production", "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" "version": "node version-bump.mjs && git add manifest.json versions.json"
}, },
"keywords": [], "keywords": [],
"author": "", "author": "",
"license": "MIT", "license": "MIT",
"devDependencies": { "devDependencies": {
"@types/dagre": "^0.7.52",
"@types/node": "^16.11.6", "@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/eslint-plugin": "5.29.0",
"@typescript-eslint/parser": "5.29.0", "@typescript-eslint/parser": "5.29.0",
"builtin-modules": "3.3.0", "builtin-modules": "3.3.0",
"dagre": "^0.8.5",
"esbuild": "0.17.3", "esbuild": "0.17.3",
"obsidian": "latest", "obsidian": "latest",
"react": "^18.3.1",
"react-dom": "^18.3.1",
"reactflow": "^11.11.3",
"tslib": "^2.4.0", "tslib": "^2.4.0",
"typescript": "4.7.4" "typescript": "^5.4.5"
} }
} }

View File

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

View File

@ -1,138 +1,302 @@
export class GraphPanel { // ----------------------------------------------- React & React Flow Imports ---------------------------------------------------
container: HTMLElement; import * as React from 'react';
props: { import * as ReactDOM from 'react-dom/client'; // <-- ИСПРАВЛЕНО: для createRoot нужен 'react-dom/client'
graphData: { nodes: any[]; edges: any[] }; import ReactFlow, {
onNodeClick: (nodeId: string) => void; addEdge,
}; useNodesState,
nodes: any[]; useEdgesState,
edges: any[]; Background,
Controls,
ReactFlowProvider, // <-- ИСПРАВЛЕНО: импортируем прямо ReactFlowProvider без переименования
Connection,
Edge,
BackgroundVariant // <-- ИСПРАВЛЕНО: используем BackgroundVariant для типизации
} from 'reactflow';
constructor( // <-- ИСПРАВЛЕНО: Это ключевой момент. Убедитесь, что esbuild настроен на обработку CSS импортов.
container: HTMLElement, // Обычно, при использовании esbuild для React-проектов, вы импортируете CSS прямо в JS/TS файл.
props: { // esbuild должен быть настроен для копирования или встраивания этого CSS.
graphData: { nodes: any[]; edges: any[] }; // Если стили не применяются, проверьте конфигурацию esbuild (например, через плагин esbuild-plugin-css-bundle).
onNodeClick: (nodeId: string) => void; import 'reactflow/dist/style.css'; // Импортируем стили ReactFlow
}
) {
this.container = container;
this.props = props;
this.nodes = [];
this.edges = [];
this.init(); // Если вы используете какие-либо кастомные ноды, импортируйте их здесь
} // import CustomNode from './CustomNode';
// const nodeTypes = { custom: CustomNode };
init(): void { // ----------------------------------------------- Custom Nodes -----------------------------------------------------------------
// Создаем структуру для React Flow
this.container.innerHTML = `
<div class="llm-agent-graph-panel">
<div id="reactflow-container" style="width: 100%; height: 100%;"></div>
</div>
`;
// Загружаем React Flow динамически (если доступен) import { Handle, Position } from 'reactflow';
this.initReactFlow();
}
async initReactFlow(): Promise<void> { // Простой компонент для узла типа 'user'
try { // Это базовый пример. Ты можешь стилизовать его по своему усмотрению.
// В среде Obsidian нужно будет подключить React Flow через CDN или bundler const UserNode: React.FC<any> = ({ data, isConnectable }) => {
// Для демонстрации создаем упрощенную визуализацию return React.createElement('div', {
this.createSimpleGraph(); className: 'react-flow__node-user'
} catch (error) { },
console.error('Ошибка инициализации React Flow:', error); React.createElement(Handle, {
this.createFallbackGraph(); type: "source",
} position: Position.Bottom,
} isConnectable: isConnectable,
}),
React.createElement(Handle, {
type: "target",
position: Position.Top,
isConnectable: isConnectable,
}),
data.label
);
};
createSimpleGraph(): void { // Простой компонент для узла типа 'llm'
const container = this.container.querySelector('#reactflow-container') as HTMLElement; const LLMNode: React.FC<any> = ({ data, isConnectable }) => {
container.innerHTML = ` return React.createElement('div', {
<div class="simple-graph"> className: 'react-flow__node-llm'
<div class="graph-nodes" id="graph-nodes"></div> },
<svg class="graph-edges" id="graph-edges"></svg> React.createElement(Handle, {
</div> type: "source",
`; position: Position.Bottom,
isConnectable: isConnectable,
}),
React.createElement(Handle, {
type: "target",
position: Position.Top,
isConnectable: isConnectable,
}),
data.label
);
};
this.renderNodes(); // Простой компонент для узла типа 'tool'
this.renderEdges(); 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 { // Простой компонент для узла типа 'error'
const nodesContainer = this.container.querySelector('#graph-nodes') as HTMLElement; const ErrorNode: React.FC<any> = ({ data, isConnectable }) => {
nodesContainer.innerHTML = ''; 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 AssistantNode: React.FC<any> = ({ data, isConnectable }) => {
const nodeEl = document.createElement('div'); return React.createElement('div', {
nodeEl.className = `graph-node node-${node.type || 'default'}`; className: 'react-flow__node-assistant' // Используем CSS класс для узла assistant
nodeEl.style.cssText = ` },
position: absolute; React.createElement(Handle, {
left: ${50 + index * 150}px; type: "source",
top: ${50 + Math.floor(index / 5) * 80}px; position: Position.Bottom,
padding: 8px 12px; isConnectable: isConnectable,
border: 2px solid #333; }),
border-radius: 4px; React.createElement(Handle, {
background: #fff; type: "target",
cursor: pointer; position: Position.Top,
max-width: 120px; isConnectable: isConnectable,
word-wrap: break-word; }),
`; data.label
nodeEl.textContent = node.data?.label || `Node ${node.id}`; );
nodeEl.dataset.nodeId = node.id; };
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 { // Типы для Dagre (были уже корректны)
const edgesContainer = this.container.querySelector('#graph-edges') as HTMLElement; interface DagreNode {
edgesContainer.innerHTML = ''; x: number;
// Упрощенная отрисовка связей - в реальной реализации нужно будет вычислять позиции y: number;
} width: number;
height: number;
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 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 { export class HistoryPanel {
container: HTMLElement; container: HTMLElement;
props: { props: {

View File

@ -1,4 +1,10 @@
/*
* Определяет интерфейс для элемента истории чата, включающего роль участника
* (пользователь/агент) и содержимое сообщения.
*/
export interface ChatHistoryItem { export interface ChatHistoryItem {
role: string; role: string;
content: 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 { export class EventBus {
private events: { [key: string]: ((data?: any) => void)[] } = {}; private events: { [key: string]: ((data?: any) => void)[] } = {};

View File

@ -1,6 +1,10 @@
/*
* ChatView управляет пользовательским интерфейсом чата, отображает историю сообщений,
* отправляет запросы LLM-агенту и обрабатывает ответы, обновляя состояние чата и графа.
*/
import { ItemView, WorkspaceLeaf } from 'obsidian'; import { ItemView, WorkspaceLeaf } from 'obsidian';
import { ChatPanel } from '../components/ChatPanel'; import { ChatPanel } from '../components/ChatPanel';
import { HistoryPanel } from '../components/HistoryPanel';
import LLMAgentPlugin from 'main'; import LLMAgentPlugin from 'main';
export const CHAT_VIEW_TYPE = 'llm-agent-chat-view'; 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 { export class ChatView extends ItemView {
plugin: LLMAgentPlugin; plugin: LLMAgentPlugin;
chatHistory: any[]; chatHistory: any[];
graphsList: any[];
selectedGraphId: string | null; selectedGraphId: string | null;
currentNode: string | null; currentNode: string | null;
chatContainer: HTMLDivElement; chatContainer: HTMLDivElement;
historyContainer: HTMLDivElement;
chatPanel: ChatPanel | null; chatPanel: ChatPanel | null;
historyPanel: HistoryPanel | null;
constructor(leaf: WorkspaceLeaf, plugin: LLMAgentPlugin) { constructor(leaf: WorkspaceLeaf, plugin: LLMAgentPlugin) {
super(leaf); super(leaf);
this.plugin = plugin; this.plugin = plugin;
this.chatHistory = []; this.chatHistory = [];
this.graphsList = [];
this.selectedGraphId = null; this.selectedGraphId = null;
this.currentNode = null; this.currentNode = null;
this.chatPanel = null; this.chatPanel = null;
this.historyPanel = null;
} }
getViewType(): string { getViewType(): string {
@ -45,31 +44,20 @@ export class ChatView extends ItemView {
const container = this.containerEl.children[1] as HTMLElement; const container = this.containerEl.children[1] as HTMLElement;
container.empty(); container.empty();
// Создаем контейнеры для React компонентов // Создаем только контейнер для чата (без истории)
this.chatContainer = container.createDiv() as HTMLDivElement; this.chatContainer = container.createDiv() as HTMLDivElement;
this.chatContainer.addClass('llm-agent-chat-container'); 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, { this.chatPanel = new ChatPanel(this.chatContainer, {
chatHistory: this.chatHistory, chatHistory: this.chatHistory,
onSendMessage: this.handleSendMessage.bind(this) 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('node-selected', this.handleNodeSelected.bind(this));
this.plugin.eventBus.on('new-chat', this.handleNewChat.bind(this)); this.plugin.eventBus.on('new-chat', this.handleNewChat.bind(this));
this.plugin.eventBus.on('graph-selected', this.handleGraphSelected.bind(this));
// Загружаем список графов
this.fetchGraphsList();
} }
async onClose(): Promise<void> { async onClose(): Promise<void> {
@ -77,145 +65,143 @@ export class ChatView extends ItemView {
this.chatPanel.destroy(); this.chatPanel.destroy();
this.chatPanel = null; this.chatPanel = null;
} }
if (this.historyPanel) {
this.historyPanel.destroy();
this.historyPanel = null;
}
this.plugin.eventBus.off('node-selected', this.handleNodeSelected); this.plugin.eventBus.off('node-selected', this.handleNodeSelected);
this.plugin.eventBus.off('new-chat', this.handleNewChat); this.plugin.eventBus.off('new-chat', this.handleNewChat);
this.plugin.eventBus.off('graph-selected', this.handleGraphSelected);
} }
// ----------------------------------------------- API Methods --------------------------------------------------------------- // ----------------------------------------------- API Methods ---------------------------------------------------------------
async fetchGraphsList(): Promise<void> { /**
try { * Загружает историю сообщений от корня до указанного узла графа.
const response = await fetch(`${this.plugin.settings.apiBaseUrl}/graphs`); * Обновляет `chatHistory` и отображение в `chatPanel`.
const data = await response.json(); * @param {string} nodeId - ID конечного узла для загрузки истории.
this.graphsList = data; * @returns {Promise<void>}
if (this.historyPanel) { */
this.historyPanel.updateGraphs(this.graphsList); async fetchMessagesFromRootToNode(nodeId: string): Promise<void> {
} try {
} catch (error) { // Убедимся, что selectedGraphId установлен
console.error('Ошибка загрузки списка графов:', error); if (!this.selectedGraphId) {
} console.warn('selectedGraphId не установлен. Невозможно загрузить сообщения.');
} return;
}
async fetchGraphData(graphId: string): Promise<void> { const response = await fetch(`${this.plugin.settings.apiBaseUrl}/messages/${this.selectedGraphId}/${nodeId}`);
try { if (!response.ok) {
const response = await fetch(`${this.plugin.settings.apiBaseUrl}/graphs/${graphId}`); const errorText = await response.text();
if (!response.ok) { throw new Error(`Ошибка загрузки сообщений: ${response.status} - ${errorText}`);
if (response.status === 404) { }
console.error(`Граф с ID ${graphId} не найден.`); const data = await response.json();
} // ИСПРАВЛЕНО: Убедиться, что type передается, если это возможно
throw new Error(`HTTP ошибка! статус: ${response.status}`); this.chatHistory = data.map((msg: any) => ({
} ...msg,
const data = await response.json(); type: msg.type || msg.role // Используем type, если есть, иначе role как fallback
})) || [];
// Обновляем граф if (this.chatPanel) {
this.plugin.eventBus.emit('graph-updated', { this.chatPanel.updateHistory(this.chatHistory);
graphData: { }
nodes: data.graph_nodes || [], } catch (error) {
edges: data.graph_edges || [] console.error('Ошибка загрузки сообщений:', error);
}, // Optionally: show a notice to the user
currentNode: data.current_node_id // new Notice(`Ошибка загрузки сообщений: ${error.message}`);
}); }
}
// Обновляем историю чата
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);
}
}
// ----------------------------------------------- Event Handlers --------------------------------------------------------------- // ----------------------------------------------- Event Handlers ---------------------------------------------------------------
async handleSendMessage(message: string): Promise<void> { async handleSendMessage(message: string): Promise<void> {
try { try {
const response = await fetch(`${this.plugin.settings.apiBaseUrl}/chat`, { const response = await fetch(`${this.plugin.settings.apiBaseUrl}/chat`, {
method: 'POST', method: 'POST',
headers: { headers: {
'Content-Type': 'application/json' 'Content-Type': 'application/json',
}, },
body: JSON.stringify({ body: JSON.stringify({
message: message, message: message,
graph_id: this.selectedGraphId, graph_id: this.selectedGraphId,
parent_node_id: this.currentNode parent_node_id: this.currentNode,
}) }),
}); });
if (!response.ok) { if (!response.ok) {
throw new Error(`HTTP ошибка! статус: ${response.status}`); throw new Error(`HTTP ошибка! статус: ${response.status}`);
} }
const result = await response.json(); const result = await response.json();
// Обновляем выбранный ID графа this.selectedGraphId = result.graph_id;
this.selectedGraphId = result.graph_id; const newCurrentNodeId = result.graph_visualization_data.current_node_id;
this.currentNode = newCurrentNodeId;
// Устанавливаем текущий узел // Обновляем граф
const newCurrentNodeId = result.graph_visualization_data.current_node_id; this.plugin.eventBus.emit('graph-updated', {
this.currentNode = newCurrentNodeId; graphData: {
nodes: result.graph_visualization_data.nodes || [],
edges: result.graph_visualization_data.edges || []
},
currentNode: newCurrentNodeId
});
// Обновляем граф // Загружаем сообщения
this.plugin.eventBus.emit('graph-updated', { if (newCurrentNodeId) {
graphData: { await this.fetchMessagesFromRootToNode(newCurrentNodeId);
nodes: result.graph_visualization_data.nodes || [], } else {
edges: result.graph_visualization_data.edges || [] // ИСПРАВЛЕНО: Убедиться, что type передается, если это возможно
}, this.chatHistory = result.messages.map((msg: any) => ({
currentNode: newCurrentNodeId ...msg,
}); type: msg.type || msg.role // Используем type, если есть, иначе role как fallback
})) || [];
if (this.chatPanel) {
this.chatPanel.updateHistory(this.chatHistory);
}
}
// Загружаем сообщения от корня до нового текущего узла } catch (error) {
if (newCurrentNodeId) { console.error('Ошибка отправки сообщения:', error);
await this.fetchMessagesFromRootToNode(newCurrentNodeId); }
} else { }
this.chatHistory = result.messages || [];
if (this.chatPanel) {
this.chatPanel.updateHistory(this.chatHistory);
}
}
// Обновляем список графов /**
this.fetchGraphsList(); * Обрабатывает выбор узла на графе. Загружает историю сообщений до выбранного узла
} catch (error) { * и обновляет панель чата.
console.error('Ошибка отправки сообщения:', error); * @param {{ nodeId: string }} data - Объект, содержащий ID выбранного узла.
} */
} handleNodeSelected(data: { nodeId: string }): void {
this.currentNode = data.nodeId;
if (this.selectedGraphId) {
this.fetchMessagesFromRootToNode(data.nodeId);
}
}
handleNodeSelected(data: { nodeId: string }): void { /**
this.currentNode = data.nodeId; * Обрабатывает событие начала нового чата. Сбрасывает текущее состояние чата
if (this.selectedGraphId) { * и очищает отображение истории.
this.fetchMessagesFromRootToNode(data.nodeId); */
} handleNewChat(): void {
} this.chatHistory = [];
this.currentNode = null;
this.selectedGraphId = null;
if (this.chatPanel) {
this.chatPanel.updateHistory(this.chatHistory);
}
}
handleGraphSelect(graphId: string): void { /**
this.selectedGraphId = graphId; * Обрабатывает выбор графа из `GraphView`. Устанавливает выбранный граф и обновляет
this.fetchGraphData(graphId); * историю чата, загружая сообщения, относящиеся к этому графу.
} * @param {{ graphId: string; currentNode: string | null }} data - Объект, содержащий ID выбранного графа и текущий узел.
* @returns {Promise<void>}
handleNewChat(): void { */
this.chatHistory = []; async handleGraphSelected(data: { graphId: string; currentNode: string | null }): Promise<void> {
this.currentNode = null; this.selectedGraphId = data.graphId;
this.selectedGraphId = null; this.currentNode = data.currentNode;
if (this.chatPanel) { if (this.currentNode) {
this.chatPanel.updateHistory(this.chatHistory); 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 { ItemView, WorkspaceLeaf } from 'obsidian';
import { GraphPanel } from '../components/GraphPanel'; // ----------------------------------------------- Plugin Imports ------------------------------------------------------------
import LLMAgentPlugin from 'main'; 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'; export const GRAPH_VIEW_TYPE = 'llm-agent-graph-view';
// ----------------------------------------------- Graph View --------------------------------------------------------------- // ----------------------------------------------- Graph View ---------------------------------------------------------------
export class GraphView extends ItemView { export class GraphView extends ItemView {
plugin: LLMAgentPlugin; plugin: LLMAgentPlugin;
graphData: { nodes: any[]; edges: any[] }; graphData: { nodes: any[], edges: any[] };
currentNode: string | null; currentNode: string | null;
graphPanel: GraphPanel | null; graphsList: any[];
reactContainer: HTMLDivElement; selectedGraphId: string | null;
constructor(leaf: WorkspaceLeaf, plugin: LLMAgentPlugin) { // UI Elements
super(leaf); historyPanel: HistoryPanel | null;
this.plugin = plugin; historyContainer: HTMLDivElement;
this.graphData = { nodes: [], edges: [] }; graphContainer: HTMLDivElement;
this.currentNode = null; reactRoot: ReactDOM.Root | null; // <-- ИСПРАВЛЕНО: Указываем тип для reactRoot
this.graphPanel = null;
}
getViewType(): string { constructor(leaf: WorkspaceLeaf, plugin: LLMAgentPlugin) {
return GRAPH_VIEW_TYPE; 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; * @returns {string} Иконка представления.
container.empty(); */
getIcon(): string {
return 'brain-circuit';
}
// Создаем контейнер для React компонента /**
this.reactContainer = container.createDiv() as HTMLDivElement; * Вызывается при открытии представления. Инициализирует UI и загружает данные.
this.reactContainer.addClass('llm-agent-graph-container'); * @returns {Promise<void>}
*/
async onOpen(): Promise<void> {
const container = this.containerEl.children[1] as HTMLElement;
container.empty();
// Инициализируем React компонент // Создаем основной контейнер с flexbox layout
this.graphPanel = new GraphPanel(this.reactContainer, { const mainContainer = container.createDiv('llm-agent-graph-main');
graphData: this.graphData, mainContainer.style.cssText = `
onNodeClick: this.handleNodeClick.bind(this) display: flex;
}); height: 100%;
width: 100%;
background-color: var(--background-primary); /* Убедимся, что фон установлен */
`;
// Подписываемся на события // Левая панель для истории
this.plugin.eventBus.on('graph-updated', this.handleGraphUpdate.bind(this)); this.historyContainer = mainContainer.createDiv('llm-agent-history-sidebar') as HTMLDivElement;
this.plugin.eventBus.on('new-chat', this.handleNewChat.bind(this)); 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.graphContainer = mainContainer.createDiv('llm-agent-graph-container') as HTMLDivElement;
this.graphPanel.destroy(); this.graphContainer.style.cssText = `
this.graphPanel = null; flex: 1;
} position: relative;
this.plugin.eventBus.off('graph-updated', this.handleGraphUpdate); background-color: var(--background-primary); /* Убедимся, что фон установлен */
this.plugin.eventBus.off('new-chat', this.handleNewChat); `;
}
// ----------------------------------------------- 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 { // Инициализируем React Flow
this.currentNode = nodeId; this.initializeGraphPanel();
this.plugin.eventBus.emit('node-selected', { nodeId });
}
handleGraphUpdate(data: { graphData: { nodes: any[]; edges: any[] }; currentNode: string | null }): void { // Подписываемся на события
this.graphData = data.graphData; this.plugin.eventBus.on('graph-updated', this.handleGraphUpdate.bind(this));
this.currentNode = data.currentNode; this.plugin.eventBus.on('new-chat', this.handleNewChat.bind(this));
if (this.graphPanel) {
this.graphPanel.updateData(this.graphData);
}
}
handleNewChat(): void { // Загружаем список графов
this.graphData = { nodes: [], edges: [] }; await this.fetchGraphsList();
this.currentNode = null; }
if (this.graphPanel) {
this.graphPanel.updateData(this.graphData); /**
} * Вызывается при закрытии представления. Очищает ресурсы.
} * @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": { "compilerOptions": {
"baseUrl": ".", "baseUrl": ".",
"inlineSourceMap": true, "inlineSourceMap": true,
"inlineSources": true, "inlineSources": true,
"module": "ESNext", "module": "ESNext",
"target": "ES6", "target": "ES2019", // Обновляем таргет до ES2019
"allowJs": true, "allowJs": true,
"noImplicitAny": true, "noImplicitAny": true,
"moduleResolution": "node", "moduleResolution": "node",
"importHelpers": true, "importHelpers": true,
"isolatedModules": true, "isolatedModules": true,
"strictNullChecks": true, "strictNullChecks": true,
"lib": [ "lib": [
"DOM", "DOM",
"ES5", "ESNext" // Используем ESNext для более полного набора API
"ES6", ],
"ES7" "jsx": "react-jsx", // Включаем поддержку JSX
] "esModuleInterop": true, // Включаем для лучшей совместимости импортов
}, "skipLibCheck": true, // Пропускаем проверку типов в библиотеках
"include": [ "forceConsistentCasingInFileNames": true // Обеспечиваем единообразие в именах файлов
"**/*.ts" },
] "include": [
"**/*.ts",
"**/*.tsx" // Добавляем поддержку TSX файлов
]
} }