feat(frontend): Ambient/WebGL console revamp + lint/type cleanup
Ground-up rebuild of the GMW frontend as an Ambient Field console: - WebGL ambient background (Three.js shader, drifting motes, reduced-motion aware) - Glassmorphism dark cyber theme across all 8 routes - SSR page + client view split with SWR fallback; realtime via WebSocket - Command palette (Cmd+K), chatbot FAB, guild/channel pickers - Chart primitives: donut, radial-gauge, area-activity, sparkline, equalizer Cleanup (review pass): - Remove stray Puppeteer nav-test/nav-debug scripts - Replace non-null assertions with guards (dashboard/moderation) - Drop unused useGuilds fetches in messages/voice views - Type implicit-any `let` declarations across pages - Add a11y roles/labels to SVG charts and audio, tidy imports
This commit is contained in:
@@ -1,15 +1,17 @@
|
||||
"use client";
|
||||
|
||||
import { Hash, Search, Sparkles, TrendingUp } from "lucide-react";
|
||||
import { useEffect, useState } from "react";
|
||||
import { Search, Sparkles, TrendingUp, Hash } from "lucide-react";
|
||||
import { useMessageSearch, useTopReactors, useChannels } from "@/hooks";
|
||||
import { useAmbient } from "@/components/ambient/ambient-context";
|
||||
import { GlassPanel, GlassCard, Avatar, Input, Badge } from "@/components/primitives";
|
||||
import { SectionHeader, EmptyState, LoadingState } from "@/components/shared";
|
||||
import { renderMessageContent, getMessageChannelLabel } from "@/lib/format";
|
||||
import { Avatar, Badge, GlassPanel, Input } from "@/components/primitives";
|
||||
import { EmptyState, LoadingState, SectionHeader } from "@/components/shared";
|
||||
import { useChannels, useMessageSearch, useTopReactors } from "@/hooks";
|
||||
import { getMessageChannelLabel, renderMessageContent } from "@/lib/format";
|
||||
import type { AiStatus } from "@/lib/types";
|
||||
|
||||
function aiTone(s?: AiStatus | null): "signal" | "amber" | "vermilion" | "neutral" {
|
||||
function aiTone(
|
||||
s?: AiStatus | null,
|
||||
): "signal" | "amber" | "vermilion" | "neutral" {
|
||||
if (s === "clean") return "signal";
|
||||
if (s === "warn") return "amber";
|
||||
if (s === "flagged" || s === "error") return "vermilion";
|
||||
@@ -24,7 +26,11 @@ export function AnalysisView() {
|
||||
const ambient = useAmbient();
|
||||
|
||||
useEffect(() => {
|
||||
ambient.set(query ? "amber" : "signal", 0.3, query ? "analyzing" : "search");
|
||||
ambient.set(
|
||||
query ? "amber" : "signal",
|
||||
0.3,
|
||||
query ? "analyzing" : "search",
|
||||
);
|
||||
}, [query, ambient]);
|
||||
|
||||
return (
|
||||
@@ -49,28 +55,57 @@ export function AnalysisView() {
|
||||
/>
|
||||
</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>
|
||||
<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">
|
||||
<GlassPanel className="lg:col-span-3">
|
||||
<SectionHeader eyebrow="results" title="Matches" action={<span className="mono text-xs text-ink-faint">{(search.data ?? []).length}</span>} />
|
||||
{query.trim().length >= 2 && search.isLoading && <LoadingState label="Scanning" />}
|
||||
<SectionHeader
|
||||
eyebrow="results"
|
||||
title="Matches"
|
||||
action={
|
||||
<span className="mono text-xs text-ink-faint">
|
||||
{(search.data ?? []).length}
|
||||
</span>
|
||||
}
|
||||
/>
|
||||
{query.trim().length >= 2 && search.isLoading && (
|
||||
<LoadingState label="Scanning" />
|
||||
)}
|
||||
{(search.data ?? []).length === 0 ? (
|
||||
<EmptyState icon={<Search className="size-7" />} title="No matches yet" description="Run a search to surface messages across the guild." />
|
||||
<EmptyState
|
||||
icon={<Search className="size-7" />}
|
||||
title="No matches yet"
|
||||
description="Run a search to surface messages across the guild."
|
||||
/>
|
||||
) : (
|
||||
<div className="space-y-1.5">
|
||||
{(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">
|
||||
<div
|
||||
key={m.id}
|
||||
className="flex items-start gap-3 rounded-[12px] border border-hairline bg-white/[0.03] p-3"
|
||||
>
|
||||
<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">{m.username}</span>
|
||||
<span className="mono text-[0.65rem] text-ink-faint">{getMessageChannelLabel(m)}</span>
|
||||
{m.ai_status && <Badge tone={aiTone(m.ai_status)} className="ml-auto">{m.ai_status}</Badge>}
|
||||
<span className="text-sm font-semibold text-ink">
|
||||
{m.username}
|
||||
</span>
|
||||
<span className="mono text-[0.65rem] text-ink-faint">
|
||||
{getMessageChannelLabel(m)}
|
||||
</span>
|
||||
{m.ai_status && (
|
||||
<Badge tone={aiTone(m.ai_status)} className="ml-auto">
|
||||
{m.ai_status}
|
||||
</Badge>
|
||||
)}
|
||||
</div>
|
||||
<div className="mt-0.5 text-sm text-ink-soft">
|
||||
{renderMessageContent(m.content, m.metadata) || "(embed)"}
|
||||
</div>
|
||||
<div className="mt-0.5 text-sm text-ink-soft">{renderMessageContent(m.content, m.metadata) || "(embed)"}</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
@@ -80,29 +115,63 @@ export function AnalysisView() {
|
||||
|
||||
<div className="space-y-5 lg:col-span-2">
|
||||
<GlassPanel>
|
||||
<SectionHeader eyebrow="culture" title={<span className="flex items-center gap-2"><TrendingUp className="size-4 text-signal" /> Top reactors</span>} />
|
||||
<SectionHeader
|
||||
eyebrow="culture"
|
||||
title={
|
||||
<span className="flex items-center gap-2">
|
||||
<TrendingUp className="size-4 text-signal" /> Top reactors
|
||||
</span>
|
||||
}
|
||||
/>
|
||||
<div className="space-y-2">
|
||||
{(reactors ?? []).slice(0, 6).map((r, i) => (
|
||||
<div key={r.user_id} className="flex items-center gap-3 text-sm">
|
||||
<div
|
||||
key={r.user_id}
|
||||
className="flex items-center gap-3 text-sm"
|
||||
>
|
||||
<span className="mono w-5 text-ink-faint">{i + 1}</span>
|
||||
<span className="flex-1 truncate text-ink">{r.username}</span>
|
||||
<span className="mono text-xs text-signal">+{r.net_count}</span>
|
||||
<span className="mono text-xs text-signal">
|
||||
+{r.net_count}
|
||||
</span>
|
||||
</div>
|
||||
))}
|
||||
{(reactors ?? []).length === 0 && <div className="py-4 text-center text-xs text-ink-faint">No data</div>}
|
||||
{(reactors ?? []).length === 0 && (
|
||||
<div className="py-4 text-center text-xs text-ink-faint">
|
||||
No data
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</GlassPanel>
|
||||
|
||||
<GlassPanel>
|
||||
<SectionHeader eyebrow="channels" title={<span className="flex items-center gap-2"><Hash className="size-4 text-signal" /> Top channels</span>} />
|
||||
<SectionHeader
|
||||
eyebrow="channels"
|
||||
title={
|
||||
<span className="flex items-center gap-2">
|
||||
<Hash className="size-4 text-signal" /> Top channels
|
||||
</span>
|
||||
}
|
||||
/>
|
||||
<div className="space-y-2">
|
||||
{(channels ?? []).slice(0, 6).map((c) => (
|
||||
<div key={c.channel_id} className="flex items-center gap-3 text-sm">
|
||||
<span className="flex-1 truncate text-ink-soft">{c.channel_name ?? c.channel_id.slice(0, 8)}</span>
|
||||
<span className="mono text-xs text-ink-faint">{c.total_messages}</span>
|
||||
<div
|
||||
key={c.channel_id}
|
||||
className="flex items-center gap-3 text-sm"
|
||||
>
|
||||
<span className="flex-1 truncate text-ink-soft">
|
||||
{c.channel_name ?? c.channel_id.slice(0, 8)}
|
||||
</span>
|
||||
<span className="mono text-xs text-ink-faint">
|
||||
{c.total_messages}
|
||||
</span>
|
||||
</div>
|
||||
))}
|
||||
{(channels ?? []).length === 0 && <div className="py-4 text-center text-xs text-ink-faint">No data</div>}
|
||||
{(channels ?? []).length === 0 && (
|
||||
<div className="py-4 text-center text-xs text-ink-faint">
|
||||
No data
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</GlassPanel>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user