Spaces:
Running
Running
| import React, { useState, useEffect, useRef } from 'react'; | |
| import { Search, Zap, Shield, Activity, Share2, Globe, Cpu, ChevronRight, Shuffle, AlertCircle, Database, TrendingUp, Upload, Link2, FileText, X, CheckCircle2 } from 'lucide-react'; | |
| // ========================================== | |
| // API CONFIGURATION (Import from config.js) | |
| // ========================================== | |
| // Note: In actual projects, should import from config.js | |
| // Here we inline config for compatibility | |
| // If config.js is loaded, window.API_CONFIG will be set | |
| const getAPIConfig = () => { | |
| if (typeof window !== 'undefined' && window.API_CONFIG) { | |
| return window.API_CONFIG; | |
| } | |
| // Default configuration (consistent with config.js) | |
| const defaultConfig = { | |
| baseURL: typeof window !== 'undefined' ? window.location.origin : '', | |
| wsURL: (() => { | |
| if (typeof window === 'undefined') return ''; | |
| const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:'; | |
| return `${protocol}//${window.location.host}`; | |
| })(), | |
| endpoints: { | |
| search: '/api/search', | |
| feedback: '/api/feedback', | |
| trending: '/api/trending', | |
| feed: '/api/feed', | |
| item: '/api/item', | |
| upload: { | |
| url: '/api/upload/url', | |
| text: '/api/upload/text', | |
| image: '/api/upload/image' | |
| } | |
| }, | |
| getURL: function(endpoint) { | |
| if (endpoint.startsWith('http://') || endpoint.startsWith('https://')) { | |
| return endpoint; | |
| } | |
| return `${this.baseURL}${endpoint}`; | |
| }, | |
| getWebSocketURL: function(path = '/ws') { | |
| return `${this.wsURL}${path}`; | |
| } | |
| }; | |
| return defaultConfig; | |
| }; | |
| // ========================================== | |
| // COMPONENT: 3D PARTICLE NETWORK BACKGROUND | |
| // ========================================== | |
| const ParticleNetwork = () => { | |
| const canvasRef = useRef(null); | |
| useEffect(() => { | |
| const canvas = canvasRef.current; | |
| if (!canvas) return; | |
| const ctx = canvas.getContext('2d'); | |
| let animationFrameId; | |
| let width, height; | |
| let particles = []; | |
| const particleCount = 60; | |
| const connectionDistance = 150; | |
| const mouseDistance = 200; | |
| let mouse = { x: null, y: null }; | |
| const resize = () => { | |
| width = window.innerWidth; | |
| height = window.innerHeight; | |
| canvas.width = width; | |
| canvas.height = height; | |
| initParticles(); | |
| }; | |
| class Particle { | |
| constructor() { | |
| this.x = Math.random() * width; | |
| this.y = Math.random() * height; | |
| this.vx = (Math.random() - 0.5) * 0.5; | |
| this.vy = (Math.random() - 0.5) * 0.5; | |
| this.size = Math.random() * 2 + 1; | |
| this.color = `rgba(${Math.random() * 50 + 100}, ${Math.random() * 100 + 155}, 255, ${Math.random() * 0.5 + 0.2})`; | |
| } | |
| update() { | |
| this.x += this.vx; | |
| this.y += this.vy; | |
| if (this.x < 0 || this.x > width) this.vx *= -1; | |
| if (this.y < 0 || this.y > height) this.vy *= -1; | |
| if (mouse.x != null) { | |
| let dx = mouse.x - this.x; | |
| let dy = mouse.y - this.y; | |
| let distance = Math.sqrt(dx * dx + dy * dy); | |
| if (distance < mouseDistance) { | |
| const forceDirectionX = dx / distance; | |
| const forceDirectionY = dy / distance; | |
| const force = (mouseDistance - distance) / mouseDistance; | |
| this.vx += forceDirectionX * force * 0.6; | |
| this.vy += forceDirectionY * force * 0.6; | |
| } | |
| } | |
| } | |
| draw() { | |
| ctx.beginPath(); | |
| ctx.arc(this.x, this.y, this.size, 0, Math.PI * 2); | |
| ctx.fillStyle = this.color; | |
| ctx.fill(); | |
| } | |
| } | |
| const initParticles = () => { | |
| particles = []; | |
| for (let i = 0; i < particleCount; i++) { | |
| particles.push(new Particle()); | |
| } | |
| }; | |
| const animate = () => { | |
| ctx.clearRect(0, 0, width, height); | |
| for (let i = 0; i < particles.length; i++) { | |
| for (let j = i + 1; j < particles.length; j++) { | |
| let dx = particles[i].x - particles[j].x; | |
| let dy = particles[i].y - particles[j].y; | |
| let distance = Math.sqrt(dx * dx + dy * dy); | |
| if (distance < connectionDistance) { | |
| ctx.beginPath(); | |
| ctx.strokeStyle = `rgba(100, 200, 255, ${1 - distance / connectionDistance})`; | |
| ctx.lineWidth = 0.5; | |
| ctx.moveTo(particles[i].x, particles[i].y); | |
| ctx.lineTo(particles[j].x, particles[j].y); | |
| ctx.stroke(); | |
| } | |
| } | |
| } | |
| particles.forEach(particle => { | |
| particle.update(); | |
| particle.draw(); | |
| }); | |
| animationFrameId = requestAnimationFrame(animate); | |
| }; | |
| const handleMouseMove = (e) => { | |
| mouse.x = e.x; | |
| mouse.y = e.y; | |
| }; | |
| window.addEventListener('resize', resize); | |
| window.addEventListener('mousemove', handleMouseMove); | |
| resize(); | |
| animate(); | |
| return () => { | |
| window.removeEventListener('resize', resize); | |
| window.removeEventListener('mousemove', handleMouseMove); | |
| cancelAnimationFrame(animationFrameId); | |
| }; | |
| }, []); | |
| return ( | |
| <canvas | |
| ref={canvasRef} | |
| className="fixed top-0 left-0 w-full h-full -z-10 bg-slate-900" | |
| /> | |
| ); | |
| }; | |
| // ========================================== | |
| // COMPONENT: ALGORITHM VISUALIZER | |
| // ========================================== | |
| const AlgorithmStep = ({ label, active, completed, icon: Icon, color }) => ( | |
| <div className={`flex items-center gap-3 transition-all duration-300 ${active ? 'scale-105 opacity-100' : 'opacity-50'}`}> | |
| <div className={`w-8 h-8 rounded-full flex items-center justify-center border ${ | |
| active || completed ? `border-${color}-500 bg-${color}-500/20 text-${color}-400` : 'border-slate-700 bg-slate-800 text-slate-600' | |
| }`}> | |
| {completed ? <div className="w-2 h-2 rounded-full bg-current" /> : <Icon size={14} />} | |
| </div> | |
| <div className="flex flex-col"> | |
| <span className={`text-xs font-mono uppercase tracking-wider ${active ? `text-${color}-400` : 'text-slate-500'}`}> | |
| {label} | |
| </span> | |
| {active && ( | |
| <span className="text-[10px] text-slate-400 animate-pulse">Processing...</span> | |
| )} | |
| </div> | |
| </div> | |
| ); | |
| // ========================================== | |
| // COMPONENT: SEARCH RESULT CARD | |
| // ========================================== | |
| const ResultCard = ({ item, index, onItemClick, onRecordInteraction }) => { | |
| const isExploration = item.is_exploration; | |
| const isHighTrust = item.pr > 0.8; | |
| const handleClick = (e) => { | |
| e.preventDefault(); | |
| if (onRecordInteraction) { | |
| onRecordInteraction(item.id, item.url); | |
| } | |
| if (onItemClick) { | |
| onItemClick(item.id); | |
| } | |
| }; | |
| return ( | |
| <div | |
| className={`group relative overflow-hidden rounded-xl border backdrop-blur-md transition-all duration-500 hover:-translate-y-1 hover:shadow-2xl cursor-pointer | |
| ${isExploration | |
| ? 'bg-purple-900/20 border-purple-500/50 hover:border-purple-400' | |
| : 'bg-slate-800/40 border-slate-700 hover:border-cyan-500/50' | |
| } | |
| `} | |
| onClick={handleClick} | |
| style={{ animationDelay: `${index * 100}ms` }} | |
| > | |
| <div className={`absolute inset-0 opacity-0 group-hover:opacity-100 transition-opacity duration-500 bg-gradient-to-r | |
| ${isExploration ? 'from-purple-600/10 to-pink-600/10' : 'from-cyan-600/10 to-blue-600/10'}`} | |
| /> | |
| <div className="p-6 relative z-10"> | |
| <div className="flex justify-between items-start mb-3"> | |
| <div className="flex gap-2"> | |
| {isExploration && ( | |
| <span className="flex items-center gap-1 px-2 py-0.5 rounded text-[10px] font-bold uppercase tracking-wider bg-purple-500/20 text-purple-300 border border-purple-500/30"> | |
| <Shuffle size={10} /> Serendipity | |
| </span> | |
| )} | |
| {isHighTrust && ( | |
| <span className="flex items-center gap-1 px-2 py-0.5 rounded text-[10px] font-bold uppercase tracking-wider bg-orange-500/20 text-orange-300 border border-orange-500/30"> | |
| <Shield size={10} /> High Trust | |
| </span> | |
| )} | |
| </div> | |
| <div className="flex items-center gap-2"> | |
| <span className="text-xs font-mono text-slate-500">SIM:</span> | |
| <span className={`text-sm font-bold font-mono ${isExploration ? 'text-purple-400' : 'text-cyan-400'}`}> | |
| {((item.score || 0) * 100).toFixed(1)}% | |
| </span> | |
| </div> | |
| </div> | |
| <h3 className="text-lg font-bold text-slate-100 mb-1 group-hover:text-cyan-300 transition-colors line-clamp-1"> | |
| {item.url || 'Untitled Neural Node'} | |
| </h3> | |
| <div className="text-xs font-mono text-slate-500 mb-3 truncate flex items-center gap-1"> | |
| <Globe size={10} /> {item.url} | |
| </div> | |
| <p className="text-sm text-slate-400 leading-relaxed line-clamp-3 font-light" | |
| dangerouslySetInnerHTML={{ | |
| __html: item.highlighted_snippet | |
| ? item.highlighted_snippet.replace( | |
| /\[\[HIGHLIGHT\]\](.*?)\[\[\/HIGHLIGHT\]\]/gi, | |
| '<strong class="font-bold text-cyan-400 bg-cyan-500/20 px-1 rounded">$1</strong>' | |
| ) | |
| : (item.content || "No neural trace available for this node.") | |
| }} | |
| /> | |
| <div className="mt-4 pt-4 border-t border-slate-700/50 flex justify-between items-center text-xs text-slate-500"> | |
| <div className="flex gap-3"> | |
| <span className="flex items-center gap-1 hover:text-slate-300 transition-colors"> | |
| <Activity size={12} /> ID: {item.id ? item.id.substring(0,6) : 'N/A'} | |
| </span> | |
| </div> | |
| <button className={`flex items-center gap-1 font-bold transition-colors ${isExploration ? 'text-purple-400 hover:text-purple-300' : 'text-cyan-600 hover:text-cyan-400'}`}> | |
| ACCESS NODE <ChevronRight size={12} /> | |
| </button> | |
| </div> | |
| </div> | |
| </div> | |
| ); | |
| }; | |
| // ========================================== | |
| // COMPONENT: PROGRESS TOAST | |
| // ========================================== | |
| const ProgressToast = ({ visible, count, message, onClose }) => { | |
| if (!visible) return null; | |
| return ( | |
| <div className="fixed bottom-6 right-6 z-50 transform transition-all duration-300"> | |
| <div className="bg-slate-800 rounded-xl shadow-xl border border-slate-700 p-4 w-80"> | |
| <div className="flex items-center justify-between mb-2"> | |
| <h4 className="text-sm font-bold text-slate-100">Ingesting Knowledge...</h4> | |
| <span className="text-xs font-mono text-cyan-600 bg-cyan-900/30 px-2 py-0.5 rounded-full"> | |
| {count} items | |
| </span> | |
| </div> | |
| <div className="w-full bg-slate-700 rounded-full h-1.5 mb-2 overflow-hidden"> | |
| <div className="bg-cyan-600 h-1.5 rounded-full animate-pulse" style={{ width: '70%' }} /> | |
| </div> | |
| <p className="text-xs text-slate-400 truncate">{message || 'Processing...'}</p> | |
| </div> | |
| </div> | |
| ); | |
| }; | |
| // ========================================== | |
| // COMPONENT: NOTIFICATION TOAST | |
| // ========================================== | |
| const NotificationToast = ({ visible, message, onClose }) => { | |
| if (!visible) return null; | |
| return ( | |
| <div className="fixed bottom-5 right-5 z-50 transform transition-all duration-300"> | |
| <div className="bg-slate-900 text-white px-6 py-4 rounded-lg shadow-2xl flex items-center gap-4 max-w-md border border-slate-700"> | |
| <div className="flex-1"> | |
| <h4 className="font-bold text-sm text-cyan-400 mb-1">System Update</h4> | |
| <p className="text-xs text-slate-300">{message}</p> | |
| </div> | |
| <button onClick={onClose} className="text-slate-500 hover:text-white"> | |
| <X size={16} /> | |
| </button> | |
| </div> | |
| </div> | |
| ); | |
| }; | |
| // ========================================== | |
| // COMPONENT: EDUCATIONAL CARDS | |
| // ========================================== | |
| const EducationalCards = () => ( | |
| <div className="grid grid-cols-1 md:grid-cols-3 gap-8 mb-16"> | |
| <div className="bg-slate-800/40 backdrop-blur-md p-6 rounded-xl border border-slate-700 hover:border-cyan-500/50 transition-all"> | |
| <div className="w-10 h-10 bg-cyan-500/20 rounded-lg flex items-center justify-center mb-4 text-cyan-400"> | |
| <Database size={20} /> | |
| </div> | |
| <h3 className="text-lg font-bold text-slate-100 mb-2">Knowledge Commons</h3> | |
| <p className="text-sm text-slate-400"> | |
| The raw data layer where all crawled information resides. It serves as the foundation for semantic retrieval. | |
| </p> | |
| </div> | |
| <div className="bg-slate-800/40 backdrop-blur-md p-6 rounded-xl border border-slate-700 hover:border-orange-500/50 transition-all"> | |
| <div className="w-10 h-10 bg-orange-500/20 rounded-lg flex items-center justify-center mb-4 text-orange-400"> | |
| <Shield size={20} /> | |
| </div> | |
| <h3 className="text-lg font-bold text-slate-100 mb-2">Curated Core</h3> | |
| <p className="text-sm text-slate-400"> | |
| A curated layer of "Novelty Anchors". Only unique, high-entropy knowledge is promoted here to form the network's backbone. | |
| </p> | |
| </div> | |
| <div className="bg-slate-800/40 backdrop-blur-md p-6 rounded-xl border border-slate-700 hover:border-purple-500/50 transition-all"> | |
| <div className="w-10 h-10 bg-purple-500/20 rounded-lg flex items-center justify-center mb-4 text-purple-400"> | |
| <Zap size={20} /> | |
| </div> | |
| <h3 className="text-lg font-bold text-slate-100 mb-2">Transitive Trust</h3> | |
| <p className="text-sm text-slate-400"> | |
| Authority flows through user navigation. If you trust A, and A links to B, then B gains trust. | |
| </p> | |
| </div> | |
| </div> | |
| ); | |
| // ========================================== | |
| // COMPONENT: KNOWLEDGE INJECTION PANEL | |
| // ========================================== | |
| const KnowledgeInjectionPanel = ({ apiConfig, onNotification }) => { | |
| const [urlInput, setUrlInput] = useState(''); | |
| const [urlPassword, setUrlPassword] = useState(''); | |
| const [textInput, setTextInput] = useState(''); | |
| const [isUploading, setIsUploading] = useState(false); | |
| const handleUploadUrl = async () => { | |
| if (!urlInput.trim()) { | |
| if (onNotification) { | |
| onNotification("Please enter URL", "error"); | |
| } | |
| return; | |
| } | |
| if (!urlPassword.trim()) { | |
| if (onNotification) { | |
| onNotification("Please enter password", "error"); | |
| } | |
| return; | |
| } | |
| setIsUploading(true); | |
| try { | |
| const formData = new FormData(); | |
| formData.append('url', urlInput); | |
| formData.append('password', urlPassword); | |
| const response = await fetch(apiConfig.getURL(apiConfig.endpoints.upload.url), { | |
| method: 'POST', | |
| body: formData | |
| }); | |
| const result = await response.json(); | |
| if (response.ok) { | |
| setUrlInput(''); | |
| setUrlPassword(''); | |
| if (onNotification) { | |
| onNotification(result.message || "URL injected into the cortex. Processing..."); | |
| } | |
| } else { | |
| if (onNotification) { | |
| onNotification(result.detail || "Incorrect password, crawl rejected", "error"); | |
| } | |
| } | |
| } catch (e) { | |
| console.error("Failed to upload URL", e); | |
| if (onNotification) { | |
| onNotification("Upload failed: " + e.message, "error"); | |
| } | |
| } finally { | |
| setIsUploading(false); | |
| } | |
| }; | |
| const handleUploadText = async () => { | |
| if (!textInput.trim()) return; | |
| setIsUploading(true); | |
| try { | |
| const formData = new FormData(); | |
| formData.append('text', textInput); | |
| await fetch(apiConfig.getURL(apiConfig.endpoints.upload.text), { | |
| method: 'POST', | |
| body: formData | |
| }); | |
| setTextInput(''); | |
| if (onNotification) { | |
| onNotification("Text fragment absorbed. Processing..."); | |
| } | |
| } catch (e) { | |
| console.error("Failed to upload text", e); | |
| } finally { | |
| setIsUploading(false); | |
| } | |
| }; | |
| return ( | |
| <div className="mt-24 border-t border-slate-700/50 pt-12"> | |
| <h2 className="text-2xl font-bold text-slate-100 mb-6 text-center">Inject Knowledge</h2> | |
| <div className="grid grid-cols-1 md:grid-cols-2 gap-8 max-w-4xl mx-auto"> | |
| <div className="bg-slate-800/40 backdrop-blur-md p-6 rounded-xl border border-slate-700"> | |
| <h3 className="font-semibold mb-4 flex items-center text-slate-100"> | |
| <span className="bg-cyan-500/20 text-cyan-400 text-xs font-medium mr-2 px-2.5 py-0.5 rounded border border-cyan-500/30">URL</span> | |
| Crawl & Absorb | |
| </h3> | |
| <div className="space-y-2"> | |
| <input | |
| type="text" | |
| value={urlInput} | |
| onChange={(e) => setUrlInput(e.target.value)} | |
| placeholder="https://tum.de/..." | |
| className="w-full bg-slate-900 border border-slate-600 text-slate-100 text-sm rounded-lg focus:ring-cyan-500 focus:border-cyan-500 block p-2.5" | |
| onKeyPress={(e) => e.key === 'Enter' && handleUploadUrl()} | |
| /> | |
| <div className="flex gap-2"> | |
| <input | |
| type="password" | |
| value={urlPassword} | |
| onChange={(e) => setUrlPassword(e.target.value)} | |
| placeholder="Enter password..." | |
| className="flex-1 bg-slate-900 border border-slate-600 text-slate-100 text-sm rounded-lg focus:ring-cyan-500 focus:border-cyan-500 block p-2.5" | |
| onKeyPress={(e) => e.key === 'Enter' && handleUploadUrl()} | |
| /> | |
| <button | |
| onClick={handleUploadUrl} | |
| disabled={isUploading} | |
| className="text-white bg-cyan-600 hover:bg-cyan-700 disabled:opacity-50 font-medium rounded-lg text-sm px-5 py-2.5 transition-colors" | |
| > | |
| <Upload size={16} /> | |
| </button> | |
| </div> | |
| </div> | |
| </div> | |
| <div className="bg-slate-800/40 backdrop-blur-md p-6 rounded-xl border border-slate-700"> | |
| <h3 className="font-semibold mb-4 flex items-center text-slate-100"> | |
| <span className="bg-slate-500/20 text-slate-300 text-xs font-medium mr-2 px-2.5 py-0.5 rounded border border-slate-500/30">TEXT</span> | |
| Direct Input | |
| </h3> | |
| <div className="flex gap-2"> | |
| <input | |
| type="text" | |
| value={textInput} | |
| onChange={(e) => setTextInput(e.target.value)} | |
| placeholder="Paste knowledge snippet..." | |
| className="flex-1 bg-slate-900 border border-slate-600 text-slate-100 text-sm rounded-lg focus:ring-slate-500 focus:border-slate-500 block p-2.5" | |
| onKeyPress={(e) => e.key === 'Enter' && handleUploadText()} | |
| /> | |
| <button | |
| onClick={handleUploadText} | |
| disabled={isUploading} | |
| className="text-white bg-slate-700 hover:bg-slate-600 disabled:opacity-50 font-medium rounded-lg text-sm px-5 py-2.5 transition-colors" | |
| > | |
| <Upload size={16} /> | |
| </button> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| ); | |
| }; | |
| // ========================================== | |
| // COMPONENT: TRENDING SECTION | |
| // ========================================== | |
| const TrendingSection = ({ apiConfig, onItemClick, onRecordInteraction }) => { | |
| const [trending, setTrending] = useState([]); | |
| const [loading, setLoading] = useState(true); | |
| useEffect(() => { | |
| const loadTrending = async () => { | |
| try { | |
| const response = await fetch(apiConfig.getURL(`${apiConfig.endpoints.trending}?limit=3`)); | |
| const data = await response.json(); | |
| if (data.results) { | |
| setTrending(data.results); | |
| } | |
| } catch (e) { | |
| console.error("Failed to load trending", e); | |
| } finally { | |
| setLoading(false); | |
| } | |
| }; | |
| loadTrending(); | |
| }, [apiConfig]); | |
| if (loading) { | |
| return ( | |
| <div className="max-w-7xl mx-auto mb-16 px-4"> | |
| <h2 className="text-2xl font-bold text-slate-100 mb-6 flex items-center gap-2"> | |
| <span className="w-2 h-8 bg-orange-500 rounded-full"></span> | |
| 🔥 Trending Now | |
| </h2> | |
| <div className="text-center py-8 text-slate-400">Loading hot topics...</div> | |
| </div> | |
| ); | |
| } | |
| if (trending.length === 0) { | |
| return ( | |
| <div className="max-w-7xl mx-auto mb-16 px-4"> | |
| <h2 className="text-2xl font-bold text-slate-100 mb-6 flex items-center gap-2"> | |
| <span className="w-2 h-8 bg-orange-500 rounded-full"></span> | |
| 🔥 Trending Now | |
| </h2> | |
| <div className="text-center py-8 text-slate-400">No trending topics yet. Start exploring!</div> | |
| </div> | |
| ); | |
| } | |
| return ( | |
| <div className="max-w-7xl mx-auto mb-16 px-4"> | |
| <h2 className="text-2xl font-bold text-slate-100 mb-6 flex items-center gap-2"> | |
| <span className="w-2 h-8 bg-orange-500 rounded-full"></span> | |
| 🔥 Trending Now | |
| </h2> | |
| <div className="grid grid-cols-1 md:grid-cols-3 gap-6"> | |
| {trending.map((item, index) => { | |
| const p = item.payload || {}; | |
| return ( | |
| <div | |
| key={item.id || index} | |
| onClick={() => { | |
| if (onRecordInteraction) onRecordInteraction(item.id, p.url); | |
| if (onItemClick) onItemClick(item.id); | |
| }} | |
| className="bg-gradient-to-br from-orange-900/30 to-slate-800/40 p-5 rounded-xl border border-orange-500/30 shadow-sm hover:shadow-md transition-all relative overflow-hidden cursor-pointer backdrop-blur-md" | |
| > | |
| <div className="absolute top-0 right-0 bg-orange-500/20 text-orange-400 text-xs font-bold px-3 py-1 rounded-bl-xl"> | |
| #{index + 1} | |
| </div> | |
| <div className="text-xs font-bold text-orange-400 mb-2 uppercase tracking-wider">Hot Topic</div> | |
| <div className="text-lg font-bold text-slate-100 mb-2 line-clamp-1">{p.url || 'Untitled'}</div> | |
| <div className="text-sm text-slate-400 line-clamp-2 mb-3">{p.content_preview || ''}</div> | |
| <div className="flex items-center gap-2 text-xs text-slate-400 font-mono"> | |
| <span className="flex items-center gap-1 text-orange-500 font-bold"> | |
| <TrendingUp size={12} /> | |
| {item.clicks || 0} Clicks | |
| </span> | |
| </div> | |
| </div> | |
| ); | |
| })} | |
| </div> | |
| </div> | |
| ); | |
| }; | |
| // ========================================== | |
| // COMPONENT: FEED SECTION | |
| // ========================================== | |
| const FeedSection = ({ apiConfig, onItemClick, onRecordInteraction }) => { | |
| const [feed, setFeed] = useState([]); | |
| const [loading, setLoading] = useState(true); | |
| useEffect(() => { | |
| const loadFeed = async () => { | |
| try { | |
| const response = await fetch(apiConfig.getURL(`${apiConfig.endpoints.feed}?limit=6`)); | |
| const data = await response.json(); | |
| if (data.points) { | |
| setFeed(data.points); | |
| } | |
| } catch (e) { | |
| console.error("Failed to load feed", e); | |
| } finally { | |
| setLoading(false); | |
| } | |
| }; | |
| loadFeed(); | |
| }, [apiConfig]); | |
| if (loading) { | |
| return ( | |
| <div className="max-w-7xl mx-auto mb-16 px-4"> | |
| <h2 className="text-2xl font-bold text-slate-100 mb-6 flex items-center gap-2"> | |
| <span className="w-2 h-8 bg-cyan-600 rounded-full"></span> | |
| Recent Knowledge Ingestions | |
| </h2> | |
| <div className="text-center py-8 text-slate-400">Loading feed...</div> | |
| </div> | |
| ); | |
| } | |
| if (feed.length === 0) { | |
| return ( | |
| <div className="max-w-7xl mx-auto mb-16 px-4"> | |
| <h2 className="text-2xl font-bold text-slate-100 mb-6 flex items-center gap-2"> | |
| <span className="w-2 h-8 bg-cyan-600 rounded-full"></span> | |
| Recent Knowledge Ingestions | |
| </h2> | |
| <div className="text-center py-8 text-slate-400">No knowledge found in the Commons.</div> | |
| </div> | |
| ); | |
| } | |
| return ( | |
| <div className="max-w-7xl mx-auto mb-16 px-4"> | |
| <h2 className="text-2xl font-bold text-slate-100 mb-6 flex items-center gap-2"> | |
| <span className="w-2 h-8 bg-cyan-600 rounded-full"></span> | |
| Recent Knowledge Ingestions | |
| </h2> | |
| <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6"> | |
| {feed.map((pt) => { | |
| const p = pt.payload || {}; | |
| return ( | |
| <div | |
| key={pt.id} | |
| onClick={() => { | |
| if (onRecordInteraction) onRecordInteraction(pt.id, p.url); | |
| if (onItemClick) onItemClick(pt.id); | |
| }} | |
| className="bg-slate-800/40 backdrop-blur-md p-5 rounded-lg border border-slate-700 shadow-sm hover:shadow-md transition-all cursor-pointer" | |
| > | |
| <div className="text-xs font-bold text-slate-400 mb-2 uppercase tracking-wider">{p.type || 'Unknown'}</div> | |
| <div className="text-base font-bold text-slate-100 hover:text-cyan-400 mb-2 line-clamp-1">{p.url || 'Untitled'}</div> | |
| <div className="text-sm text-slate-400 line-clamp-3 mb-3">{p.content_preview || ''}</div> | |
| <div className="text-xs text-slate-500 font-mono">ID: {pt.id ? pt.id.substring(0, 6) : 'N/A'}...</div> | |
| </div> | |
| ); | |
| })} | |
| </div> | |
| </div> | |
| ); | |
| }; | |
| // ========================================== | |
| // MAIN APP COMPONENT | |
| // ========================================== | |
| export default function App() { | |
| const [query, setQuery] = useState(""); | |
| const [isSearching, setIsSearching] = useState(false); | |
| const [results, setResults] = useState([]); | |
| const [searchStep, setSearchStep] = useState(0); | |
| const [showIntro, setShowIntro] = useState(true); | |
| const [progressToast, setProgressToast] = useState({ visible: false, count: 0, message: '' }); | |
| const [notification, setNotification] = useState({ visible: false, message: '' }); | |
| const [ws, setWs] = useState(null); | |
| const apiConfig = getAPIConfig(); | |
| // WebSocket connection | |
| useEffect(() => { | |
| const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:'; | |
| const wsUrl = apiConfig.getWebSocketURL(); | |
| const websocket = new WebSocket(wsUrl); | |
| websocket.onmessage = (event) => { | |
| const data = JSON.parse(event.data); | |
| if (data.type === 'progress') { | |
| setProgressToast({ | |
| visible: true, | |
| count: data.count || 0, | |
| message: data.message || data.details || 'Processing...' | |
| }); | |
| } else if (data.type === 'system_update') { | |
| setProgressToast({ | |
| visible: true, | |
| count: 'Done', | |
| message: data.message || 'Synchronization Complete.' | |
| }); | |
| setTimeout(() => { | |
| setProgressToast({ visible: false, count: 0, message: '' }); | |
| }, 5000); | |
| // Refresh feed and trending | |
| window.location.reload(); | |
| } else if (data.type === 'error') { | |
| setNotification({ | |
| visible: true, | |
| message: `System Error: ${data.message}` | |
| }); | |
| } | |
| }; | |
| websocket.onerror = (error) => { | |
| console.error('WebSocket error:', error); | |
| }; | |
| setWs(websocket); | |
| return () => { | |
| websocket.close(); | |
| }; | |
| }, [apiConfig]); | |
| // Record user interaction | |
| const recordInteraction = async (itemId, url) => { | |
| try { | |
| const formData = new FormData(); | |
| formData.append('item_id', itemId); | |
| formData.append('action', 'click'); | |
| await fetch(apiConfig.getURL(apiConfig.endpoints.feedback), { | |
| method: 'POST', | |
| body: formData, | |
| keepalive: true | |
| }); | |
| console.log("Interaction recorded for", itemId); | |
| } catch (e) { | |
| console.error("Failed to record interaction", e); | |
| } | |
| }; | |
| // Navigate to detail page | |
| const handleItemClick = (itemId) => { | |
| window.location.href = `/view/${itemId}`; | |
| }; | |
| // Show notification | |
| const showNotification = (message) => { | |
| setNotification({ visible: true, message }); | |
| setTimeout(() => { | |
| setNotification({ visible: false, message: '' }); | |
| }, 5000); | |
| }; | |
| // Search functionality | |
| const handleSearch = async (e) => { | |
| e?.preventDefault(); | |
| if (!query.trim()) return; | |
| setIsSearching(true); | |
| setShowIntro(false); | |
| setResults([]); | |
| setSearchStep(1); | |
| setTimeout(() => { | |
| setSearchStep(2); | |
| setTimeout(() => { | |
| setSearchStep(3); | |
| setTimeout(async () => { | |
| try { | |
| const res = await fetch(apiConfig.getURL(`${apiConfig.endpoints.search}?q=${encodeURIComponent(query)}`)); | |
| if (!res.ok) throw new Error("Backend offline"); | |
| const data = await res.json(); | |
| setResults(data.results || []); | |
| } catch (err) { | |
| console.warn("Backend unavailable, using mock data."); | |
| setResults([]); | |
| } | |
| setSearchStep(4); | |
| setIsSearching(false); | |
| }, 800); | |
| }, 800); | |
| }, 800); | |
| }; | |
| return ( | |
| <div className="min-h-screen text-slate-200 font-sans selection:bg-cyan-500/30 selection:text-cyan-200 overflow-x-hidden"> | |
| <ParticleNetwork /> | |
| <nav className="fixed top-0 w-full z-50 border-b border-slate-800/50 bg-slate-900/60 backdrop-blur-md"> | |
| <div className="max-w-7xl mx-auto px-6 h-16 flex justify-between items-center"> | |
| <div className="flex items-center gap-3"> | |
| <div className="w-8 h-8 bg-gradient-to-br from-cyan-500 to-blue-600 rounded-lg flex items-center justify-center shadow-[0_0_15px_rgba(6,182,212,0.5)]"> | |
| <span className="font-bold text-white font-mono">N</span> | |
| </div> | |
| <span className="font-bold text-xl tracking-tight bg-clip-text text-transparent bg-gradient-to-r from-cyan-400 to-blue-500"> | |
| TUM Neural Cortex | |
| </span> | |
| </div> | |
| <div className="flex items-center gap-4 text-xs font-mono text-slate-400"> | |
| <span className="flex items-center gap-1.5"> | |
| <span className="w-2 h-2 rounded-full bg-emerald-500 animate-pulse" /> | |
| SYSTEM ONLINE | |
| </span> | |
| <span className="opacity-50">|</span> | |
| <span>SPACE_X::LOADED</span> | |
| </div> | |
| </div> | |
| </nav> | |
| <main className="pt-32 pb-20 px-4 max-w-7xl mx-auto relative z-10"> | |
| {showIntro && ( | |
| <> | |
| <div className="text-center mb-16"> | |
| <h1 className="text-5xl md:text-7xl font-extrabold mb-6 tracking-tight"> | |
| Query the <br/> | |
| <span className="bg-clip-text text-transparent bg-gradient-to-r from-cyan-300 via-blue-400 to-purple-400"> | |
| Collective Intelligence | |
| </span> | |
| </h1> | |
| <p className="text-slate-400 max-w-2xl mx-auto text-lg mb-12 font-light"> | |
| Access the high-dimensional knowledge vector space of TUM. | |
| Powered by CLIP embeddings, consistency checks, and serendipity algorithms. | |
| </p> | |
| </div> | |
| <EducationalCards /> | |
| </> | |
| )} | |
| <form onSubmit={handleSearch} className="relative max-w-2xl mx-auto group mb-16"> | |
| <div className="absolute -inset-1 bg-gradient-to-r from-cyan-500 via-blue-500 to-purple-600 rounded-xl blur opacity-30 group-hover:opacity-100 transition duration-1000 group-hover:duration-200" /> | |
| <div className="relative flex bg-slate-900 rounded-xl border border-slate-700/50 shadow-2xl overflow-hidden"> | |
| <input | |
| type="text" | |
| value={query} | |
| onChange={(e) => setQuery(e.target.value)} | |
| placeholder="Input neural query pattern (e.g., 'Robotics Research')..." | |
| className="flex-1 bg-transparent border-none text-slate-100 px-6 py-4 text-lg focus:outline-none placeholder:text-slate-600 font-light" | |
| /> | |
| <button | |
| type="submit" | |
| disabled={isSearching} | |
| className="bg-slate-800 hover:bg-slate-700 text-cyan-400 px-8 py-2 font-medium transition-colors border-l border-slate-700 flex items-center gap-2" | |
| > | |
| {isSearching ? <Activity className="animate-spin" /> : <Search />} | |
| IGNITE | |
| </button> | |
| </div> | |
| </form> | |
| {(isSearching || searchStep > 0) && ( | |
| <div className="mt-8 max-w-2xl mx-auto grid grid-cols-3 gap-4 p-4 rounded-xl bg-slate-900/50 border border-slate-800 backdrop-blur-sm mb-16"> | |
| <AlgorithmStep | |
| label="CLIP Embedding" | |
| icon={Cpu} | |
| color="cyan" | |
| active={searchStep === 1} | |
| completed={searchStep > 1} | |
| /> | |
| <AlgorithmStep | |
| label="Vector Search" | |
| icon={Globe} | |
| color="blue" | |
| active={searchStep === 2} | |
| completed={searchStep > 2} | |
| /> | |
| <AlgorithmStep | |
| label="Consistency & Ranking" | |
| icon={Shield} | |
| color="purple" | |
| active={searchStep === 3} | |
| completed={searchStep > 3} | |
| /> | |
| </div> | |
| )} | |
| {!showIntro && searchStep === 4 && ( | |
| <div className="mt-8 mb-16"> | |
| <div className="flex items-center justify-between mb-6 px-2"> | |
| <h2 className="text-xl font-bold text-slate-100 flex items-center gap-2"> | |
| <Activity className="text-cyan-500" size={20} /> | |
| Neural Responses | |
| </h2> | |
| <span className="text-xs font-mono text-slate-500 bg-slate-800 px-3 py-1 rounded-full"> | |
| {results.length} NODES ACTIVATED | |
| </span> | |
| </div> | |
| <div className="grid grid-cols-1 md:grid-cols-2 gap-6"> | |
| {results.map((item, idx) => ( | |
| <ResultCard | |
| key={item.id || idx} | |
| item={item} | |
| index={idx} | |
| onItemClick={handleItemClick} | |
| onRecordInteraction={recordInteraction} | |
| /> | |
| ))} | |
| </div> | |
| {results.length === 0 && ( | |
| <div className="text-center py-20 text-slate-500"> | |
| <AlertCircle className="mx-auto mb-4 opacity-50" size={48} /> | |
| <p>No convergent nodes found in the vector space.</p> | |
| </div> | |
| )} | |
| </div> | |
| )} | |
| {showIntro && ( | |
| <> | |
| <TrendingSection | |
| apiConfig={apiConfig} | |
| onItemClick={handleItemClick} | |
| onRecordInteraction={recordInteraction} | |
| /> | |
| <FeedSection | |
| apiConfig={apiConfig} | |
| onItemClick={handleItemClick} | |
| onRecordInteraction={recordInteraction} | |
| /> | |
| </> | |
| )} | |
| <KnowledgeInjectionPanel | |
| apiConfig={apiConfig} | |
| onNotification={showNotification} | |
| /> | |
| </main> | |
| <ProgressToast | |
| visible={progressToast.visible} | |
| count={progressToast.count} | |
| message={progressToast.message} | |
| onClose={() => setProgressToast({ visible: false, count: 0, message: '' })} | |
| /> | |
| <NotificationToast | |
| visible={notification.visible} | |
| message={notification.message} | |
| onClose={() => setNotification({ visible: false, message: '' })} | |
| /> | |
| <div className="fixed bottom-0 left-0 w-full h-1 bg-gradient-to-r from-transparent via-cyan-500/50 to-transparent z-50 opacity-50" /> | |
| </div> | |
| ); | |
| } | |