GitHub Action
Sync from GitHub Actions (Clean Commit)
7f22d3c
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>TUM Neural Knowledge Network</title>
<script src="https://cdn.tailwindcss.com"></script>
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/echarts.min.js"></script>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;600;700&display=swap" rel="stylesheet">
<style>
body {
font-family: 'Inter', sans-serif;
}
.glass-panel {
background: rgba(255, 255, 255, 0.9);
backdrop-filter: blur(10px);
border: 1px solid rgba(255, 255, 255, 0.2);
}
.gradient-text {
background: linear-gradient(135deg, #3070b3 0%, #e37222 100%);
-webkit-background-clip: text;
background-clip: text;
-webkit-text-fill-color: transparent;
}
/* Particle background Canvas styles */
#particle-canvas {
position: fixed !important;
top: 0 !important;
left: 0 !important;
width: 100% !important;
height: 100% !important;
z-index: -10 !important;
background: #0f172a !important;
display: block !important;
pointer-events: none;
}
</style>
</head>
<body class="bg-slate-900 text-slate-200 min-h-screen">
<!-- Particle Background Canvas -->
<canvas id="particle-canvas"></canvas>
<!-- Navbar -->
<nav class="bg-slate-900/60 backdrop-blur-md border-b border-slate-800/50 sticky top-0 z-50">
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div class="flex justify-between h-16">
<div class="flex items-center gap-8">
<a href="/" class="flex items-center gap-2 group">
<div
class="w-8 h-8 bg-gradient-to-br from-blue-600 to-purple-600 rounded-lg flex items-center justify-center text-white font-bold shadow-md group-hover:shadow-lg transition-all">
N</div>
<span class="text-xl font-bold tracking-tight gradient-text">TUM Neural Net</span>
</a>
<div class="hidden md:flex items-center space-x-6 text-sm font-medium text-slate-400">
<a href="/" class="hover:text-cyan-400 transition-colors">Home</a>
<a href="#" class="hover:text-cyan-400 transition-colors">Knowledge Graph</a>
<a href="#" class="hover:text-cyan-400 transition-colors">About</a>
</div>
</div>
<div class="flex items-center space-x-4">
<div id="status-indicator"
class="flex items-center text-xs font-medium text-emerald-400 bg-emerald-500/20 px-3 py-1.5 rounded-full border border-emerald-500/30">
<span class="w-2 h-2 bg-emerald-400 rounded-full mr-2 animate-pulse"></span>
System Active
</div>
</div>
</div>
</div>
</nav>
<!-- Progress Notification (Hidden by default) -->
<div id="progress-toast"
class="fixed bottom-6 right-6 z-50 transform transition-all duration-300 translate-y-24 opacity-0">
<div class="bg-slate-800/90 backdrop-blur-md rounded-xl shadow-xl border border-slate-700/50 p-4 w-80">
<div class="flex items-center justify-between mb-2">
<h4 class="text-sm font-bold text-slate-100" id="progress-title">Ingesting Knowledge...</h4>
<span id="progress-count" class="text-xs font-mono text-cyan-400 bg-cyan-500/20 px-2 py-0.5 rounded-full">0 items</span>
</div>
<!-- Determinate Progress Bar (for URL crawling) -->
<div id="progress-bar-container" class="hidden w-full bg-slate-700/50 rounded-full h-2 mb-2 overflow-hidden">
<div id="progress-bar" class="bg-gradient-to-r from-cyan-500 to-blue-500 h-2 rounded-full transition-all duration-300" style="width: 0%"></div>
</div>
<!-- Indeterminate Progress Bar (for other tasks) -->
<div id="progress-bar-indeterminate" class="w-full bg-slate-700/50 rounded-full h-1.5 mb-2 overflow-hidden">
<div class="bg-cyan-500 h-1.5 rounded-full animate-progress-indeterminate"></div>
</div>
<p id="progress-detail" class="text-xs text-slate-400 truncate">Initializing...</p>
<p id="progress-url" class="text-xs text-slate-500 truncate mt-1 hidden"></p>
</div>
</div>
<style>
@keyframes progress-indeterminate {
0% {
width: 0%;
margin-left: 0%;
}
50% {
width: 70%;
margin-left: 30%;
}
100% {
width: 0%;
margin-left: 100%;
}
}
.animate-progress-indeterminate {
animation: progress-indeterminate 1.5s infinite ease-in-out;
}
</style>
<main class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-12">
<!-- Hero Section -->
<div class="text-center mb-16">
<h1 class="text-4xl sm:text-5xl font-extrabold text-slate-100 mb-4">
Query the <span class="bg-clip-text text-transparent bg-gradient-to-r from-cyan-400 to-blue-500">Collective Intelligence</span>
</h1>
<p class="text-lg text-slate-400 max-w-2xl mx-auto">
Access the distributed knowledge of the Technical University of Munich.
Powered by semantic understanding and transitive trust algorithms.
</p>
</div>
<!-- Search Interface -->
<div class="max-w-3xl mx-auto mb-16">
<div class="relative group">
<div
class="absolute -inset-1 bg-gradient-to-r from-blue-600 to-orange-600 rounded-lg blur opacity-25 group-hover:opacity-50 transition duration-1000 group-hover:duration-200">
</div>
<div class="relative">
<input type="text" id="search-input"
class="block w-full p-5 pl-6 text-lg text-slate-100 border border-slate-700/50 rounded-lg bg-slate-800/50 backdrop-blur-sm shadow-xl focus:ring-2 focus:ring-cyan-500 focus:border-cyan-500/50 outline-none transition-all placeholder:text-slate-500"
placeholder="Ask the network (e.g., 'Engineering Degrees')..." required>
<button onclick="performSearch()"
class="absolute right-2.5 bottom-2.5 bg-gradient-to-r from-cyan-500 to-blue-600 text-white hover:from-cyan-600 hover:to-blue-700 focus:ring-4 focus:outline-none focus:ring-cyan-300 font-medium rounded-lg text-sm px-6 py-3 transition-all shadow-lg shadow-cyan-500/50">
Ignite
</button>
</div>
</div>
</div>
<!-- Educational Cards (How it Works) -->
<div id="intro-cards" class="grid grid-cols-1 md:grid-cols-3 gap-8 mb-16">
<!-- Card 1 -->
<div class="bg-slate-800/50 backdrop-blur-sm p-6 rounded-xl shadow-lg border border-slate-700/50 hover:shadow-xl hover:border-cyan-500/50 transition-all">
<div class="w-10 h-10 bg-blue-100 rounded-lg flex items-center justify-center mb-4 text-blue-600">
<svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M19 11H5m14 0a2 2 0 012 2v6a2 2 0 01-2 2H5a2 2 0 01-2-2v-6a2 2 0 012-2m14 0V9a2 2 0 00-2-2M5 11V9a2 2 0 012-2m0 0V5a2 2 0 012-2h6a2 2 0 012 2v2M7 7h10">
</path>
</svg>
</div>
<h3 class="text-lg font-bold text-slate-100 mb-2">Knowledge Commons</h3>
<p class="text-sm text-slate-400">
The raw data layer where all crawled information resides. It serves as the foundation for semantic
retrieval.
</p>
</div>
<!-- Card 2 -->
<div class="bg-slate-800/50 backdrop-blur-sm p-6 rounded-xl shadow-lg border border-slate-700/50 hover:shadow-xl hover:border-cyan-500/50 transition-all">
<div class="w-10 h-10 bg-orange-100 rounded-lg flex items-center justify-center mb-4 text-orange-600">
<svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z"></path>
</svg>
</div>
<h3 class="text-lg font-bold text-slate-100 mb-2">Curated Core</h3>
<p class="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>
<!-- Card 3 -->
<div class="bg-slate-800/50 backdrop-blur-sm p-6 rounded-xl shadow-lg border border-slate-700/50 hover:shadow-xl hover:border-cyan-500/50 transition-all">
<div class="w-10 h-10 bg-purple-100 rounded-lg flex items-center justify-center mb-4 text-purple-600">
<svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M13 10V3L4 14h7v7l9-11h-7z"></path>
</svg>
</div>
<h3 class="text-lg font-bold text-slate-100 mb-2">Transitive Trust</h3>
<p class="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>
<!-- Results Section with Tabs -->
<div id="results-section" class="max-w-7xl mx-auto hidden">
<!-- Tab Navigation -->
<div class="flex items-center justify-center mb-6 gap-2">
<button id="tab-list" onclick="switchTab('list')"
class="px-6 py-3 text-sm font-semibold rounded-lg transition-all duration-200 bg-cyan-500/20 text-cyan-400 border-2 border-cyan-500/50 hover:bg-cyan-500/30 active-tab">
<svg class="w-5 h-5 inline mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 6h16M4 10h16M4 14h16M4 18h16"></path>
</svg>
List View
</button>
<button id="tab-graph" onclick="switchTab('graph')"
class="px-6 py-3 text-sm font-semibold rounded-lg transition-all duration-200 bg-slate-800/50 text-slate-400 border-2 border-slate-700/50 hover:bg-slate-700/50 hover:text-cyan-400 hover:border-cyan-500/50">
<svg class="w-5 h-5 inline mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 20l-5.447-2.724A1 1 0 013 16.382V5.618a1 1 0 011.447-.894L9 7m0 13l6-3m-6 3V7m6 10l4.553 2.276A1 1 0 0021 18.382V7.618a1 1 0 00-.553-.894L15 4m0 13V4m0 0L9 7"></path>
</svg>
Graph View
</button>
</div>
<!-- List View -->
<div id="list-view" class="space-y-6">
<div id="results-area" class="max-w-4xl mx-auto space-y-6">
<!-- Results will be injected here -->
</div>
</div>
<!-- Graph View -->
<div id="graph-view" class="hidden">
<div id="graph-container" style="width: 100%; height: 700px; background: rgba(15, 23, 42, 0.5); border-radius: 12px; border: 1px solid rgba(148, 163, 184, 0.2);"></div>
</div>
</div>
<!-- Injection Panel (Upload) -->
<div class="mt-24 border-t border-slate-700/50 pt-12">
<h2 class="text-2xl font-bold text-slate-100 mb-6 text-center">Inject Knowledge</h2>
<div class="grid grid-cols-1 md:grid-cols-2 gap-8 max-w-4xl mx-auto">
<!-- XML Dump Upload -->
<div class="bg-slate-800/50 backdrop-blur-sm p-6 rounded-xl border border-slate-700/50 md:col-span-2">
<h3 class="font-semibold mb-4 flex items-center text-slate-100">
<span class="bg-purple-100 text-purple-800 text-xs font-medium mr-2 px-2.5 py-0.5 rounded">XML</span>
Wiki Dump Import
</h3>
<div class="space-y-3">
<div class="flex gap-2 items-center">
<input type="file" id="xml-dump-file-input" accept=".xml,.bz2,.gz"
class="hidden">
<label for="xml-dump-file-input"
class="flex-1 bg-slate-700/50 border border-slate-600/50 text-slate-100 text-sm rounded-lg focus:ring-cyan-500 focus:border-cyan-500/50 block p-2.5 cursor-pointer hover:bg-slate-700/70 transition-colors">
<span id="xml-dump-file-name" class="text-slate-400">Select XML Dump file...</span>
</label>
<button onclick="document.getElementById('xml-dump-file-input').click()"
class="text-white bg-purple-600 hover:bg-purple-700 focus:ring-4 focus:ring-purple-300 font-medium rounded-lg text-sm px-4 py-2.5">Select File</button>
</div>
<div class="flex gap-2">
<input type="text" id="xml-dump-base-url"
class="flex-1 bg-slate-700/50 border border-slate-600/50 text-slate-100 text-sm rounded-lg focus:ring-cyan-500 focus:border-cyan-500/50 block p-2.5 placeholder:text-slate-500"
placeholder="Wiki base URL (optional, e.g.: https://wiki.example.com)">
<input type="number" id="xml-dump-max-pages"
class="w-32 bg-slate-700/50 border border-slate-600/50 text-slate-100 text-sm rounded-lg focus:ring-cyan-500 focus:border-cyan-500/50 block p-2.5 placeholder:text-slate-500"
placeholder="Max pages (optional, for testing)">
<button onclick="uploadXMLDump()" id="xml-dump-upload-btn"
class="text-white bg-purple-600 hover:bg-purple-700 focus:ring-4 focus:ring-purple-300 font-medium rounded-lg text-sm px-5 py-2.5">Import Dump</button>
</div>
<!-- Upload Progress Bar (hidden by default) -->
<div id="xml-dump-progress-container" class="hidden mt-3">
<div class="flex items-center justify-between mb-1">
<span class="text-xs font-medium text-purple-400">Uploading...</span>
<span id="xml-dump-progress-percent" class="text-xs font-mono text-purple-400">0%</span>
</div>
<div class="w-full bg-slate-700/50 rounded-full h-2 overflow-hidden">
<div id="xml-dump-progress-bar" class="bg-gradient-to-r from-purple-600 to-purple-400 h-2 rounded-full transition-all duration-300" style="width: 0%"></div>
</div>
<div class="flex items-center justify-between mt-1">
<span id="xml-dump-progress-size" class="text-xs text-slate-500">0 KB / 0 KB</span>
<span id="xml-dump-progress-speed" class="text-xs text-slate-500">-- KB/s</span>
</div>
</div>
<div class="text-xs text-slate-400">
💡 Supports MediaWiki/Wikipedia XML Dump files (.xml, .xml.bz2, .xml.gz). Automatically parses pages and link relationships, no crawler needed.
</div>
</div>
</div>
<!-- URL Injection -->
<div class="bg-slate-800/50 backdrop-blur-sm p-6 rounded-xl border border-slate-700/50">
<h3 class="font-semibold mb-4 flex items-center">
<span
class="bg-blue-100 text-blue-800 text-xs font-medium mr-2 px-2.5 py-0.5 rounded">URL</span>
Crawl & Absorb
</h3>
<div class="space-y-2">
<input type="text" id="url-input"
class="w-full bg-slate-700/50 border border-slate-600/50 text-slate-100 text-sm rounded-lg focus:ring-cyan-500 focus:border-cyan-500/50 block p-2.5 placeholder:text-slate-500"
placeholder="https://tum.de/...">
<div class="flex gap-2">
<input type="password" id="url-password"
class="flex-1 bg-slate-700/50 border border-slate-600/50 text-slate-100 text-sm rounded-lg focus:ring-cyan-500 focus:border-cyan-500/50 block p-2.5 placeholder:text-slate-500"
placeholder="Enter password..."
onkeypress="if(event.key === 'Enter') uploadUrl()">
<button onclick="uploadUrl()"
class="text-white bg-blue-600 hover:bg-blue-700 focus:ring-4 focus:ring-blue-300 font-medium rounded-lg text-sm px-5 py-2.5">Inject</button>
</div>
</div>
</div>
<!-- Text Injection -->
<div class="bg-slate-800/50 backdrop-blur-sm p-6 rounded-xl border border-slate-700/50">
<h3 class="font-semibold mb-4 flex items-center text-slate-100">
<span
class="bg-slate-700/50 text-slate-300 text-xs font-medium mr-2 px-2.5 py-0.5 rounded">TEXT</span>
Direct Input
</h3>
<div class="flex gap-2">
<input type="text" id="text-input"
class="flex-1 bg-slate-700/50 border border-slate-600/50 text-slate-100 text-sm rounded-lg focus:ring-cyan-500 focus:border-cyan-500/50 block p-2.5 placeholder:text-slate-500"
placeholder="Paste knowledge snippet...">
<button onclick="uploadText()"
class="text-white bg-gray-700 hover:bg-gray-800 focus:ring-4 focus:ring-gray-300 font-medium rounded-lg text-sm px-5 py-2.5">Inject</button>
</div>
</div>
</div>
</div>
<!-- Trending Section -->
<div class="max-w-7xl mx-auto mb-16 px-4">
<h2 class="text-2xl font-bold text-slate-100 mb-6 flex items-center gap-2">
<span class="w-2 h-8 bg-orange-500 rounded-full"></span>
🔥 Trending Now
</h2>
<div id="trending-area" class="grid grid-cols-1 md:grid-cols-3 gap-6">
<!-- Trending items will be injected here -->
<div class="col-span-full text-center py-8 text-slate-400">Loading hot topics...</div>
</div>
</div>
<!-- Knowledge Feed Section -->
<div class="max-w-7xl mx-auto mb-16 px-4">
<h2 class="text-2xl font-bold text-slate-100 mb-6 flex items-center gap-2">
<span class="w-2 h-8 bg-blue-600 rounded-full"></span>
Recent Knowledge Ingestions
</h2>
<div id="feed-area" class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
<!-- Feed items will be injected here -->
<div class="col-span-full text-center py-8 text-slate-400">Loading feed...</div>
</div>
</div>
</main>
<!-- Notification Toast -->
<div id="notification-toast"
class="fixed bottom-5 right-5 bg-slate-900 text-white px-6 py-4 rounded-lg shadow-2xl transform translate-y-24 transition-transform duration-300 flex items-center gap-4 z-50 max-w-md">
<div class="flex-1">
<h4 class="font-bold text-sm text-blue-400 mb-1">System Update</h4>
<p class="text-xs text-slate-300" id="notification-msg">Network synchronized.</p>
</div>
<button onclick="hideNotification()" class="text-slate-500 hover:text-white"></button>
</div>
<script>
// ==========================================
// 3D PARTICLE NETWORK BACKGROUND
// ==========================================
// Ensure DOM is loaded before initializing particle effect
// Use IIFE, execute immediately if DOM is loaded, otherwise wait for DOMContentLoaded
(function initParticleNetwork() {
function startParticleNetwork() {
const canvas = document.getElementById('particle-canvas');
if (!canvas) {
console.error('Particle canvas not found!');
return;
}
try {
const ctx = canvas.getContext('2d');
if (!ctx) {
console.error('Failed to get 2D context from canvas');
return;
}
let animationFrameId = null;
let width = 0;
let height = 0;
let particles = [];
const particleCount = 60;
const connectionDistance = 150;
const mouseDistance = 200;
let mouse = { x: null, y: null };
let animationRunning = false;
const resize = () => {
// Get window dimensions
width = window.innerWidth || document.documentElement.clientWidth || 1920;
height = window.innerHeight || document.documentElement.clientHeight || 1080;
// Ensure dimensions are valid (at least 1)
if (width <= 0) width = 1920;
if (height <= 0) height = 1080;
// Set Canvas dimensions
canvas.width = width;
canvas.height = height;
console.log(`✅ Canvas resized to ${width}x${height}`);
// Only initialize if particles are empty
if (particles.length === 0) {
initParticles();
} else {
// If particles exist, reposition them to fit new dimensions
particles.forEach(p => {
p.x = Math.min(p.x, width);
p.y = Math.min(p.y, height);
});
}
};
class Particle {
constructor() {
// Use current actual width and height, or default values if not initialized
const w = width || window.innerWidth || 1920;
const h = height || window.innerHeight || 1080;
this.x = Math.random() * w;
this.y = Math.random() * h;
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 && distance > 0) {
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;
}
}
// Limit velocity
const maxSpeed = 2.0;
const speed = Math.sqrt(this.vx * this.vx + this.vy * this.vy);
if (speed > maxSpeed) {
this.vx = (this.vx / speed) * maxSpeed;
this.vy = (this.vy / speed) * maxSpeed;
}
}
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 = () => {
if (!ctx || !animationRunning) return;
if (width <= 0 || height <= 0 || particles.length === 0) {
console.warn('Animation skipped: invalid dimensions or no particles');
animationFrameId = requestAnimationFrame(animate);
return;
}
try {
ctx.clearRect(0, 0, width, height);
// Draw connection lines
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 && distance > 0) {
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();
}
}
}
// Update and draw particles
particles.forEach(particle => {
particle.update();
particle.draw();
});
animationFrameId = requestAnimationFrame(animate);
} catch (error) {
console.error('Error in animation loop:', error);
animationRunning = false;
}
};
const handleMouseMove = (e) => {
mouse.x = e.clientX || e.x;
mouse.y = e.clientY || e.y;
};
const handleMouseLeave = () => {
mouse.x = null;
mouse.y = null;
};
// Cleanup function
const cleanup = () => {
window.removeEventListener('resize', resize);
window.removeEventListener('mousemove', handleMouseMove);
window.removeEventListener('mouseleave', handleMouseLeave);
if (animationFrameId) {
cancelAnimationFrame(animationFrameId);
}
};
// Bind events
window.addEventListener('resize', resize);
window.addEventListener('mousemove', handleMouseMove);
window.addEventListener('mouseleave', handleMouseLeave);
// Initialize dimensions and particles
resize();
// Verify initialization success
if (particles.length === 0) {
console.error('Failed to initialize particles!');
return;
}
if (width <= 0 || height <= 0) {
console.error(`Invalid canvas dimensions: ${width}x${height}`);
return;
}
// Start animation loop
animationRunning = true;
setTimeout(() => {
animate();
console.log('✅ Particle network initialized successfully');
console.log(` Canvas: ${canvas.width}x${canvas.height}`);
console.log(` Particles: ${particles.length}`);
}, 150);
} catch (error) {
console.error('Error initializing particle network:', error);
}
}
// Prevent duplicate initialization
let isInitialized = false;
// Multiple checks to ensure Canvas element exists before execution
function tryInitParticleNetwork() {
// Prevent duplicate initialization
if (isInitialized) {
console.log('Particle network already initialized, skipping...');
return;
}
const canvas = document.getElementById('particle-canvas');
if (canvas) {
// Mark as initialized to avoid duplicates
isInitialized = true;
canvas.dataset.initialized = 'true';
startParticleNetwork();
} else {
// If Canvas is not ready yet, wait and retry (max 10 attempts)
const maxRetries = 10;
let retryCount = 0;
function retry() {
retryCount++;
const canvas = document.getElementById('particle-canvas');
if (canvas) {
isInitialized = true;
canvas.dataset.initialized = 'true';
startParticleNetwork();
} else if (retryCount < maxRetries) {
console.warn(`Canvas not found, retrying... (${retryCount}/${maxRetries})`);
setTimeout(retry, 200);
} else {
console.error('Failed to find Canvas element after multiple attempts!');
}
}
setTimeout(retry, 100);
}
}
// Choose initialization timing based on DOM state
if (document.readyState === 'loading') {
// DOM is still loading, wait for DOMContentLoaded
document.addEventListener('DOMContentLoaded', function() {
console.log('DOMContentLoaded event fired, initializing particle network...');
setTimeout(tryInitParticleNetwork, 100);
});
} else if (document.readyState === 'interactive' || document.readyState === 'complete') {
// DOM is loaded or fully loaded
console.log(`DOM ready state: ${document.readyState}, initializing particle network...`);
setTimeout(tryInitParticleNetwork, 100);
}
// As final fallback, listen to window.onload
window.addEventListener('load', function() {
if (!isInitialized) {
console.log('Window load event fired, attempting particle network initialization...');
setTimeout(tryInitParticleNetwork, 200);
}
});
})();
// ==========================================
// WebSocket for Real-time Updates
// ==========================================
const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
const ws = new WebSocket(`${protocol}//${window.location.host}/ws`);
const progressToast = document.getElementById('progress-toast');
const progressCount = document.getElementById('progress-count');
const progressDetail = document.getElementById('progress-detail');
let progressTimer;
const progressBarContainer = document.getElementById('progress-bar-container');
const progressBar = document.getElementById('progress-bar');
const progressBarIndeterminate = document.getElementById('progress-bar-indeterminate');
const progressTitle = document.getElementById('progress-title');
const progressUrl = document.getElementById('progress-url');
// WebSocket connection handlers
ws.onopen = function() {
console.log('✅ WebSocket connected successfully');
// 连接成功后,立即测试连接状态
fetch('/api/debug/websocket')
.then(r => r.json())
.then(data => {
console.log('📊 WebSocket debug info:', data);
if (data.active_connections === 0) {
console.error('⚠️ WARNING: No active connections reported by server!');
}
})
.catch(e => console.error('Failed to get debug info:', e));
};
ws.onerror = function(error) {
console.error('❌ WebSocket error:', error);
};
ws.onclose = function(event) {
console.log('⚠️ WebSocket closed:', event.code, event.reason);
};
ws.onmessage = function (event) {
try {
const data = JSON.parse(event.data);
console.log('WebSocket message received:', data);
if (data.type === 'progress') {
// Show toast - check if elements exist
if (!progressToast) {
console.error('Progress toast element not found!');
return;
}
progressToast.classList.remove('translate-y-24', 'opacity-0');
console.log('Processing progress message:', data);
// Check if this is URL crawling progress with detailed info
if (data.task_type === 'url') {
console.log('URL crawling progress detected, updating progress bar...');
// 即使没有total,也要显示进度条
const hasTotal = data.total !== undefined;
if (hasTotal) {
// Use determinate progress bar for URL crawling
if (progressBarContainer) progressBarContainer.classList.remove('hidden');
if (progressBarIndeterminate) progressBarIndeterminate.classList.add('hidden');
if (progressUrl) progressUrl.classList.remove('hidden');
// Update progress bar
if (progressBar) {
const percent = Math.min(data.percent || 0, 100);
progressBar.style.width = percent + '%';
}
// Update title and count
if (progressTitle) progressTitle.textContent = "Crawling URLs...";
if (progressCount) {
const percent = Math.min(data.percent || 0, 100);
progressCount.textContent = `${data.count || 0}/${data.total || 0} (${percent}%)`;
progressCount.className = "text-xs font-mono text-cyan-400 bg-cyan-500/20 px-2 py-0.5 rounded-full";
}
// Update detail and URL
if (progressDetail) progressDetail.textContent = data.message || `Processing page ${data.count || 0}`;
if (progressUrl && data.current_url) {
progressUrl.textContent = data.current_url;
}
} else {
// URL task but no total yet, show indeterminate with message
if (progressBarContainer) progressBarContainer.classList.add('hidden');
if (progressBarIndeterminate) progressBarIndeterminate.classList.remove('hidden');
if (progressUrl && data.current_url) {
progressUrl.textContent = data.current_url;
progressUrl.classList.remove('hidden');
}
if (progressTitle) progressTitle.textContent = "Starting Crawl...";
if (progressCount) progressCount.textContent = "Initializing...";
if (progressDetail) progressDetail.textContent = data.message || "Initializing crawler...";
}
} else {
// Use indeterminate progress bar for other tasks
if (progressBarContainer) progressBarContainer.classList.add('hidden');
if (progressBarIndeterminate) progressBarIndeterminate.classList.remove('hidden');
if (progressUrl) progressUrl.classList.add('hidden');
if (progressTitle) progressTitle.textContent = "Ingesting Knowledge...";
if (progressCount) progressCount.textContent = `${data.count || 0} items`;
if (progressDetail) progressDetail.textContent = data.details || data.message || "Processing...";
}
// Auto hide after inactivity
clearTimeout(progressTimer);
progressTimer = setTimeout(() => {
progressToast.classList.add('translate-y-24', 'opacity-0');
}, 5000); // Keep visible for 5s after last update
}
else if (data.type === 'system_update') {
// Show completion message
if (!progressToast) return;
progressToast.classList.remove('translate-y-24', 'opacity-0');
if (data.task_type === 'url') {
// For URL tasks, show final count
if (progressBarContainer) progressBarContainer.classList.remove('hidden');
if (progressBarIndeterminate) progressBarIndeterminate.classList.add('hidden');
if (progressBar) progressBar.style.width = '100%';
if (progressTitle) progressTitle.textContent = "Crawl Complete!";
if (progressCount) {
progressCount.textContent = "Done";
progressCount.className = "text-xs font-mono text-emerald-400 bg-emerald-500/20 px-2 py-0.5 rounded-full";
}
if (progressDetail) progressDetail.textContent = data.message || `Processed ${data.count || 0} pages`;
if (progressUrl) progressUrl.classList.add('hidden');
} else {
// For other tasks
if (progressBarContainer) progressBarContainer.classList.add('hidden');
if (progressBarIndeterminate) progressBarIndeterminate.classList.remove('hidden');
if (progressTitle) progressTitle.textContent = "Complete!";
if (progressCount) {
progressCount.textContent = "Done";
progressCount.className = "text-xs font-mono text-emerald-400 bg-emerald-500/20 px-2 py-0.5 rounded-full";
}
if (progressDetail) progressDetail.textContent = data.message || "Synchronization Complete.";
if (progressUrl) progressUrl.classList.add('hidden');
}
// Refresh feeds
loadFeed();
loadTrending();
// Hide after 5s
setTimeout(() => {
progressToast.classList.add('translate-y-24', 'opacity-0');
}, 5000);
}
else if (data.type === 'error') {
alert("System Error: " + data.message);
}
} catch (error) {
console.error('Error parsing WebSocket message:', error, event.data);
}
};
function showNotification(msg, type = "success") {
const toast = document.getElementById('notification-toast');
const msgElement = document.getElementById('notification-msg');
msgElement.textContent = msg;
// Set different styles based on type
if (type === "error") {
toast.classList.remove('bg-green-500', 'bg-blue-500');
toast.classList.add('bg-red-500');
} else {
toast.classList.remove('bg-red-500');
toast.classList.add('bg-green-500');
}
toast.classList.remove('translate-y-24');
setTimeout(hideNotification, 5000);
}
function hideNotification() {
document.getElementById('notification-toast').classList.add('translate-y-24');
}
async function recordInteraction(itemId, url) {
const formData = new FormData();
formData.append('item_id', itemId); // Use UUID for tracking
formData.append('action', 'click');
try {
await fetch('/api/feedback', {
method: 'POST',
body: formData,
keepalive: true
});
console.log("Interaction recorded for", itemId);
} catch (e) {
console.error("Failed to record interaction", e);
}
}
async function loadFeed() {
const feedDiv = document.getElementById('feed-area');
try {
const response = await fetch('/api/feed?limit=6');
const data = await response.json();
if (!data.points || data.points.length === 0) {
feedDiv.innerHTML = '<div class="col-span-full text-center py-8 text-slate-400">No knowledge found in the Commons.</div>';
return;
}
feedDiv.innerHTML = '';
data.points.forEach(pt => {
const p = pt.payload;
const card = document.createElement('div');
card.className = 'bg-slate-800/50 backdrop-blur-sm p-5 rounded-lg border border-slate-700/50 shadow-lg hover:shadow-xl hover:border-cyan-500/50 transition-all';
card.innerHTML = `
<div class="text-xs font-bold text-slate-400 mb-2 uppercase tracking-wider">${p.type || 'Unknown'}</div>
<a href="/view/${pt.id}" class="block text-base font-bold text-slate-100 hover:text-cyan-400 mb-2 line-clamp-1">${p.url || 'Untitled'}</a>
<div class="text-sm text-slate-400 line-clamp-3 mb-3">${p.content_preview || ''}</div>
<div class="text-xs text-slate-400 font-mono">ID: ${pt.id.substring(0, 6)}...</div>
`;
feedDiv.appendChild(card);
});
} catch (e) {
feedDiv.innerHTML = `<div class="col-span-full text-center text-red-400">Failed to load feed: ${e.message}</div>`;
}
}
let currentView = 'list';
let currentSearchResults = null;
let graphChart = null;
function switchTab(view) {
currentView = view;
const listTab = document.getElementById('tab-list');
const graphTab = document.getElementById('tab-graph');
const listView = document.getElementById('list-view');
const graphView = document.getElementById('graph-view');
if (view === 'list') {
listTab.className = 'px-6 py-3 text-sm font-semibold rounded-lg transition-all duration-200 bg-cyan-500/20 text-cyan-400 border-2 border-cyan-500/50 hover:bg-cyan-500/30 active-tab';
graphTab.className = 'px-6 py-3 text-sm font-semibold rounded-lg transition-all duration-200 bg-slate-800/50 text-slate-400 border-2 border-slate-700/50 hover:bg-slate-700/50 hover:text-cyan-400 hover:border-cyan-500/50';
listView.classList.remove('hidden');
graphView.classList.add('hidden');
} else {
listTab.className = 'px-6 py-3 text-sm font-semibold rounded-lg transition-all duration-200 bg-slate-800/50 text-slate-400 border-2 border-slate-700/50 hover:bg-slate-700/50 hover:text-cyan-400 hover:border-cyan-500/50';
graphTab.className = 'px-6 py-3 text-sm font-semibold rounded-lg transition-all duration-200 bg-cyan-500/20 text-cyan-400 border-2 border-cyan-500/50 hover:bg-cyan-500/30 active-tab';
listView.classList.add('hidden');
graphView.classList.remove('hidden');
// Render graph if we have search results
if (currentSearchResults) {
renderGraphView(currentSearchResults);
}
}
}
async function renderGraphView(query) {
const graphContainer = document.getElementById('graph-container');
// Show loading
graphContainer.innerHTML = '<div class="flex items-center justify-center h-full"><div class="text-center"><div class="animate-spin rounded-full h-12 w-12 border-b-2 border-cyan-600 mx-auto"></div><p class="mt-4 text-slate-400">Building neural network graph...</p></div></div>';
try {
const response = await fetch(`/api/search/graph?q=${encodeURIComponent(query)}`);
const graphData = await response.json();
if (!graphData.nodes || graphData.nodes.length === 0) {
graphContainer.innerHTML = '<div class="flex items-center justify-center h-full text-slate-400">No connections found. Try a different search.</div>';
return;
}
// Initialize ECharts
if (graphChart) {
graphChart.dispose();
}
graphChart = echarts.init(graphContainer);
// Prepare nodes and edges for ECharts
const nodes = graphData.nodes.map((node, idx) => ({
id: node.id,
name: node.name,
value: node.value || 30,
category: node.isCenter ? 0 : 1, // 0 = center, 1 = related
symbolSize: node.isCenter ? Math.max(40, node.value || 40) : Math.max(20, (node.value || 20) * 0.8),
itemStyle: {
color: node.isCenter ? '#06b6d4' : '#8b5cf6', // cyan for center, purple for related
borderColor: node.isCenter ? '#0891b2' : '#7c3aed',
borderWidth: 2,
shadowBlur: 10,
shadowColor: node.isCenter ? 'rgba(6, 182, 212, 0.5)' : 'rgba(139, 92, 246, 0.5)'
},
label: {
show: true,
fontSize: 12,
fontWeight: node.isCenter ? 'bold' : 'normal',
color: node.isCenter ? '#06b6d4' : '#cbd5e1',
formatter: (params) => {
const name = params.data.name || 'Node';
return name.length > 20 ? name.substring(0, 17) + '...' : name;
}
},
url: node.url,
content: node.content
}));
const edges = graphData.edges.map(edge => ({
source: edge.source,
target: edge.target,
value: edge.value || 1,
lineStyle: {
color: '#475569',
width: Math.max(1, edge.value * 2),
curveness: 0.3,
opacity: 0.6
},
label: {
show: false
}
}));
const option = {
backgroundColor: 'transparent',
tooltip: {
trigger: 'item',
formatter: (params) => {
if (params.dataType === 'node') {
return `
<div style="padding: 8px;">
<div style="font-weight: bold; color: #06b6d4; margin-bottom: 4px;">${params.data.name}</div>
${params.data.url ? `<div style="font-size: 11px; color: #94a3b8; margin-bottom: 4px;">${params.data.url}</div>` : ''}
${params.data.content ? `<div style="font-size: 11px; color: #cbd5e1; max-width: 300px;">${params.data.content}</div>` : ''}
</div>
`;
}
return '';
},
backgroundColor: 'rgba(15, 23, 42, 0.95)',
borderColor: '#06b6d4',
borderWidth: 1,
textStyle: {
color: '#e2e8f0'
}
},
series: [{
type: 'graph',
layout: 'force',
data: nodes,
links: edges,
categories: [
{ name: 'Center', itemStyle: { color: '#06b6d4' } },
{ name: 'Related', itemStyle: { color: '#8b5cf6' } }
],
roam: true,
label: {
show: true,
position: 'right',
formatter: '{b}'
},
labelLayout: {
hideOverlap: true
},
emphasis: {
focus: 'adjacency',
lineStyle: {
width: 4
}
},
force: {
repulsion: 200,
gravity: 0.1,
edgeLength: 150,
layoutAnimation: true
},
lineStyle: {
opacity: 0.6,
curveness: 0.3
}
}]
};
graphChart.setOption(option);
// Handle node click
graphChart.on('click', (params) => {
if (params.dataType === 'node' && params.data.url) {
recordInteraction(params.data.id, params.data.url);
window.location.href = `/view/${params.data.id}`;
}
});
// Handle window resize
window.addEventListener('resize', () => {
if (graphChart) {
graphChart.resize();
}
});
} catch (e) {
console.error('Graph render error:', e);
graphContainer.innerHTML = `<div class="flex items-center justify-center h-full text-red-400">Error rendering graph: ${e.message}</div>`;
}
}
async function performSearch() {
const query = document.getElementById('search-input').value;
if (!query) return;
const resultsSection = document.getElementById('results-section');
const resultsDiv = document.getElementById('results-area');
const introCards = document.getElementById('intro-cards');
// Hide intro cards and show results section
if (introCards) introCards.style.display = 'none';
if (resultsSection) {
resultsSection.classList.remove('hidden');
// Reset to list view
switchTab('list');
}
// Store query for graph view
currentSearchResults = query;
resultsDiv.innerHTML = '<div class="text-center py-12"><div class="animate-spin rounded-full h-12 w-12 border-b-2 border-blue-600 mx-auto"></div><p class="mt-4 text-slate-500">Traversing the neural pathways...</p></div>';
try {
const response = await fetch(`/api/search?q=${encodeURIComponent(query)}`);
const data = await response.json();
resultsDiv.innerHTML = '';
if (data.results.length === 0) {
resultsDiv.innerHTML = '<div class="text-center py-12 text-slate-500">No neural connections found. Try injecting more knowledge.</div>';
return;
}
data.results.forEach(item => {
const title = item.url || "Untitled Node";
const snippet = item.content || "No preview available.";
const score = (item.score * 100).toFixed(1);
// HTML escape helper function (define before use)
const escapeHtml = (text) => {
if (!text) return '';
const div = document.createElement('div');
div.textContent = text;
return div.innerHTML;
};
// Process highlighted snippet (if available)
let highlightedSnippet = snippet;
if (item.highlighted_snippet) {
// First replace highlight markers with temporary placeholders
const placeholderStart = '___HIGHLIGHT_START___';
const placeholderEnd = '___HIGHLIGHT_END___';
let textWithPlaceholders = item.highlighted_snippet
.replace(/\[\[HIGHLIGHT\]\](.*?)\[\[\/HIGHLIGHT\]\]/gi,
placeholderStart + '$1' + placeholderEnd);
let escapedText = escapeHtml(textWithPlaceholders);
// Replace placeholders with HTML tags
highlightedSnippet = escapedText
.replace(new RegExp(escapeHtml(placeholderStart), 'g'),
'<strong class="font-bold text-cyan-400 bg-cyan-500/20 px-1 rounded">')
.replace(new RegExp(escapeHtml(placeholderEnd), 'g'), '</strong>');
} else {
// If no highlighted snippet, still escape HTML
highlightedSnippet = escapeHtml(snippet);
}
const card = document.createElement('div');
card.className = 'bg-slate-800/50 backdrop-blur-sm p-6 rounded-xl shadow-lg border border-slate-700/50 hover:shadow-xl hover:border-cyan-500/50 transition-all hover:-translate-y-1 duration-200 cursor-pointer';
// Add click tracking
card.onclick = (e) => {
// Prevent default if it was a link click, we handle navigation
if (!e.target.closest('a')) {
recordInteraction(item.id, item.url);
window.location.href = `/view/${item.id}`;
}
};
// Create card elements
const titleLink = document.createElement('a');
titleLink.href = `/view/${item.id}`;
titleLink.className = 'text-lg font-bold text-cyan-400 hover:text-cyan-300 transition-colors line-clamp-1';
titleLink.textContent = title;
titleLink.onclick = (e) => {
e.stopPropagation();
recordInteraction(item.id, item.url);
};
const scoreBadge = document.createElement('span');
scoreBadge.className = 'bg-cyan-500/20 text-cyan-400 text-xs font-bold px-2 py-1 rounded-full border border-cyan-500/30';
scoreBadge.textContent = `${score}% Match`;
const headerDiv = document.createElement('div');
headerDiv.className = 'flex justify-between items-start mb-2';
headerDiv.appendChild(titleLink);
headerDiv.appendChild(scoreBadge);
const urlDiv = document.createElement('div');
urlDiv.className = 'text-xs text-slate-400 mb-3 font-mono truncate';
urlDiv.textContent = item.url;
const snippetPara = document.createElement('p');
snippetPara.className = 'text-slate-300 text-sm leading-relaxed line-clamp-3';
snippetPara.innerHTML = highlightedSnippet; // Directly use innerHTML to support HTML highlighting
const footerDiv = document.createElement('div');
footerDiv.className = 'mt-4 pt-4 border-t border-slate-700/50 flex justify-between items-center';
footerDiv.innerHTML = `
<span class="text-xs text-slate-400">ID: ${item.id.substring(0, 8)}...</span>
<span class="text-sm font-medium text-slate-100 flex items-center gap-1">
Explore Node
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M17 8l4 4m0 0l-4 4m4-4H3"></path></svg>
</span>
`;
card.appendChild(headerDiv);
card.appendChild(urlDiv);
card.appendChild(snippetPara);
card.appendChild(footerDiv);
resultsDiv.appendChild(card);
});
} catch (e) {
resultsDiv.innerHTML = `<div class="text-center text-red-500 py-8">Neural Link Error: ${e.message}</div>`;
}
// Also prepare graph data in background
if (currentView === 'graph') {
renderGraphView(query);
}
}
async function uploadUrl() {
const url = document.getElementById('url-input').value;
const password = document.getElementById('url-password').value;
if (!url) {
showNotification("Please enter URL", "error");
return;
}
if (!password) {
showNotification("Please enter password", "error");
document.getElementById('url-password').focus();
return;
}
try {
const formData = new FormData();
formData.append('url', url);
formData.append('password', password);
const response = await fetch('/api/upload/url', {
method: 'POST',
body: formData
});
const result = await response.json();
if (response.ok) {
document.getElementById('url-input').value = '';
document.getElementById('url-password').value = '';
// Immediately show progress bar when URL upload starts
const progressToast = document.getElementById('progress-toast');
const progressBarContainer = document.getElementById('progress-bar-container');
const progressBarIndeterminate = document.getElementById('progress-bar-indeterminate');
const progressTitle = document.getElementById('progress-title');
const progressCount = document.getElementById('progress-count');
const progressDetail = document.getElementById('progress-detail');
const progressUrl = document.getElementById('progress-url');
if (progressToast) {
progressToast.classList.remove('translate-y-24', 'opacity-0');
// Show indeterminate progress bar while waiting for WebSocket messages
if (progressBarContainer) progressBarContainer.classList.add('hidden');
if (progressBarIndeterminate) progressBarIndeterminate.classList.remove('hidden');
if (progressUrl) progressUrl.classList.add('hidden');
if (progressTitle) progressTitle.textContent = "Starting URL Crawl...";
if (progressCount) {
progressCount.textContent = "0/1000 (0%)";
progressCount.className = "text-xs font-mono text-cyan-400 bg-cyan-500/20 px-2 py-0.5 rounded-full";
}
if (progressDetail) progressDetail.textContent = "Waiting for crawler to start...";
console.log('Progress bar shown after URL upload');
} else {
console.error('Progress toast element not found!');
}
showNotification(result.message || "URL injected into the cortex. Processing...");
} else {
showNotification(result.detail || "Incorrect password, crawl rejected", "error");
}
} catch (error) {
console.error("Failed to upload URL", error);
showNotification("Upload failed: " + error.message, "error");
}
}
async function uploadText() {
const text = document.getElementById('text-input').value;
if (!text) return;
const formData = new FormData();
formData.append('text', text);
await fetch('/api/upload/text', { method: 'POST', body: formData });
document.getElementById('text-input').value = '';
showNotification("Text fragment absorbed. Processing...");
}
function formatBytes(bytes) {
if (bytes === 0) return '0 Bytes';
const k = 1024;
const sizes = ['Bytes', 'KB', 'MB', 'GB'];
const i = Math.floor(Math.log(bytes) / Math.log(k));
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
}
function uploadXMLDump() {
const fileInput = document.getElementById('xml-dump-file-input');
const baseUrl = document.getElementById('xml-dump-base-url').value;
const maxPages = document.getElementById('xml-dump-max-pages').value;
const uploadBtn = document.getElementById('xml-dump-upload-btn');
const progressContainer = document.getElementById('xml-dump-progress-container');
const progressBar = document.getElementById('xml-dump-progress-bar');
const progressPercent = document.getElementById('xml-dump-progress-percent');
const progressSize = document.getElementById('xml-dump-progress-size');
const progressSpeed = document.getElementById('xml-dump-progress-speed');
if (!fileInput.files || !fileInput.files[0]) {
showNotification("Please select an XML Dump file", "error");
return;
}
const file = fileInput.files[0];
// Check file extension
const validExtensions = ['.xml', '.bz2', '.gz'];
const fileName = file.name.toLowerCase();
const hasValidExtension = validExtensions.some(ext => fileName.endsWith(ext)) ||
fileName.endsWith('.xml.bz2') || fileName.endsWith('.xml.gz');
if (!hasValidExtension) {
showNotification("Only XML dump file formats are supported (.xml, .xml.bz2, .xml.gz)", "error");
return;
}
// Prepare form data
const formData = new FormData();
formData.append('file', file);
if (baseUrl) {
formData.append('base_url', baseUrl);
}
if (maxPages) {
formData.append('max_pages', maxPages);
}
// Setup progress tracking
const totalSize = file.size;
let uploadedSize = 0;
let startTime = Date.now();
let lastUploaded = 0;
let lastTime = startTime;
// Show progress bar
progressContainer.classList.remove('hidden');
progressBar.style.width = '0%';
progressPercent.textContent = '0%';
progressSize.textContent = `0 KB / ${formatBytes(totalSize)}`;
progressSpeed.textContent = '-- KB/s';
// Disable upload button
uploadBtn.disabled = true;
uploadBtn.textContent = 'Uploading...';
// Use XMLHttpRequest for upload progress support
const xhr = new XMLHttpRequest();
// Upload progress event
xhr.upload.addEventListener('progress', (e) => {
if (e.lengthComputable) {
uploadedSize = e.loaded;
const percent = Math.round((e.loaded / e.total) * 100);
// Update progress bar
progressBar.style.width = percent + '%';
progressPercent.textContent = percent + '%';
progressSize.textContent = `${formatBytes(e.loaded)} / ${formatBytes(e.total)}`;
// Calculate upload speed
const currentTime = Date.now();
const timeDelta = (currentTime - lastTime) / 1000; // seconds
if (timeDelta >= 0.5) { // Update speed every 500ms
const bytesDelta = e.loaded - lastUploaded;
const speed = bytesDelta / timeDelta; // bytes per second
progressSpeed.textContent = formatBytes(speed) + '/s';
lastUploaded = e.loaded;
lastTime = currentTime;
}
}
});
// Load event (upload complete)
xhr.addEventListener('load', () => {
if (xhr.status >= 200 && xhr.status < 300) {
try {
const result = JSON.parse(xhr.responseText);
showNotification(result.message || "XML Dump file received, parsing and importing...");
// Hide progress bar after a short delay
setTimeout(() => {
progressContainer.classList.add('hidden');
}, 1000);
// Clear form
fileInput.value = '';
document.getElementById('xml-dump-file-name').textContent = 'Select XML Dump file...';
document.getElementById('xml-dump-base-url').value = '';
document.getElementById('xml-dump-max-pages').value = '';
} catch (e) {
console.error("Failed to parse response", e);
showNotification("Upload completed but failed to parse response", "error");
progressContainer.classList.add('hidden');
}
} else {
try {
const result = JSON.parse(xhr.responseText);
showNotification(result.detail || "XML Dump import failed", "error");
} catch (e) {
showNotification("Upload failed with status " + xhr.status, "error");
}
progressContainer.classList.add('hidden');
}
uploadBtn.disabled = false;
uploadBtn.textContent = 'Import Dump';
});
// Error event
xhr.addEventListener('error', () => {
showNotification("Upload failed: Network error", "error");
progressContainer.classList.add('hidden');
uploadBtn.disabled = false;
uploadBtn.textContent = 'Import Dump';
});
// Abort event
xhr.addEventListener('abort', () => {
showNotification("Upload cancelled", "error");
progressContainer.classList.add('hidden');
uploadBtn.disabled = false;
uploadBtn.textContent = 'Import Dump';
});
// Send request
xhr.open('POST', '/api/upload/xml-dump');
xhr.send(formData);
}
// Enter key to search
document.getElementById('search-input').addEventListener('keypress', function (e) {
if (e.key === 'Enter') performSearch();
});
// XML Dump file input change handler
document.getElementById('xml-dump-file-input').addEventListener('change', function(e) {
const fileName = e.target.files[0] ? e.target.files[0].name : 'Select XML Dump file...';
document.getElementById('xml-dump-file-name').textContent = fileName;
});
// Load feed on start
loadFeed();
loadTrending();
async function loadTrending() {
const trendingDiv = document.getElementById('trending-area');
try {
const response = await fetch('/api/trending?limit=3');
const data = await response.json();
if (!data.results || data.results.length === 0) {
trendingDiv.innerHTML = '<div class="col-span-full text-center py-8 text-slate-400">No trending topics yet. Start exploring!</div>';
return;
}
trendingDiv.innerHTML = '';
data.results.forEach((item, index) => {
const p = item.payload;
const card = document.createElement('div');
card.className = 'bg-gradient-to-br from-orange-50 to-white p-5 rounded-xl border border-orange-100 shadow-sm hover:shadow-md transition-all relative overflow-hidden cursor-pointer';
card.onclick = () => window.location.href = `/view/${item.id}`;
card.innerHTML = `
<div class="absolute top-0 right-0 bg-orange-100 text-orange-600 text-xs font-bold px-3 py-1 rounded-bl-xl">#${index + 1}</div>
<div class="text-xs font-bold text-orange-400 mb-2 uppercase tracking-wider">Hot Topic</div>
<div class="text-lg font-bold text-slate-100 mb-2 line-clamp-1">${p.url || 'Untitled'}</div>
<div class="text-sm text-slate-500 line-clamp-2 mb-3">${p.content_preview || ''}</div>
<div class="flex items-center gap-2 text-xs text-slate-400 font-mono">
<span class="flex items-center gap-1 text-orange-500 font-bold">
<svg class="w-3 h-3" fill="currentColor" viewBox="0 0 20 20"><path d="M12.316 3.051a1 1 0 01.633 1.265l-4 12a1 1 0 11-1.898-.632l4-12a1 1 0 011.265-.633zM5.707 6.293a1 1 0 010 1.414L3.414 10l2.293 2.293a1 1 0 11-1.414 1.414l-3-3a1 1 0 010-1.414l3-3a1 1 0 011.414 0zm8.586 0a1 1 0 011.414 0l3 3a1 1 0 010 1.414l-3 3a1 1 0 11-1.414-1.414L16.586 10l-2.293-2.293a1 1 0 010-1.414z"/></svg>
${item.clicks} Clicks
</span>
</div>
`;
trendingDiv.appendChild(card);
});
} catch (e) {
trendingDiv.innerHTML = `<div class="col-span-full text-center text-red-400">Failed to load trending: ${e.message}</div>`;
}
}
</script>
</body>
</html>