Spaces:
Sleeping
Sleeping
| // 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; | |