Files
B.Tech-Project-III/thirdeye/dashboard/app/mission/page.tsx
2026-04-05 00:43:23 +05:30

349 lines
16 KiB
TypeScript

"use client";
import React, { useEffect, useState } from "react";
import Sidebar from "../components/Sidebar";
import TopBar from "../components/TopBar";
import {
fetchGroups,
fetchAllSignals,
fetchCrossGroupInsights,
Group,
Signal,
CrossGroupInsight,
formatRelativeTime,
getSignalIcon,
getSeverityColor,
parseMetaList,
} from "../lib/api";
function SignalCard({ signal, delay }: { signal: Signal; delay: number }) {
const meta = signal.metadata;
const icon = getSignalIcon(meta.type);
const color = getSeverityColor(meta.severity);
const time = formatRelativeTime(meta.timestamp);
return (
<div
className="neon-card-gradient rounded-xl p-6 relative border-l-[3px] border-t border-r border-b border-white/5 flex flex-col h-full group hover:brightness-110 hover:-translate-y-1 transition-all duration-300 shadow-md animate-fade-in-up opacity-0"
style={{ borderLeftColor: color, animationDelay: `${delay}ms` }}
>
<div className="flex justify-between items-start mb-6">
<div className="p-2.5 bg-[#A78BFA]/10 rounded-lg">
<span className="material-symbols-outlined text-xl" style={{ color }}>
{icon}
</span>
</div>
<span className="text-[10px] text-zinc-400 uppercase tracking-tighter">{time}</span>
</div>
<p className="text-[14px] font-medium text-zinc-200 leading-relaxed mb-8 flex-1">
{signal.document}
</p>
<div className="mt-auto flex items-center justify-between">
<span
className="text-[10px] font-bold uppercase tracking-[0.1em]"
style={{ color: "#A78BFA" }}
>
{meta.type.replace(/_/g, " ")}
</span>
<span className="material-symbols-outlined text-zinc-500 group-hover:text-[#A78BFA] transition-all cursor-pointer">
arrow_forward
</span>
</div>
</div>
);
}
function InsightHeroCard({ insight }: { insight: CrossGroupInsight }) {
const groupAName = insight.group_a?.name || insight.group_a?.group_id || "Group A";
const groupBName = insight.group_b?.name || insight.group_b?.group_id || "Group B";
return (
<section className="relative neon-card-gradient rounded-2xl border-l-[3px] border-[#A78BFA] primary-glow overflow-hidden animate-fade-in-up opacity-0 delay-200 shadow-2xl">
<div className="p-10 grid md:grid-cols-3 gap-10 relative z-10">
<div className="md:col-span-2 space-y-6">
<div className="flex items-center gap-4">
<span
className="px-3 py-1 bg-[#A78BFA]/20 text-[#A78BFA] text-[10px] font-bold tracking-[0.1em] rounded-full border border-[#A78BFA]/30"
>
{insight.severity.toUpperCase()}_ALERT
</span>
<span className="text-zinc-400 text-[11px] uppercase tracking-widest font-semibold">
Cross-Group Intelligence Analysis
</span>
</div>
<h2 className="text-3xl font-bold tracking-tight text-white leading-tight">
{insight.type.replace(/_/g, " ")}
</h2>
<p className="text-zinc-300 text-[15px] leading-relaxed max-w-2xl font-light">
{insight.description}
</p>
<div className="flex gap-4 pt-4">
<button className="px-6 py-3 bg-[#A78BFA] text-background text-[11px] font-bold uppercase tracking-widest rounded-lg hover:opacity-90 transition-all shadow-lg shadow-[#A78BFA]/20">
Schedule Sync
</button>
<button className="px-6 py-3 bg-white/5 border border-white/5 text-zinc-300 text-[11px] font-bold uppercase tracking-widest rounded-lg hover:bg-white/10 transition-all">
Dismiss Signal
</button>
</div>
</div>
<div className="space-y-6">
<div className="bg-black/20 p-5 rounded-xl border border-white/5">
<div className="flex justify-between items-center mb-3">
<span className="text-[11px] text-zinc-300 font-semibold">{groupAName}</span>
</div>
<p className="text-[10px] text-zinc-400 mt-3 italic font-light tracking-wide">
{insight.group_a?.evidence || "Evidence collected"}
</p>
</div>
<div className="bg-black/20 p-5 rounded-xl border border-white/5">
<div className="flex justify-between items-center mb-3">
<span className="text-[11px] text-zinc-300 font-semibold">{groupBName}</span>
</div>
<p className="text-[10px] text-zinc-400 mt-3 italic font-light tracking-wide">
{insight.group_b?.evidence || "Evidence collected"}
</p>
</div>
{insight.recommendation && (
<div className="p-5 border border-[#A78BFA]/20 bg-[#A78BFA]/10 rounded-xl">
<p className="text-[12px] text-[#A78BFA] font-medium leading-relaxed">
<span className="font-bold">RECOMMENDATION:</span> {insight.recommendation}
</p>
</div>
)}
</div>
</div>
<div className="absolute right-0 top-0 h-full w-96 opacity-10 pointer-events-none bg-gradient-to-l from-[#A78BFA] to-transparent" />
</section>
);
}
export default function MissionDashboard() {
const [groups, setGroups] = useState<Group[]>([]);
const [signals, setSignals] = useState<Signal[]>([]);
const [insights, setInsights] = useState<CrossGroupInsight[]>([]);
const [loading, setLoading] = useState(true);
const [insightsLoading, setInsightsLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
useEffect(() => {
const ctrl = new AbortController();
const timer = setTimeout(() => ctrl.abort(), 12000);
async function loadCore() {
setLoading(true);
setError(null);
try {
const [grps, allGroupSignals] = await Promise.all([
fetchGroups(),
fetchAllSignals(),
]);
setGroups(grps);
const flat = allGroupSignals
.flatMap((g) => g.signals)
.sort(
(a, b) =>
new Date(b.metadata.timestamp).getTime() -
new Date(a.metadata.timestamp).getTime()
)
.slice(0, 16);
setSignals(flat);
} catch (e) {
if ((e as Error)?.name !== "AbortError") {
setError("Backend unavailable — check that the API server is running.");
}
} finally {
clearTimeout(timer);
setLoading(false);
}
}
async function loadInsights() {
setInsightsLoading(true);
try {
const cgi = await fetchCrossGroupInsights();
setInsights(cgi);
} catch {
// Non-fatal — insights just won't show
} finally {
setInsightsLoading(false);
}
}
loadCore();
loadInsights();
return () => {
ctrl.abort();
clearTimeout(timer);
};
}, []);
const totalSignals = groups.reduce((acc, g) => acc + g.signal_count, 0);
const criticalSignals = signals.filter(
(s) => s.metadata.severity === "critical" || s.metadata.severity === "high"
).length;
const criticalInsights = insights.filter((i) => i.severity === "critical").length;
return (
<div className="flex h-screen w-full bg-[#09090B] text-white font-['Poppins'] overflow-hidden selection:bg-[#A78BFA]/30 relative">
<Sidebar />
<TopBar />
<main className="absolute left-[240px] top-20 right-0 bottom-0 overflow-y-auto custom-scrollbar bg-[#09090B] z-10 flex flex-col">
<div className="p-10 space-y-10 animate-fade-in-up opacity-0 delay-100">
{/* Status Banner */}
{error && (
<div className="px-5 py-3 rounded-xl border border-yellow-500/20 bg-yellow-500/5 text-yellow-400 text-[11px] font-mono flex items-center gap-3">
<span className="material-symbols-outlined text-sm">warning</span>
{error}
</div>
)}
{/* Metric Tiles */}
<section className="grid grid-cols-4 gap-6">
<div className="neon-card-gradient border border-white/5 p-6 rounded-xl flex flex-col justify-between hover:brightness-110 transition-all shadow-lg animate-fade-in-up opacity-0 delay-100">
<div className="flex justify-between items-start mb-4">
<span className="text-[10px] uppercase tracking-[0.15em] font-semibold text-zinc-400">Monitored Groups</span>
<span className="material-symbols-outlined text-[#A78BFA]/60 text-sm">sensors</span>
</div>
<div className="text-3xl font-semibold text-white">
{loading ? "—" : groups.length}
</div>
<div className="text-[11px] text-[#A78BFA] mt-3 flex items-center gap-1.5 font-medium">
<span className="material-symbols-outlined text-[11px]">trending_up</span>
<span>Active Streams</span>
</div>
</div>
<div className="neon-card-gradient border border-white/5 p-6 rounded-xl flex flex-col justify-between hover:brightness-110 transition-all shadow-lg animate-fade-in-up opacity-0 delay-150">
<div className="flex justify-between items-start mb-4">
<span className="text-[10px] uppercase tracking-[0.15em] font-semibold text-zinc-400">Signals Processed</span>
<span className="material-symbols-outlined text-[#A78BFA]/60 text-sm">data_exploration</span>
</div>
<div className="text-3xl font-semibold text-white">
{loading ? "—" : totalSignals >= 1000 ? `${(totalSignals / 1000).toFixed(1)}k` : totalSignals}
</div>
<div className="text-[11px] text-zinc-400 mt-3 uppercase tracking-tighter">Total Indexed</div>
</div>
<div className="neon-card-gradient border border-white/5 p-6 rounded-xl flex flex-col justify-between hover:brightness-110 transition-all shadow-lg animate-fade-in-up opacity-0 delay-200">
<div className="flex justify-between items-start mb-4">
<span className="text-[10px] uppercase tracking-[0.15em] font-semibold text-zinc-400">Open Insights</span>
<span className="material-symbols-outlined text-[#A78BFA]/60 text-sm">lightbulb</span>
</div>
<div className="text-3xl font-semibold text-white">
{loading ? "—" : insights.length}
</div>
{criticalInsights > 0 && (
<div className="text-[11px] text-[#ff6f78] mt-3 font-semibold uppercase tracking-tighter">
{criticalInsights} Critical Priority
</div>
)}
{criticalInsights === 0 && !loading && (
<div className="text-[11px] text-zinc-400 mt-3 uppercase tracking-tighter">All Clear</div>
)}
</div>
<div className="neon-card-gradient border border-white/5 p-6 rounded-xl flex flex-col justify-between hover:brightness-110 transition-all shadow-lg animate-fade-in-up opacity-0 delay-300">
<div className="flex justify-between items-start mb-4">
<span className="text-[10px] uppercase tracking-[0.15em] font-semibold text-zinc-400">High Priority</span>
<span className="material-symbols-outlined text-[#A78BFA]/60 text-sm">priority_high</span>
</div>
<div className="text-3xl font-semibold text-white">
{loading ? "—" : criticalSignals}
</div>
<div className="text-[11px] text-zinc-400 mt-3 uppercase tracking-tighter">
{criticalSignals > 0 ? "Needs Attention" : "Optimal Range"}
</div>
</div>
</section>
{/* Hero Insight */}
{insightsLoading && !loading && (
<section className="relative neon-card-gradient rounded-2xl border border-white/5 p-10 flex items-center gap-3 text-zinc-600 animate-fade-in-up opacity-0 delay-200">
<span className="material-symbols-outlined animate-spin text-sm">autorenew</span>
<span className="text-[11px] uppercase tracking-widest">Analysing cross-group patterns...</span>
</section>
)}
{!insightsLoading && insights.length > 0 && (
<InsightHeroCard insight={insights[0]} />
)}
{!insightsLoading && insights.length === 0 && groups.length >= 2 && (
<section className="relative neon-card-gradient rounded-2xl border border-white/5 p-10 text-center animate-fade-in-up opacity-0 delay-200">
<span className="material-symbols-outlined text-[#A78BFA] text-4xl mb-4 block">hub</span>
<p className="text-zinc-400 text-sm">No cross-group insights yet. Signals are accumulating...</p>
</section>
)}
{!loading && groups.length < 2 && (
<section className="relative neon-card-gradient rounded-2xl border border-white/5 p-10 text-center animate-fade-in-up opacity-0 delay-200">
<span className="material-symbols-outlined text-zinc-600 text-4xl mb-4 block">hub</span>
<p className="text-zinc-500 text-sm">Cross-group analysis requires at least 2 monitored groups.</p>
</section>
)}
{/* Live Signals Stream */}
<section className="space-y-6 animate-fade-in-up opacity-0 delay-300 mb-10">
<div className="flex items-center justify-between">
<h3 className="text-[11px] uppercase tracking-[0.25em] font-bold text-zinc-500 flex items-center gap-3">
<span className="w-2.5 h-2.5 rounded-full bg-[#A78BFA] primary-glow" />
Live Signals Stream
{!loading && (
<span className="text-zinc-700 normal-case tracking-normal font-normal">
({signals.length} signals)
</span>
)}
</h3>
<div className="flex gap-4">
<span className="material-symbols-outlined text-zinc-600 text-lg cursor-pointer hover:text-[#A78BFA] transition-colors">filter_list</span>
<span className="material-symbols-outlined text-zinc-600 text-lg cursor-pointer hover:text-[#A78BFA] transition-colors">sort</span>
</div>
</div>
{loading && (
<div className="flex items-center justify-center py-20 text-zinc-600">
<span className="material-symbols-outlined animate-spin mr-3">autorenew</span>
Loading signals...
</div>
)}
{!loading && signals.length === 0 && (
<div className="text-center py-20 text-zinc-600">
<span className="material-symbols-outlined text-4xl mb-3 block">inbox</span>
No signals yet. Connect Telegram groups to start receiving intelligence.
</div>
)}
{!loading && signals.length > 0 && (
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6 pb-12">
{signals.map((signal, idx) => (
<SignalCard key={signal.id} signal={signal} delay={idx * 50} />
))}
</div>
)}
</section>
{/* Intelligence Ticker */}
<footer className="bg-[#0C0C0E] p-4 rounded-xl border border-white/5 overflow-hidden shadow-lg mb-8 animate-fade-in-up opacity-0 delay-400">
<div className="flex items-center gap-6 whitespace-nowrap overflow-hidden">
<span className="text-[10px] font-extrabold text-[#A78BFA] uppercase tracking-widest bg-[#A78BFA]/10 px-3 py-1 rounded-full border border-[#A78BFA]/20 z-10 relative shadow-[0_0_15px_rgba(167,139,250,0.2)]">
System_Log
</span>
<div className="flex gap-12 text-[10px] font-medium text-zinc-500 uppercase tracking-wide opacity-80 ticker-track">
<span>[Signal_Rcv] :: Groups={groups.length} :: Status=Active</span>
<span>[Signal_Count] :: Total={totalSignals} :: Indexed</span>
<span>[Insight_Engine] :: CrossGroup_Analysis_Running</span>
<span>[Health] :: API=Online :: DB=Connected</span>
<span>[Signal_Rcv] :: Groups={groups.length} :: Status=Active</span>
<span>[Signal_Count] :: Total={totalSignals} :: Indexed</span>
<span>[Insight_Engine] :: CrossGroup_Analysis_Running</span>
<span>[Health] :: API=Online :: DB=Connected</span>
</div>
</div>
</footer>
</div>
</main>
</div>
);
}