diff --git a/services/frontend/src/app/(dashboard)/analysis/view.tsx b/services/frontend/src/app/(dashboard)/analysis/view.tsx index 1b6dc1c2..73236a61 100644 --- a/services/frontend/src/app/(dashboard)/analysis/view.tsx +++ b/services/frontend/src/app/(dashboard)/analysis/view.tsx @@ -1,7 +1,9 @@ "use client"; +import { useGSAP } from "@gsap/react"; +import gsap from "gsap"; import { Hash, Search, Sparkles, TrendingUp } from "lucide-react"; -import { useEffect, useState } from "react"; +import { useEffect, useRef, useState } from "react"; import { useAmbient } from "@/components/ambient/ambient-context"; import { Avatar, Badge, GlassPanel, Input } from "@/components/primitives"; import { EmptyState, SectionHeader, SkeletonRows } from "@/components/shared"; @@ -13,6 +15,10 @@ import { renderMessageContent, } from "@/lib/format"; +if (typeof window !== "undefined") { + gsap.registerPlugin(useGSAP); +} + export function AnalysisView() { const [query, setQuery] = useState(""); const search = useMessageSearch(query, query.trim().length >= 2); @@ -20,6 +26,9 @@ export function AnalysisView() { const { data: channels } = useChannels(); const ambient = useAmbient(); + const reactorsRef = useRef(null); + const channelsRef = useRef(null); + useEffect(() => { ambient.set( query ? "amber" : "signal", @@ -28,8 +37,96 @@ export function AnalysisView() { ); }, [query, ambient]); + // Count-up + bar-fill reveal for the reactor leaderboard — HUD gauge motif. + useGSAP( + () => { + if (!reactorsRef.current) return; + const bars = reactorsRef.current.querySelectorAll(".reactor-bar-fill"); + const counters = reactorsRef.current.querySelectorAll(".reactor-count"); + if (bars.length === 0) return; + const prefersReduced = window.matchMedia( + "(prefers-reduced-motion: reduce)", + ).matches; + if (prefersReduced) { + gsap.set(bars, { scaleX: 1 }); + return; + } + gsap.fromTo( + bars, + { scaleX: 0 }, + { + scaleX: 1, + duration: 0.7, + stagger: 0.06, + ease: "power2.out", + transformOrigin: "left center", + }, + ); + counters.forEach((el) => { + const target = Number(el.getAttribute("data-target") ?? "0"); + const obj = { val: 0 }; + gsap.to(obj, { + val: target, + duration: 0.8, + ease: "power2.out", + onUpdate: () => { + el.textContent = `+${Math.round(obj.val)}`; + }, + }); + }); + }, + { scope: reactorsRef, dependencies: [reactors] }, + ); + + useGSAP( + () => { + if (!channelsRef.current) return; + const rows = channelsRef.current.querySelectorAll(".channel-row"); + if (rows.length === 0) return; + const prefersReduced = window.matchMedia( + "(prefers-reduced-motion: reduce)", + ).matches; + if (prefersReduced) { + gsap.set(rows, { opacity: 1, x: 0 }); + return; + } + gsap.fromTo( + rows, + { opacity: 0, x: -10 }, + { + opacity: 1, + x: 0, + duration: 0.35, + stagger: 0.04, + ease: "power2.out", + clearProps: "transform", + }, + ); + }, + { scope: channelsRef, dependencies: [channels] }, + ); + return ( -
+
+ {/* Tactical HUD Header Bar */} +
+
+
+ + +
+

+ ANALYSIS ENGINE · DEEP_SCAN +

+
+
+ MODE: + + {query.trim().length >= 2 ? "SEARCHING" : "IDLE"} + +
+
+
@@ -123,7 +220,7 @@ export function AnalysisView() { } /> -
+
{(reactors ?? []).slice(0, 6).map((r, i) => { const maxNet = reactors?.[0]?.net_count || 1; const pct = Math.max( @@ -141,12 +238,15 @@ export function AnalysisView() {
- - +{r.net_count} + + +0
); @@ -168,11 +268,11 @@ export function AnalysisView() { } /> -
+
{(channels ?? []).slice(0, 6).map((c) => (
{c.channel_name ?? c.channel_id.slice(0, 8)}