Refactor moderation and recordings views; enhance UI components and improve performance

- Cleaned up imports and removed unused hooks in ModerationView and RecordingsView.
- Updated UI elements for better visual consistency and accessibility.
- Improved performance by optimizing rendering logic and reducing unnecessary state updates.
- Added filtering functionality in ChannelCultureGlossary and TermGlossary components.
- Enhanced LiveModerationFeed with GSAP animations for smoother transitions.
- Updated chatbot component for better user experience and responsiveness.
- Refined NavRail component for cleaner code and improved navigation item rendering.
This commit is contained in:
asepharyana committed 2026-08-26 10:39:00 +07:00
1 parent 3b688e0d0f
commit ccfe769b2f
12 files changed
+693 -509

No files matched your search

@@ -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<HTMLDivElement>(null);
const channelsRef = useRef<HTMLDivElement>(null);
const resultsRef = useStaggerReveal<HTMLDivElement>(".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 (
<div className="space-y-4">
{/* Tactical HUD Header Bar */}
<div className="flex flex-wrap items-center justify-between gap-3 border-b border-hairline pb-3">
<div className="flex items-center gap-3">
<div className="relative flex size-3 items-center justify-center">
<span className="absolute inline-flex size-full animate-ping rounded-full bg-signal opacity-75" />
<span className="relative inline-flex size-2 rounded-full bg-signal" />
</div>
<h1 className="font-mono text-xs font-semibold tracking-widest text-ink uppercase">
ANALYSIS ENGINE · DEEP_SCAN
<div className="flex flex-wrap items-center justify-between gap-3 border-b border-white/[0.06] pb-3">
<div className="flex items-center gap-2.5">
<span className="h-2 w-2 rounded-full bg-[#7170ff] shadow-[0_0_8px_#7170ff]" />
<h1 className="font-mono text-xs font-semibold tracking-wide text-[#f7f8f8] uppercase">
Deep Scan · Semantic Search & Telemetry Analysis
</h1>
</div>
<div className="inline-flex items-center gap-1.5 rounded-sm bg-surface px-2 py-0.5 font-mono text-[11px] text-ink-soft">
<span className="text-ink-faint">MODE:</span>
<span className="font-bold text-signal">
{query.trim().length >= 2 ? "SEARCHING" : "IDLE"}
<div className="flex items-center gap-2 font-mono text-[11px] text-[#8a8f98]">
<span>ENGINE:</span>
<span className="rounded bg-white/[0.04] px-1.5 py-0.5 font-medium text-[#7170ff] border border-white/[0.06]">
{query.trim().length >= 2 ? "SCANNING" : "STANDBY"}
</span>
</div>
</div>
<GlassPanel glow className="relative overflow-hidden">
<div className="scan-line absolute inset-x-0 top-0" />
{/* Hero Query Stage */}
<GlassPanel className="p-5">
<div className="flex items-center gap-3">
<Sparkles className="size-5 text-signal" />
<span className="flex size-8 items-center justify-center rounded-[6px] border border-white/[0.08] bg-[#7170ff]/10 text-[#7170ff]">
<Sparkles className="size-4" />
</span>
<div>
<div className="eyebrow">Semantic search</div>
<h2 className="display text-balance text-2xl text-ink">
Search the archive
<h2 className="text-base font-semibold text-ink">
Cross-Guild Archive Intelligence
</h2>
<p className="font-mono text-[11px] text-[#8a8f98]">
Query vectorized messages, heuristic flags, and user behavior
</p>
</div>
</div>
<div className="relative mt-4">
<Search className="absolute left-4 top-1/2 size-5 -translate-y-1/2 text-ink-faint" />
<Search className="absolute left-3.5 top-1/2 size-4 -translate-y-1/2 text-ink-faint" />
<Input
className="h-12 pl-12 text-base"
placeholder="Find messages, patterns, flags…"
className="h-10 pl-10 text-xs text-ink"
placeholder="Search keywords, behavioral patterns, infraction terms..."
value={query}
onChange={(e) => setQuery(e.target.value)}
autoFocus
/>
</div>
{query.trim().length > 0 && query.trim().length < 2 && (
<div className="mono mt-2 text-xs text-ink-faint">
Type at least 2 characters…
</div>
)}
</GlassPanel>
<div className="grid gap-5 lg:grid-cols-5">
<div className="grid gap-3 lg:grid-cols-5">
{/* Search Results Column */}
<GlassPanel className="lg:col-span-3">
<SectionHeader
eyebrow="results"
title="Matches"
eyebrow="search output"
title="Matched Log Packets"
action={
<span className="mono text-xs text-ink-faint">
{(search.data ?? []).length}
<span className="mono text-xs text-[#8a8f98]">
{(search.data ?? []).length} results
</span>
}
/>
{query.trim().length >= 2 && search.isLoading && (
<SkeletonRows rows={6} />
)}
{(search.data ?? []).length === 0 ? (
{(search.data ?? []).length === 0 && !search.isLoading ? (
<EmptyState
icon={<Search className="size-7" />}
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."
/>
) : (
<div className="space-y-1.5">
<div ref={resultsRef} className="space-y-1.5 mt-3">
{(search.data ?? []).map((m) => (
<div
key={m.id}
className="flex items-start gap-3 rounded-[12px] border border-hairline bg-white/[0.03] p-3"
className="search-result-card flex items-start gap-3 rounded-[6px] border border-white/[0.06] bg-white/[0.02] p-3 transition-all hover:border-white/[0.12] hover:bg-white/[0.04]"
>
<Avatar src={m.avatar_url} name={m.username} size={32} />
<div className="min-w-0 flex-1">
<div className="flex items-center gap-2">
<span className="text-sm font-semibold text-ink">
<span className="text-xs font-semibold text-ink">
{m.username}
</span>
<span className="mono text-[0.65rem] text-ink-faint">
<span className="font-mono text-[10px] text-ink-faint">
{getMessageChannelLabel(m)}
</span>
{m.ai_status && (
<Badge tone={aiTone(m.ai_status)} className="ml-auto">
<Badge
tone={aiTone(m.ai_status)}
className="ml-auto font-mono text-[9px]"
>
{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() {
</Badge>
)}
</div>
<div className="mt-0.5 text-sm text-ink-soft">
<div className="mt-1 text-xs text-ink-soft leading-relaxed">
{renderMessageContent(m.content, m.metadata) || "(embed)"}
</div>
</div>
@@ -210,17 +217,19 @@ export function AnalysisView() {
)}
</GlassPanel>
<div className="space-y-5 lg:col-span-2">
{/* Culture & Leaderboard Side Column */}
<div className="space-y-3 lg:col-span-2">
<GlassPanel>
<SectionHeader
eyebrow="culture"
eyebrow="engagement"
title={
<span className="flex items-center gap-2">
<TrendingUp className="size-4 text-signal" /> Top reactors
<span className="flex items-center gap-1.5">
<TrendingUp className="size-3.5 text-[#7170ff]" /> Top
Reactors
</span>
}
/>
<div ref={reactorsRef} className="space-y-2">
<div ref={reactorsRef} className="space-y-2 mt-3">
{(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 (
<div
key={r.user_id}
className="flex items-center gap-3 text-sm"
className="flex items-center gap-3 text-xs"
>
<span className="mono w-5 text-ink-faint">{i + 1}</span>
<span className="w-28 shrink-0 truncate text-ink-soft sm:w-40">
<span className="font-mono w-4 text-[10px] text-ink-faint">
0{i + 1}
</span>
<span className="w-24 shrink-0 truncate font-mono text-xs text-ink sm:w-32">
{r.username}
</span>
<div className="h-1.5 flex-1 overflow-hidden rounded-full bg-white/8">
<div className="h-1 flex-1 overflow-hidden rounded-full bg-white/[0.06]">
<div
className="reactor-bar-fill h-full rounded-full bg-signal/70"
className="reactor-bar-fill h-full rounded-full bg-gradient-to-r from-[#5e6ad2] to-[#7170ff]"
style={{ width: `${pct}%` }}
/>
</div>
<span
className="reactor-count mono w-12 text-right text-xs text-signal"
className="reactor-count font-mono w-10 text-right text-[11px] font-semibold text-[#7170ff]"
data-target={r.net_count}
>
+0
@@ -252,8 +263,8 @@ export function AnalysisView() {
);
})}
{(reactors ?? []).length === 0 && (
<div className="py-4 text-center text-xs text-ink-faint">
No data
<div className="py-4 text-center font-mono text-xs text-ink-faint">
NO REACTOR DATA
</div>
)}
</div>
@@ -261,30 +272,32 @@ export function AnalysisView() {
<GlassPanel>
<SectionHeader
eyebrow="channels"
eyebrow="volume"
title={
<span className="flex items-center gap-2">
<Hash className="size-4 text-signal" /> Top channels
<span className="flex items-center gap-1.5">
<Hash className="size-3.5 text-[#7170ff]" /> Top Active
Channels
</span>
}
/>
<div ref={channelsRef} className="space-y-2">
<div ref={channelsRef} className="space-y-2 mt-3">
{(channels ?? []).slice(0, 6).map((c) => (
<div
key={c.channel_id}
className="channel-row flex items-center gap-3 text-sm"
className="channel-row flex items-center justify-between rounded-[6px] border border-white/[0.04] bg-white/[0.01] p-2 text-xs hover:bg-white/[0.03]"
>
<span className="flex-1 truncate text-ink-soft">
{c.channel_name ?? c.channel_id.slice(0, 8)}
<span className="flex items-center gap-1.5 truncate font-mono text-xs text-ink">
<Hash className="size-3 text-[#7170ff]" />
{c.channel_name ?? c.channel_id.slice(0, 10)}
</span>
<span className="mono text-xs text-ink-faint">
{c.total_messages}
<span className="font-mono text-[10px] text-[#8a8f98]">
{c.total_messages.toLocaleString()} msgs
</span>
</div>
))}
{(channels ?? []).length === 0 && (
<div className="py-4 text-center text-xs text-ink-faint">
No data
<div className="py-4 text-center font-mono text-xs text-ink-faint">
NO CHANNEL DATA
</div>
)}
</div>