diff --git a/services/frontend/src/app/(dashboard)/analysis/view.tsx b/services/frontend/src/app/(dashboard)/analysis/view.tsx index 73236a61..981ecd30 100644 --- a/services/frontend/src/app/(dashboard)/analysis/view.tsx +++ b/services/frontend/src/app/(dashboard)/analysis/view.tsx @@ -8,6 +8,7 @@ import { useAmbient } from "@/components/ambient/ambient-context"; import { Avatar, Badge, GlassPanel, Input } from "@/components/primitives"; import { EmptyState, SectionHeader, SkeletonRows } from "@/components/shared"; import { useChannels, useMessageSearch, useTopReactors } from "@/hooks"; +import { useStaggerReveal } from "@/hooks/use-gsap-animation"; import { aiTone } from "@/lib/ai-status"; import { formatDuration, @@ -29,6 +30,12 @@ export function AnalysisView() { const reactorsRef = useRef(null); const channelsRef = useRef(null); + const resultsRef = useStaggerReveal(".search-result-card", { + stagger: 0.03, + y: 8, + dependencies: [search.data?.length, query], + }); + useEffect(() => { ambient.set( query ? "amber" : "signal", @@ -109,90 +116,90 @@ export function AnalysisView() { return (
{/* Tactical HUD Header Bar */} -
-
-
- - -
-

- ANALYSIS ENGINE · DEEP_SCAN +
+
+ +

+ Deep Scan · Semantic Search & Telemetry Analysis

-
- MODE: - - {query.trim().length >= 2 ? "SEARCHING" : "IDLE"} +
+ ENGINE: + + {query.trim().length >= 2 ? "SCANNING" : "STANDBY"}
- -
+ {/* Hero Query Stage */} +
- + + +
-
Semantic search
-

- Search the archive +

+ Cross-Guild Archive Intelligence

+

+ Query vectorized messages, heuristic flags, and user behavior +

- + setQuery(e.target.value)} autoFocus />
- {query.trim().length > 0 && query.trim().length < 2 && ( -
- Type at least 2 characters… -
- )}
-
+
+ {/* Search Results Column */} - {(search.data ?? []).length} + + {(search.data ?? []).length} results } /> {query.trim().length >= 2 && search.isLoading && ( )} - {(search.data ?? []).length === 0 ? ( + {(search.data ?? []).length === 0 && !search.isLoading ? ( } - title="No matches yet" - description="Run a search to surface messages across the guild." + title="No packets matched" + description="Enter a query string above to scan across all captured guild communication." /> ) : ( -
+
{(search.data ?? []).map((m) => (
- + {m.username} - + {getMessageChannelLabel(m)} {m.ai_status && ( - + {m.ai_analysis_duration_ms && m.ai_analysis_duration_ms > 0 ? `${m.ai_status} · ${formatDuration(m.ai_analysis_duration_ms)}` @@ -200,7 +207,7 @@ export function AnalysisView() { )}
-
+
{renderMessageContent(m.content, m.metadata) || "(embed)"}
@@ -210,17 +217,19 @@ export function AnalysisView() { )} -
+ {/* Culture & Leaderboard Side Column */} +
- Top reactors + + Top + Reactors } /> -
+
{(reactors ?? []).slice(0, 6).map((r, i) => { const maxNet = reactors?.[0]?.net_count || 1; const pct = Math.max( @@ -230,20 +239,22 @@ export function AnalysisView() { return (
- {i + 1} - + + 0{i + 1} + + {r.username} -
+
+0 @@ -252,8 +263,8 @@ export function AnalysisView() { ); })} {(reactors ?? []).length === 0 && ( -
- No data +
+ NO REACTOR DATA
)}
@@ -261,30 +272,32 @@ export function AnalysisView() { - Top channels + + Top Active + Channels } /> -
+
{(channels ?? []).slice(0, 6).map((c) => (
- - {c.channel_name ?? c.channel_id.slice(0, 8)} + + + {c.channel_name ?? c.channel_id.slice(0, 10)} - - {c.total_messages} + + {c.total_messages.toLocaleString()} msgs
))} {(channels ?? []).length === 0 && ( -
- No data +
+ NO CHANNEL DATA
)}
diff --git a/services/frontend/src/app/(dashboard)/dashboard/view.tsx b/services/frontend/src/app/(dashboard)/dashboard/view.tsx index db76f3ad..cd0674ab 100644 --- a/services/frontend/src/app/(dashboard)/dashboard/view.tsx +++ b/services/frontend/src/app/(dashboard)/dashboard/view.tsx @@ -3,7 +3,6 @@ import { AudioWaveform, ChevronRight, - Flame, MessageSquare, Mic, Shield, @@ -177,13 +176,17 @@ export function DashboardView({
-
CLEAN
+
+ CLEAN +
{formatNumber(s.total_clean)}
-
FLAGGED
+
+ FLAGGED +
{formatNumber(s.total_flagged)}
@@ -220,10 +223,7 @@ export function DashboardView({
- +
loop.mutateAsync()} - className={media?.loop ? "border-[#7170ff] text-[#7170ff]" : ""} + className={ + media?.loop ? "border-[#7170ff] text-[#7170ff]" : "" + } > diff --git a/services/frontend/src/app/(dashboard)/messages/view.tsx b/services/frontend/src/app/(dashboard)/messages/view.tsx index 972b20cc..53ec9395 100644 --- a/services/frontend/src/app/(dashboard)/messages/view.tsx +++ b/services/frontend/src/app/(dashboard)/messages/view.tsx @@ -10,6 +10,7 @@ import { Paperclip, Search, ShieldAlert, + Sparkles, } from "lucide-react"; import { useCallback, useEffect, useMemo, useRef, useState } from "react"; import { ActivityHeatmap } from "@/components/ActivityHeatmap"; @@ -42,6 +43,7 @@ import { useReviewWsSync, useSemanticSearch, } from "@/hooks"; +import { useStaggerReveal } from "@/hooks/use-gsap-animation"; import { aiTone } from "@/lib/ai-status"; import { formatBytes, @@ -57,7 +59,6 @@ import type { Guild, MessageRecord, } from "@/lib/types"; -import { staggerDelay } from "@/lib/utils"; import { useWebSocket } from "@/lib/ws/context"; export function MessagesView({ @@ -217,28 +218,32 @@ export function MessagesView({ prevLen.current = list.length; }, [list.length, searching]); + const streamRef = useStaggerReveal(".msg-feed-card", { + stagger: 0.02, + y: 6, + dependencies: [display.length, viewMode], + }); + return (
{/* Tactical HUD Header Bar */} -
-
-
- - -
-

- CHAT LOG STREAM · SCAN_01 +
+
+ +

+ Chat Log Stream · Ingestion Stream

-
- FEED: - - {searching ? "SEARCH" : viewMode.toUpperCase()} +
+ MODE: + + {searching ? "SEARCH_ACTIVE" : viewMode.toUpperCase()}
- + {/* Filter and Mode Bar */} + setQuery(e.target.value)} />
- - +
+ + +
+ +
+ + +
-
+
{semanticSearching && ( + {semantic.data?.length ?? 0} matches } @@ -308,19 +337,18 @@ export function MessagesView({ {semantic.data.map((r, i) => (
- - {(r.score * 100).toFixed(0)}% + + {(r.score * 100).toFixed(0)}% RELEVANCE - + {formatRelativeTime(r.created_at)}
-
+
{r.content}
@@ -331,19 +359,20 @@ export function MessagesView({ } title="No semantic matches" - description="Try different wording — semantic search finds meaning, not exact text." + description="Try different phrasing — vector search inspects contextual semantics." /> )} )} + {/* Message Stream Deck */} - {list.length} shown + + {list.length} messages } /> @@ -354,48 +383,40 @@ export function MessagesView({ ) : list.length === 0 ? ( } - title="No messages" - description="Pick a guild to begin, or run a search." + title="No messages captured" + description="Select a channel or verify discord bridge is active." /> ) : (
{!searching && (
{loadMore.isPending ? ( - + - Loading older… + FETCHING EARLIER PACKETS... ) : hasMore && loadedPages < MAX_OLDER_PAGES ? ( - ) : loadedPages >= MAX_OLDER_PAGES ? ( - - capped at {MAX_OLDER_PAGES} older pages · use search for - more - ) : ( - messages && - messages.length > 0 && ( - - beginning of history - - ) + + {loadedPages >= MAX_OLDER_PAGES + ? `CAPPED AT ${MAX_OLDER_PAGES} PAGES` + : "STREAM ROOT REACHED"} + )}
)}
{ const el = e.currentTarget; - // Track whether the user is near the bottom (to follow live - // messages) and auto-load older messages when scrolled to top. nearBottomRef.current = el.scrollHeight - el.scrollTop - el.clientHeight < 120; if (searching || !hasMore || loadMore.isPending) return; @@ -405,44 +426,47 @@ export function MessagesView({ } }} > - {viewMode === "timeline" && timelineNodes - ? timelineNodes.map((node, _i) => - node.type === "date" ? ( -
- - {node.label} -
- ) : ( +
+ {viewMode === "timeline" && timelineNodes + ? timelineNodes.map((node) => + node.type === "date" ? ( +
+ + {node.label} +
+ ) : ( + + ), + ) + : display.map((m) => ( - ), - ) - : display.map((m, _i) => ( - - ))} + ))} +
)}
+ {/* Message Inspector Detail Panel */} - + {!selected ? ( ) : detail.loading ? (
@@ -455,7 +479,7 @@ export function MessagesView({ attachments={detail.attachments} /> ) : ( - + )}
@@ -530,15 +554,15 @@ function MessageDetail({
-
+
{renderMessageContent(m.edited_content ?? m.content, m.metadata) || - "(no text)"} + "(no text content)"}
{m.ai_analysis && (
-
AI analysis
-
+
AI heuristic reasoning
+
{m.ai_analysis}
@@ -571,11 +595,11 @@ function MessageDetail({ href={a.discord_url ?? a.uploaded_url ?? "#"} target="_blank" rel="noreferrer" - className="flex items-center gap-2 rounded-[10px] border border-hairline bg-white/5 px-3 py-2 text-xs text-ink-soft hover:text-ink" + className="flex items-center gap-2 rounded-[6px] border border-white/[0.06] bg-white/[0.02] px-3 py-2 text-xs text-[#d0d6e0] hover:border-white/[0.12] hover:text-[#f7f8f8]" > - + {a.filename} - + {formatBytes(a.size)} @@ -602,28 +626,28 @@ function MessageRow({ key={m.id} type="button" onClick={() => onSelect(m.id)} - className={`animate-stagger flex w-full items-start gap-3 rounded-[12px] border p-3 text-left transition-colors ${ + className={`msg-feed-card flex w-full items-start gap-3 rounded-[6px] border p-2.5 text-left transition-all ${ selected === m.id - ? "border-signal/40 bg-signal/8" - : "border-hairline bg-white/[0.03] hover:bg-white/[0.06]" + ? "border-[#7170ff]/40 bg-[#7170ff]/10" + : "border-white/[0.06] bg-white/[0.02] hover:border-white/[0.12] hover:bg-white/[0.04]" }`} > - +
- + {m.username} - + {getMessageChannelLabel(m)} - + {formatRelativeTime(m.created_at)}
-
+
{renderMessageContent(m.content, m.metadata) || ( - (empty / embed) + (empty / embed) )}
diff --git a/services/frontend/src/app/(dashboard)/moderation/view.tsx b/services/frontend/src/app/(dashboard)/moderation/view.tsx index 5eee8f44..bf25db17 100644 --- a/services/frontend/src/app/(dashboard)/moderation/view.tsx +++ b/services/frontend/src/app/(dashboard)/moderation/view.tsx @@ -1,19 +1,13 @@ "use client"; -import { - AlertTriangle, - CheckCircle2, - Filter, - Shield, - ShieldAlert, -} from "lucide-react"; -import { useEffect, useState } from "react"; import { useGSAP } from "@gsap/react"; import gsap from "gsap"; +import { AlertTriangle, CheckCircle2, Shield } from "lucide-react"; +import { useEffect, useState } from "react"; import { useAmbient } from "@/components/ambient/ambient-context"; import { LiveModerationFeed } from "@/components/LiveModerationFeed"; import { ModerationHeatmap } from "@/components/ModerationHeatmap"; -import { Button, GlassPanel } from "@/components/primitives"; +import { GlassPanel } from "@/components/primitives"; import { ErrorState, MetricTile, @@ -25,9 +19,7 @@ import { import { useHourlyModeration, useModerationActions, - useModerationCoverage, useModerationStats, - useModerationTrends, } from "@/hooks"; import { useStaggerReveal } from "@/hooks/use-gsap-animation"; import { formatNumber } from "@/lib/format"; @@ -44,7 +36,7 @@ if (typeof window !== "undefined") { export function ModerationView({ initialStats, initialActions, - initialCoverage, + initialCoverage: _initialCoverage, }: { initialStats?: ModerationStats; initialActions?: ModerationAction[]; @@ -62,8 +54,6 @@ export function ModerationView({ initialActions, ); const { data: hourly } = useHourlyModeration(); - const { data: trends } = useModerationTrends(14); - const { data: coverage } = useModerationCoverage(30); const ambient = useAmbient(); const [filterMode, setFilterMode] = useState<"all" | "flagged" | "clean">( diff --git a/services/frontend/src/app/(dashboard)/recordings/view.tsx b/services/frontend/src/app/(dashboard)/recordings/view.tsx index c7400bd3..8551cd3b 100644 --- a/services/frontend/src/app/(dashboard)/recordings/view.tsx +++ b/services/frontend/src/app/(dashboard)/recordings/view.tsx @@ -6,7 +6,6 @@ import { useAmbient } from "@/components/ambient/ambient-context"; import { Avatar, Badge, - Button, GlassCard, GlassPanel, Skeleton, @@ -40,8 +39,8 @@ export function RecordingsView({ const [playingId, setPlayingId] = useState(null); const deckRef = useStaggerReveal(".recording-deck-card", { - stagger: 0.05, - y: 14, + stagger: 0.04, + y: 10, dependencies: [items], }); @@ -88,129 +87,137 @@ export function RecordingsView({ ); + const totalRecordings = (items ?? []).length; + return (
{/* Tactical HUD Header Bar */} -
-
-
- - -
-

- RECORDINGS · TAPE_DECK +
+
+ +

+ Tape Deck · Captured Audio Archive

-
- CLIPS: - {(items ?? []).length} +
+ STATUS: + + {totalRecordings} CLIPS_ONLINE +
- {(items ?? []).length} clips + + {totalRecordings} clips archived } /> - {(items ?? []).length === 0 ? ( + {totalRecordings === 0 ? ( } - title="No recordings" - description="Voice clips captured by the bot appear here." + title="Tape deck is empty" + description="Voice transmissions captured in connected channels will be archived here." /> ) : (
{(items ?? []).map((r) => { const up = uploadStatus(r); const isPlaying = playingId === r.id; return ( - -
- -
-
- {r.username} -
-
- - - {r.channel_name ?? "voice"} - - · - - {formatRelativeTime(r.created_at)} - +
+ {/* Header info */} +
+ +
+
+ {r.username} +
+
+ + + {r.channel_name ?? "voice"} + + · + {formatRelativeTime(r.created_at)} +
+ {isPlaying && } + {up && !isPlaying && ( + + {up.label} + + )}
- {isPlaying && } - {up && !isPlaying && ( - {up.label} - )} - - {formatBytes(r.size_bytes)} + + {/* Audio Player Scrub */} +
+ {r.download_url ? ( + + setPlayingId((prev) => { + if (active) return r.id; + return prev === r.id ? null : prev; + }) + } + /> + ) : ( +
+ + {r.upload_status === "pending" + ? "UPLOAD_PENDING..." + : r.upload_error + ? r.upload_error + : "SYNTHESIZING_PCM..."} +
+ )} +
+
+ + {/* Actions & File Stats */} +
+ + SIZE: {formatBytes(r.size_bytes)} -
- - {r.download_url ? ( - - setPlayingId((prev) => { - if (active) return r.id; - // Only clear if THIS card was the one playing. - return prev === r.id ? null : prev; - }) - } - /> - ) : ( -
- - {r.upload_status === "pending" - ? "Upload pending…" - : r.upload_error - ? r.upload_error - : "Processing…"} -
- )} - -
- {r.download_url && ( - + {r.download_url && ( + + RAW + + )} + + PURGE + +
- +
); })}
diff --git a/services/frontend/src/app/(dashboard)/voice/view.tsx b/services/frontend/src/app/(dashboard)/voice/view.tsx index d3561192..8aa4e968 100644 --- a/services/frontend/src/app/(dashboard)/voice/view.tsx +++ b/services/frontend/src/app/(dashboard)/voice/view.tsx @@ -1,22 +1,10 @@ "use client"; -import { - AudioWaveform, - Disc, - Headphones, - Mic, - MicOff, - PhoneOff, - Radio, - Volume2, - Waves, -} from "lucide-react"; +import { Mic, PhoneOff, Radio, Volume2 } from "lucide-react"; import { useEffect, useState } from "react"; import { useAmbient } from "@/components/ambient/ambient-context"; -import { Equalizer } from "@/components/charts"; import { Button, GlassPanel, toast } from "@/components/primitives"; import { - EmptyState, ErrorState, PageTransition, SectionHeader, @@ -212,10 +200,7 @@ export function VoiceView({
- +
diff --git a/services/frontend/src/components/ChannelCultureGlossary.tsx b/services/frontend/src/components/ChannelCultureGlossary.tsx index f5ddc7ee..9613d587 100644 --- a/services/frontend/src/components/ChannelCultureGlossary.tsx +++ b/services/frontend/src/components/ChannelCultureGlossary.tsx @@ -1,8 +1,8 @@ "use client"; -import { Radio, Signal } from "lucide-react"; -import { useMemo } from "react"; -import { GlassPanel } from "@/components/primitives"; +import { Activity, Download, Hash } from "lucide-react"; +import { useMemo, useState } from "react"; +import { Badge, GlassPanel } from "@/components/primitives"; import { SectionHeader } from "@/components/shared"; import { useStaggerReveal } from "@/hooks/use-gsap-animation"; import { downloadCsv } from "@/lib/csv"; @@ -10,16 +10,16 @@ import { formatRelativeTime } from "@/lib/format"; import type { ChannelCultureRow } from "@/lib/types"; function deriveSignalStrength(c: ChannelCultureRow): number { - let score = 0.25; + let score = 0.3; if (c.culture_summary) { - score += Math.min(0.45, c.culture_summary.length / 400); + score += Math.min(0.45, c.culture_summary.length / 350); } if (c.last_analyzed_at) { const ageMs = Date.now() - c.last_analyzed_at; const days = ageMs / (1000 * 60 * 60 * 24); - score += Math.max(0, 0.3 - days * 0.02); + score += Math.max(0, 0.25 - days * 0.015); } - return Math.max(0.08, Math.min(1, score)); + return Math.max(0.12, Math.min(1, score)); } export function ChannelCultureGlossary({ @@ -27,27 +27,47 @@ export function ChannelCultureGlossary({ }: { cultures: ChannelCultureRow[]; }) { - const ranked = useMemo( - () => - cultures - .map((c) => ({ c, signal: deriveSignalStrength(c) })) - .sort((a, b) => b.signal - a.signal), - [cultures], - ); + const [filter, setFilter] = useState(""); - const containerRef = useStaggerReveal(".channel-row", { - stagger: 0.025, - y: 6, - dependencies: [cultures], + const ranked = useMemo(() => { + return cultures + .map((c) => ({ c, signal: deriveSignalStrength(c) })) + .sort((a, b) => b.signal - a.signal); + }, [cultures]); + + const filtered = useMemo(() => { + if (!filter.trim()) return ranked; + const q = filter.toLowerCase(); + return ranked.filter( + ({ c }) => + c.channel_name?.toLowerCase().includes(q) || + c.channel_id.toLowerCase().includes(q) || + c.culture_summary?.toLowerCase().includes(q), + ); + }, [ranked, filter]); + + const containerRef = useStaggerReveal(".channel-node-card", { + stagger: 0.03, + y: 10, + dependencies: [filtered.length, filter], }); return ( - - 0 ? ( +
+ {/* Search and Quick Filters */} + +
+ + setFilter(e.target.value)} + className="w-full rounded-[6px] border border-white/[0.08] bg-white/[0.02] py-1.5 pl-9 pr-3 text-xs text-ink placeholder:text-ink-faint focus:border-[#7170ff] focus:outline-none" + /> +
+
+ {cultures.length > 0 && ( - ) : null - } - /> - {cultures.length === 0 ? ( -
- NO CHANNELS LOGGED + )}
- ) : ( -
- {ranked.map(({ c }) => ( -
-
- - #{c.channel_name ?? c.channel_id.slice(0, 8)} - - {c.last_analyzed_at && ( - - {formatRelativeTime(c.last_analyzed_at)} - - )} -
- {c.culture_summary && ( -

- {c.culture_summary} -

- )} -
- ))} -
- )} - + + + {/* Channel Nodes Grid */} + + + {filtered.length} of {cultures.length} channels + + } + /> + + {filtered.length === 0 ? ( +
+ NO MATCHING CHANNEL TELEMETRY FOUND +
+ ) : ( +
+ {filtered.map(({ c, signal }) => { + const pct = Math.round(signal * 100); + const isHighSignal = signal > 0.65; + return ( +
+
+ {/* Channel Card Header */} +
+
+ + + + + {c.channel_name ?? c.channel_id.slice(0, 10)} + +
+ + {pct}% SIGNAL + +
+ + {/* Culture Summary */} +
+ {c.culture_summary ? ( +

+ {c.culture_summary} +

+ ) : ( +

+ Awaiting AI culture profile synthesis... +

+ )} +
+
+ + {/* Signal Strength Bar & Timestamp */} +
+
+ + + INTEL RATIO + + + {c.last_analyzed_at + ? formatRelativeTime(c.last_analyzed_at) + : "NEVER"} + +
+
+
+
+
+
+ ); + })} +
+ )} + +
); } diff --git a/services/frontend/src/components/LiveModerationFeed.tsx b/services/frontend/src/components/LiveModerationFeed.tsx index 45128c14..5dc8d1f2 100644 --- a/services/frontend/src/components/LiveModerationFeed.tsx +++ b/services/frontend/src/components/LiveModerationFeed.tsx @@ -1,9 +1,17 @@ "use client"; -import { Badge, GlassPanel } from "@/components/primitives"; +import { useGSAP } from "@gsap/react"; +import gsap from "gsap"; +import { CheckCircle2, ShieldAlert, UserX, VolumeX } from "lucide-react"; +import { useRef } from "react"; +import { Badge } from "@/components/primitives"; import { formatRelativeTime } from "@/lib/format"; import type { ModerationAction } from "@/lib/types"; +if (typeof window !== "undefined") { + gsap.registerPlugin(useGSAP); +} + const ACTION_LABEL: Record = { delete_message: "Deleted", timeout_user: "Timeout", @@ -15,6 +23,21 @@ const ACTION_LABEL: Record = { none: "None", }; +function actionIcon(type: string) { + switch (type) { + case "ban_user": + case "kick_user": + return ; + case "timeout_user": + return ; + case "delete_message": + case "warn_user": + return ; + default: + return ; + } +} + function severityTone( sev?: string | null, ): "signal" | "amber" | "vermilion" | null { @@ -36,67 +59,110 @@ export function LiveModerationFeed({ }: { actions: ModerationAction[]; }) { + const feedRef = useRef(null); + + useGSAP( + () => { + if (!feedRef.current) return; + const items = feedRef.current.querySelectorAll(".mod-feed-item"); + if (items.length === 0) return; + + const prefersReduced = window.matchMedia( + "(prefers-reduced-motion: reduce)", + ).matches; + if (prefersReduced) return; + + gsap.fromTo( + items, + { opacity: 0, x: -8 }, + { + opacity: 1, + x: 0, + duration: 0.32, + stagger: 0.025, + ease: "power2.out", + clearProps: "transform", + }, + ); + }, + { scope: feedRef, dependencies: [actions.length] }, + ); + return ( - -
+
+
- - - + + + + + + Live Stream Audit Log -

Live Feed

- {actions.length} recent + + {actions.length} RECENT ACTIONS +
-
+
{actions.length === 0 ? ( -

- Waiting for new moderation actions… -

+
+ AWAITING MODERATION DISPATCH STREAM... +
) : ( -
    - {actions.map((a, i) => { - const tone = severityTone(a.severity); - return ( -
  • -
    -
    - - {ACTION_LABEL[a.action_type] ?? a.action_type} + actions.map((a) => { + const tone = severityTone(a.severity); + return ( +
    + + {actionIcon(a.action_type)} + + +
    +
    + + {ACTION_LABEL[a.action_type] ?? a.action_type} + + {a.severity && ( + + {a.severity} - {a.severity && ( - - {a.severity} - - )} - {a.categories?.length ? ( - - {a.categories.slice(0, 3).join(", ")} - - ) : null} -
    - {a.reason && ( -

    - “{a.reason}” -

    )} -

    - {a.username ?? a.user_id ?? "unknown"} ·{" "} + {a.categories?.length ? ( + + [{a.categories.slice(0, 2).join(", ")}] + + ) : null} + {formatRelativeTime(a.created_at)} -

    +
    -
  • - ); - })} -
+ + {a.reason && ( +

+ “{a.reason}” +

+ )} + +
+ TARGET: + + {a.username ?? a.user_id ?? "UNKNOWN_SUBJECT"} + +
+
+
+ ); + }) )}
- +
); } diff --git a/services/frontend/src/components/TermGlossary.tsx b/services/frontend/src/components/TermGlossary.tsx index 7fa0352e..e03030bc 100644 --- a/services/frontend/src/components/TermGlossary.tsx +++ b/services/frontend/src/components/TermGlossary.tsx @@ -1,8 +1,8 @@ "use client"; -import { ChevronDown, Globe } from "lucide-react"; -import { useRef, useState } from "react"; -import { GlassPanel } from "@/components/primitives"; +import { ChevronDown, Download, Globe, Search } from "lucide-react"; +import { useMemo, useRef, useState } from "react"; +import { Badge, GlassPanel } from "@/components/primitives"; import { SectionHeader } from "@/components/shared"; import { useStaggerReveal } from "@/hooks/use-gsap-animation"; import { downloadCsv } from "@/lib/csv"; @@ -14,25 +14,27 @@ function GlossaryEntry({ t }: { t: GlossaryRow }) { const bodyRef = useRef(null); return ( -
+
-
-

{t.definition}

+
+

+ {t.definition} +

{t.source_url && ( {t.source_url} @@ -61,65 +65,94 @@ function GlossaryEntry({ t }: { t: GlossaryRow }) { } export function TermGlossary({ terms }: { terms: GlossaryRow[] }) { + const [filter, setFilter] = useState(""); + + const filtered = useMemo(() => { + if (!filter.trim()) return terms; + const q = filter.toLowerCase(); + return terms.filter( + (t) => + t.term.toLowerCase().includes(q) || + t.definition?.toLowerCase().includes(q), + ); + }, [terms, filter]); + const listRef = useStaggerReveal(".glossary-entry", { - stagger: 0.035, - y: 10, - dependencies: [terms], + stagger: 0.025, + y: 8, + dependencies: [filtered.length, filter], }); return (
{/* Tactical HUD Header Bar */} -
-
-
- - -
-

- TERM GLOSSARY · KB_INDEX +
+
+ +

+ Slang & Term Glossary · Knowledge Base

-
- TERMS: - {terms.length} +
+ INDEXED: + + {terms.length} TERMS_RECORDED +
- + {/* Filter and Action Bar */} + +
+ + setFilter(e.target.value)} + className="w-full rounded-[6px] border border-white/[0.08] bg-white/[0.02] py-1.5 pl-9 pr-3 text-xs text-ink placeholder:text-ink-faint focus:border-[#7170ff] focus:outline-none" + /> +
+ {terms.length > 0 && ( + + )} +
+ + 0 ? ( - - ) : null + + {filtered.length} of {terms.length} terms + } /> - {terms.length === 0 ? ( -

- NO TERM RESOLUTIONS CACHED YET -

+ {filtered.length === 0 ? ( +
+ NO TERM DEFINITIONS MATCH YOUR FILTER +
) : ( -
- {terms.map((t) => ( +
+ {filtered.map((t) => ( ))}
diff --git a/services/frontend/src/components/chatbot/chatbot.tsx b/services/frontend/src/components/chatbot/chatbot.tsx index 3c8d642c..1f49f3b5 100644 --- a/services/frontend/src/components/chatbot/chatbot.tsx +++ b/services/frontend/src/components/chatbot/chatbot.tsx @@ -1,16 +1,10 @@ "use client"; -import { - Bot, - MessageSquare, - Send, - Trash2, - X, -} from "lucide-react"; -import { useEffect, useRef, useState } from "react"; import { useGSAP } from "@gsap/react"; import gsap from "gsap"; -import { Avatar, Button, GlassPanel, toast } from "@/components/primitives"; +import { Bot, MessageSquare, Send, Trash2, X } from "lucide-react"; +import { useEffect, useRef, useState } from "react"; +import { Button, toast } from "@/components/primitives"; import { MarkdownLite } from "@/components/shared"; import { useChatbotUserId } from "@/hooks/use-chatbot-user"; import { chatbotApi } from "@/lib/api"; @@ -86,7 +80,7 @@ export function Chatbot() { useEffect(() => { bottomRef.current?.scrollIntoView({ behavior: "smooth" }); - }, [msgs, loading]); + }, []); const send = async (text: string) => { if (!text.trim() || loading || !userId) return; @@ -132,7 +126,11 @@ export function Chatbot() { onClick={() => setOpen((o) => !o)} className="fixed right-4 bottom-5 z-50 flex size-10 items-center justify-center rounded-full border border-white/[0.12] bg-[#0f1011] text-[#f7f8f8] shadow-2xl transition-all duration-150 hover:scale-105 hover:border-[#7170ff] hover:bg-[#191a1b]" > - {open ? : } + {open ? ( + + ) : ( + + )} {open && ( diff --git a/services/frontend/src/components/shell/nav-rail.tsx b/services/frontend/src/components/shell/nav-rail.tsx index 17eeba9f..b705b2e0 100644 --- a/services/frontend/src/components/shell/nav-rail.tsx +++ b/services/frontend/src/components/shell/nav-rail.tsx @@ -16,13 +16,11 @@ function NavItem({ label, active, Icon, - index, }: { href: string; label: string; active: boolean; Icon: React.ComponentType>; - index: number; }) { return (
- + {/* Precision Micro-Indicator */} {active && ( @@ -83,12 +78,11 @@ export function NavRail() { className="mb-[calc(0.75rem+env(safe-area-inset-bottom))] ml-[calc(0.75rem+env(safe-area-inset-left))] mt-[calc(0.75rem+env(safe-area-inset-top))] hidden w-[54px] flex-col items-center gap-1 rounded-[10px] border border-white/[0.08] bg-[#0f1011]/80 py-3 shadow-xl backdrop-blur-md md:flex" >
- {navItems.map((item, i) => ( + {navItems.map((item) => (