nullai-knowledge-system / frontend /src /components /KnowledgeSpace3D.tsx
kofdai's picture
Deploy NullAI Knowledge System to Spaces
075a2b6 verified
// src/components/KnowledgeSpace3D.tsx
import { useState, useEffect, useRef } from 'react';
import { Canvas, useFrame } from '@react-three/fiber';
import { OrbitControls, Text, Line } from '@react-three/drei';
import * as THREE from 'three';
import apiClient from '../services/api';
// Type definitions
interface KnowledgeTileData {
tile_id: string;
topic: string;
domain_id: string;
coordinates: number[]; // [x, y, z, c, g, v]
confidence_score: number;
verification_type: string;
}
interface CoordinatesResponse {
tiles: KnowledgeTileData[];
count: number;
domain_id: string;
}
interface DomainConfig {
domain_id: string;
name: string;
}
// Color mapping for verification types
const VERIFICATION_COLORS: Record<string, string> = {
'none': '#888888',
'community': '#4CAF50',
'expert': '#2196F3',
'multi_expert': '#9C27B0'
};
// Knowledge Point Component
interface KnowledgePointProps {
tile: KnowledgeTileData;
onClick: (tile: KnowledgeTileData) => void;
onHover: (tile: KnowledgeTileData | null) => void;
}
function KnowledgePoint({ tile, onClick, onHover }: KnowledgePointProps) {
const meshRef = useRef<THREE.Mesh>(null);
const [hovered, setHovered] = useState(false);
// Extract 3D coordinates (using first 3 dimensions)
const [x, y, z] = tile.coordinates;
// Convert from [0, 1] range to [-5, 5] range for better visualization
const position: [number, number, number] = [
(x - 0.5) * 10,
(y - 0.5) * 10,
(z - 0.5) * 10
];
// Animate on hover
useFrame(() => {
if (meshRef.current) {
const scale = hovered ? 1.5 : 1;
meshRef.current.scale.lerp(new THREE.Vector3(scale, scale, scale), 0.1);
}
});
const color = VERIFICATION_COLORS[tile.verification_type] || VERIFICATION_COLORS['none'];
return (
<mesh
ref={meshRef}
position={position}
onClick={() => onClick(tile)}
onPointerOver={() => {
setHovered(true);
onHover(tile);
}}
onPointerOut={() => {
setHovered(false);
onHover(null);
}}
>
<sphereGeometry args={[0.1, 16, 16]} />
<meshStandardMaterial
color={color}
emissive={color}
emissiveIntensity={hovered ? 0.5 : 0.2}
opacity={0.8}
transparent
/>
</mesh>
);
}
// Axis Labels Component
function AxisLabels() {
const axisLength = 6;
const labelOffset = 6.5;
return (
<>
{/* X Axis - Red */}
<Line
points={[[-axisLength, 0, 0], [axisLength, 0, 0]]}
color="red"
lineWidth={1}
/>
<Text
position={[labelOffset, 0, 0]}
fontSize={0.3}
color="red"
anchorX="center"
anchorY="middle"
>
X
</Text>
{/* Y Axis - Green */}
<Line
points={[[0, -axisLength, 0], [0, axisLength, 0]]}
color="green"
lineWidth={1}
/>
<Text
position={[0, labelOffset, 0]}
fontSize={0.3}
color="green"
anchorX="center"
anchorY="middle"
>
Y
</Text>
{/* Z Axis - Blue */}
<Line
points={[[0, 0, -axisLength], [0, 0, axisLength]]}
color="blue"
lineWidth={1}
/>
<Text
position={[0, 0, labelOffset]}
fontSize={0.3}
color="blue"
anchorX="center"
anchorY="middle"
>
Z
</Text>
</>
);
}
// Main Component
const KnowledgeSpace3D = () => {
const [tiles, setTiles] = useState<KnowledgeTileData[]>([]);
const [isLoading, setIsLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
const [selectedTile, setSelectedTile] = useState<KnowledgeTileData | null>(null);
const [hoveredTile, setHoveredTile] = useState<KnowledgeTileData | null>(null);
// Filter states
const [filterDomain, setFilterDomain] = useState('');
const [availableDomains, setAvailableDomains] = useState<DomainConfig[]>([]);
const [isExporting, setIsExporting] = useState(false);
// Fetch available domains
useEffect(() => {
const fetchDomains = async () => {
try {
const response = await apiClient.get('/config/domains');
setAvailableDomains(response.data);
} catch (err) {
console.error("Failed to fetch domains:", err);
}
};
fetchDomains();
}, []);
// Fetch coordinate data
useEffect(() => {
const fetchCoordinates = async () => {
setIsLoading(true);
setError(null);
try {
const response = await apiClient.get<CoordinatesResponse>('/knowledge/coordinates', {
params: {
domain_id: filterDomain || undefined
}
});
setTiles(response.data.tiles);
} catch (err) {
setError('Failed to fetch coordinate data.');
console.error(err);
} finally {
setIsLoading(false);
}
};
fetchCoordinates();
}, [filterDomain]);
const handleTileClick = (tile: KnowledgeTileData) => {
setSelectedTile(tile);
};
const handleTileHover = (tile: KnowledgeTileData | null) => {
setHoveredTile(tile);
};
const handleExportIath = async () => {
setIsExporting(true);
try {
const response = await apiClient.get('/knowledge/export/iath', {
params: {
domain_id: filterDomain || undefined,
precision: 'high_precision' // 3D View uses high precision
},
responseType: 'blob'
});
// Create download link
const url = window.URL.createObjectURL(new Blob([response.data]));
const link = document.createElement('a');
link.href = url;
// Extract filename from Content-Disposition header or use default
const contentDisposition = response.headers['content-disposition'];
let filename = 'knowledge_base_3d.iath';
if (contentDisposition) {
const filenameMatch = contentDisposition.match(/filename="?(.+)"?/);
if (filenameMatch) {
filename = filenameMatch[1];
}
}
link.setAttribute('download', filename);
document.body.appendChild(link);
link.click();
link.remove();
window.URL.revokeObjectURL(url);
} catch (err) {
console.error('Export failed:', err);
setError('Failed to export .iath file.');
} finally {
setIsExporting(false);
}
};
return (
<div style={{ width: '100%', height: '100%', position: 'relative' }}>
{/* Controls Panel */}
<div style={{
position: 'absolute',
top: 10,
left: 10,
zIndex: 10,
background: 'rgba(0, 0, 0, 0.7)',
padding: '10px',
borderRadius: '5px',
color: 'white'
}}>
<h3 style={{ margin: '0 0 10px 0' }}>Knowledge Space 3D</h3>
<div style={{ marginBottom: '10px' }}>
<label>Filter by Domain:</label>
<select
value={filterDomain}
onChange={(e) => setFilterDomain(e.target.value)}
style={{ width: '100%', padding: '5px', marginTop: '5px' }}
>
<option value="">All Domains</option>
{availableDomains.map(domain => (
<option key={domain.domain_id} value={domain.domain_id}>
{domain.name}
</option>
))}
</select>
</div>
<div style={{ fontSize: '12px', marginTop: '10px' }}>
<p style={{ margin: '5px 0' }}>Points: {tiles.length}</p>
<p style={{ margin: '5px 0', fontSize: '11px' }}>
<span style={{ color: '#888888' }}></span> None
<span style={{ color: '#4CAF50', marginLeft: '10px' }}></span> Community
<span style={{ color: '#2196F3', marginLeft: '10px' }}></span> Expert
<span style={{ color: '#9C27B0', marginLeft: '10px' }}></span> Multi-Expert
</p>
</div>
<button
onClick={handleExportIath}
disabled={isExporting}
style={{
width: '100%',
padding: '8px',
marginTop: '10px',
fontSize: '13px',
backgroundColor: isExporting ? '#555' : '#007acc',
color: 'white',
border: 'none',
borderRadius: '4px',
cursor: isExporting ? 'not-allowed' : 'pointer'
}}
>
{isExporting ? 'Exporting...' : '📦 Export .iath (High Precision)'}
</button>
</div>
{/* Info Panel - Hovered Tile */}
{hoveredTile && (
<div style={{
position: 'absolute',
top: 10,
right: 10,
zIndex: 10,
background: 'rgba(0, 0, 0, 0.8)',
padding: '10px',
borderRadius: '5px',
color: 'white',
maxWidth: '300px'
}}>
<h4 style={{ margin: '0 0 5px 0' }}>{hoveredTile.topic}</h4>
<p style={{ margin: '5px 0', fontSize: '12px' }}>
Domain: {hoveredTile.domain_id}
</p>
<p style={{ margin: '5px 0', fontSize: '12px' }}>
Confidence: {(hoveredTile.confidence_score * 100).toFixed(1)}%
</p>
<p style={{ margin: '5px 0', fontSize: '12px' }}>
Verification: {hoveredTile.verification_type}
</p>
<p style={{ margin: '5px 0', fontSize: '11px', color: '#aaa' }}>
Coords: [{hoveredTile.coordinates.map(c => c.toFixed(2)).join(', ')}]
</p>
</div>
)}
{/* Detailed Info Panel - Selected Tile */}
{selectedTile && (
<div style={{
position: 'absolute',
bottom: 10,
left: 10,
right: 10,
zIndex: 10,
background: 'rgba(0, 0, 0, 0.9)',
padding: '15px',
borderRadius: '5px',
color: 'white',
maxHeight: '200px',
overflow: 'auto'
}}>
<button
onClick={() => setSelectedTile(null)}
style={{
position: 'absolute',
top: '10px',
right: '10px',
background: 'transparent',
border: 'none',
color: 'white',
fontSize: '20px',
cursor: 'pointer'
}}
>
×
</button>
<h3 style={{ margin: '0 0 10px 0' }}>{selectedTile.topic}</h3>
<p style={{ margin: '5px 0' }}>
<strong>Tile ID:</strong> {selectedTile.tile_id}
</p>
<p style={{ margin: '5px 0' }}>
<strong>Domain:</strong> {selectedTile.domain_id}
</p>
<p style={{ margin: '5px 0' }}>
<strong>Confidence Score:</strong> {(selectedTile.confidence_score * 100).toFixed(1)}%
</p>
<p style={{ margin: '5px 0' }}>
<strong>Verification:</strong> {selectedTile.verification_type}
</p>
<p style={{ margin: '5px 0' }}>
<strong>6D Coordinates:</strong>
</p>
<ul style={{ margin: '5px 0', paddingLeft: '20px', fontSize: '12px' }}>
<li>X (Domain Axis 1): {selectedTile.coordinates[0].toFixed(3)}</li>
<li>Y (Domain Axis 2): {selectedTile.coordinates[1].toFixed(3)}</li>
<li>Z (Domain Axis 3): {selectedTile.coordinates[2].toFixed(3)}</li>
<li>C (Certainty): {selectedTile.coordinates[3].toFixed(3)}</li>
<li>G (Granularity): {selectedTile.coordinates[4].toFixed(3)}</li>
<li>V (Verification): {selectedTile.coordinates[5].toFixed(3)}</li>
</ul>
</div>
)}
{/* Loading/Error States */}
{isLoading && (
<div style={{
position: 'absolute',
top: '50%',
left: '50%',
transform: 'translate(-50%, -50%)',
zIndex: 10,
color: 'white',
fontSize: '20px'
}}>
Loading 3D space...
</div>
)}
{error && (
<div style={{
position: 'absolute',
top: '50%',
left: '50%',
transform: 'translate(-50%, -50%)',
zIndex: 10,
color: 'red',
fontSize: '16px',
background: 'rgba(0, 0, 0, 0.8)',
padding: '20px',
borderRadius: '5px'
}}>
{error}
</div>
)}
{/* 3D Canvas */}
{!isLoading && !error && (
<Canvas
camera={{ position: [10, 10, 10], fov: 60 }}
style={{ background: '#000011' }}
>
<ambientLight intensity={0.5} />
<pointLight position={[10, 10, 10]} intensity={1} />
<AxisLabels />
{tiles.map((tile) => (
<KnowledgePoint
key={tile.tile_id}
tile={tile}
onClick={handleTileClick}
onHover={handleTileHover}
/>
))}
<OrbitControls
enableDamping
dampingFactor={0.05}
rotateSpeed={0.5}
zoomSpeed={0.8}
/>
</Canvas>
)}
</div>
);
};
export default KnowledgeSpace3D;