mirror of
https://github.com/arkorty/B.Tech-Project-III.git
synced 2026-04-19 20:51:49 +00:00
349 lines
16 KiB
TypeScript
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>
|
|
);
|
|
}
|