klg-asutk-app/components/KnowledgeGraphVisualization.tsx
Yuriy a7da43be0e apply recommendations: security, get_db, exceptions, eslint, api-client
- session: set_tenant use bound param (SQL injection fix)
- health: text('SELECT 1'), REDIS_URL from config
- deps: re-export get_db from session, use settings.ENABLE_DEV_AUTH (default False)
- routes: all get_db from app.api.deps; conftest overrides deps.get_db
- main: register exception handlers from app.api.exceptions
- next.config: enable ESLint and TypeScript checks
- .eslintrc: drop @typescript-eslint/recommended; fix no-console (logger, ws-client, regulations)
- backend/.env.example added
- frontend: export apiFetch; dashboard, profile, settings, risks use api-client
- docs/ANALYSIS_AND_RECOMMENDATIONS.md

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-02-14 21:48:58 +03:00

259 lines
7.8 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

'use client';
import { useEffect, useRef, useState } from 'react';
import { logInfo, logError } from '@/lib/logger-client';
interface GraphNode {
id: string;
label: string;
type: string;
[key: string]: any;
}
interface GraphEdge {
id: string;
source: string;
target: string;
type: string;
weight: number;
}
interface KnowledgeGraphVisualizationProps {
query?: string;
onNodeClick?: (nodeId: string) => void;
}
export default function KnowledgeGraphVisualization({
query,
onNodeClick,
}: KnowledgeGraphVisualizationProps) {
const containerRef = useRef<HTMLDivElement>(null);
const [graph, setGraph] = useState<{ nodes: GraphNode[]; edges: GraphEdge[] } | null>(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
useEffect(() => {
let visNetwork: any = null;
const loadGraph = async () => {
try {
setLoading(true);
setError(null);
const url = query
? `/api/knowledge/graph?query=${encodeURIComponent(query)}&format=visualization`
: '/api/knowledge/graph?format=visualization';
const response = await fetch(url);
if (!response.ok) {
throw new Error(`HTTP ${response.status}`);
}
const data = await response.json();
setGraph(data);
// Динамически загружаем vis-network и vis-data
let Network: any = null;
let DataSet: any = null;
try {
// Импортируем vis-network для Network
const visNetwork = await import('vis-network');
Network = visNetwork.Network || visNetwork.default?.Network || visNetwork.default || visNetwork;
// Импортируем vis-data для DataSet
const visData = await import('vis-data');
DataSet = visData.DataSet || visData.default?.DataSet || visData.default || visData;
// Если не получилось через ESM, пробуем через require (для SSR)
if (!Network || !DataSet) {
if (typeof window === 'undefined') {
const visNetworkReq = require('vis-network');
Network = visNetworkReq.Network || visNetworkReq.default?.Network || visNetworkReq;
const visDataReq = require('vis-data');
DataSet = visDataReq.DataSet || visDataReq.default?.DataSet || visDataReq;
}
}
} catch (err) {
logError('vis-network/vis-data import error', err);
setError(`Ошибка загрузки библиотек визуализации: ${err instanceof Error ? err.message : 'Неизвестная ошибка'}`);
setLoading(false);
return;
}
if (!Network) {
logError('vis-network Network not found');
setError('vis-network Network не найден');
setLoading(false);
return;
}
if (!DataSet) {
logError('vis-data DataSet not found');
setError('vis-data DataSet не найден. Установите: npm install vis-data');
setLoading(false);
return;
}
if (!containerRef.current) {
return;
}
// Подготовка данных для vis-network
const nodes = new DataSet(
data.nodes.map((node: GraphNode) => {
// Создаем новый объект без id, чтобы избежать дублирования
const { id, label, type, ...restNode } = node;
return {
id,
label: label || id,
group: type,
title: `${type}: ${label}\n${JSON.stringify(node, null, 2)}`,
...restNode,
};
})
);
const edges = new DataSet(
data.edges.map((edge: GraphEdge) => ({
id: edge.id,
from: edge.source,
to: edge.target,
label: edge.type,
value: edge.weight,
title: `${edge.type} (weight: ${edge.weight})`,
}))
);
const networkData = { nodes, edges };
const options = {
nodes: {
shape: 'dot',
size: 16,
font: {
size: 12,
color: '#333',
},
borderWidth: 2,
shadow: true,
},
edges: {
width: 2,
color: { color: '#848484' },
smooth: {
type: 'continuous',
},
arrows: {
to: {
enabled: true,
scaleFactor: 0.5,
},
},
font: {
size: 10,
align: 'middle',
},
},
physics: {
enabled: true,
stabilization: {
iterations: 200,
},
},
interaction: {
hover: true,
tooltipDelay: 100,
zoomView: true,
dragView: true,
},
groups: {
aircraft: { color: { background: '#2196f3', border: '#1976d2' } },
audit: { color: { background: '#ff9800', border: '#f57c00' } },
risk: { color: { background: '#f44336', border: '#d32f2f' } },
operator: { color: { background: '#4caf50', border: '#388e3c' } },
regulation: { color: { background: '#9c27b0', border: '#7b1fa2' } },
document: { color: { background: '#00bcd4', border: '#0097a7' } },
},
};
visNetwork = new Network(containerRef.current, networkData, options);
// Обработка клика на узел
visNetwork.on('click', (params: any) => {
if (params.nodes.length > 0 && onNodeClick) {
onNodeClick(params.nodes[0]);
}
});
logInfo('Knowledge graph visualization loaded', {
nodes: data.nodes.length,
edges: data.edges.length,
});
} catch (err: any) {
logError('Failed to load knowledge graph visualization', err);
setError(err.message || 'Ошибка загрузки графа знаний');
} finally {
setLoading(false);
}
};
loadGraph();
return () => {
if (visNetwork) {
visNetwork.destroy();
}
};
}, [query, onNodeClick]);
if (loading) {
return (
<div style={{ textAlign: 'center', padding: '40px' }}>
<div style={{ fontSize: '16px', color: '#666' }}>Загрузка графа знаний...</div>
</div>
);
}
if (error) {
return (
<div style={{ textAlign: 'center', padding: '40px' }}>
<div style={{ fontSize: '16px', color: '#f44336', marginBottom: '8px' }}>
Ошибка: {error}
</div>
<button
onClick={() => window.location.reload()}
style={{
padding: '8px 16px',
backgroundColor: '#2196f3',
color: 'white',
border: 'none',
borderRadius: '4px',
cursor: 'pointer',
}}
>
Перезагрузить
</button>
</div>
);
}
if (!graph || graph.nodes.length === 0) {
return (
<div style={{ textAlign: 'center', padding: '40px' }}>
<div style={{ fontSize: '16px', color: '#666' }}>Граф знаний пуст</div>
</div>
);
}
return (
<div style={{ width: '100%', height: '600px', border: '1px solid #e0e0e0', borderRadius: '8px' }}>
<div ref={containerRef} style={{ width: '100%', height: '100%' }} />
<div style={{ padding: '8px', fontSize: '12px', color: '#666', backgroundColor: '#f5f5f5' }}>
Узлов: {graph.nodes.length} | Связей: {graph.edges.length}
</div>
</div>
);
}