Compare commits
9
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8583bcdf17 | ||
|
|
b12eb0a038 | ||
|
|
b72423c64d | ||
|
|
6b34fc97ec | ||
|
|
8ce8978755 | ||
|
|
869cad88e4 | ||
|
|
f136cf3f6b | ||
|
|
cd3ee5b5d8 | ||
|
|
93288afa0b |
No files matched your search
@@ -48,7 +48,7 @@
|
|||||||
"@types/pg": "^8.23.1",
|
"@types/pg": "^8.23.1",
|
||||||
"@types/ws": "^8.18.1",
|
"@types/ws": "^8.18.1",
|
||||||
"drizzle-kit": "^0.31.10",
|
"drizzle-kit": "^0.31.10",
|
||||||
"tsx": "^4.23.13",
|
"tsx": "^4.23.15",
|
||||||
"typescript": "^7.0.2",
|
"typescript": "^7.0.2",
|
||||||
"vitest": "latest"
|
"vitest": "latest"
|
||||||
}
|
}
|
||||||
|
|||||||
Generated
+14
-14
@@ -79,14 +79,14 @@ importers:
|
|||||||
specifier: ^0.31.10
|
specifier: ^0.31.10
|
||||||
version: 0.31.10
|
version: 0.31.10
|
||||||
tsx:
|
tsx:
|
||||||
specifier: ^4.23.13
|
specifier: ^4.23.15
|
||||||
version: 4.23.13
|
version: 4.23.15
|
||||||
typescript:
|
typescript:
|
||||||
specifier: ^7.0.2
|
specifier: ^7.0.2
|
||||||
version: 7.0.2
|
version: 7.0.2
|
||||||
vitest:
|
vitest:
|
||||||
specifier: latest
|
specifier: latest
|
||||||
version: 5.0.1(@types/node@26.4.0)(vite@8.1.5(@types/node@26.4.0)(esbuild@0.28.2)(tsx@4.23.13))
|
version: 5.0.1(@types/node@26.4.0)(vite@8.1.5(@types/node@26.4.0)(esbuild@0.28.2)(tsx@4.23.15))
|
||||||
|
|
||||||
packages:
|
packages:
|
||||||
|
|
||||||
@@ -2172,8 +2172,8 @@ packages:
|
|||||||
tslib@2.8.1:
|
tslib@2.8.1:
|
||||||
resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==}
|
resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==}
|
||||||
|
|
||||||
tsx@4.23.13:
|
tsx@4.23.15:
|
||||||
resolution: {integrity: sha512-BL5MGkRln6aDYhb0xbQlEAGw743BaZYWdbWtdJOBriYJboKgUUYCadFp2/FpBBZquBC/ezNBn7wMMPx7FDZUDw==}
|
resolution: {integrity: sha512-Yiex1Ovn8z2xPpOWckIiysV1SSyRMY9BkLF++q0yKiDxCqRhosKfMg3janKkiLBwZ5c/YryloKwGZcrEmtwxKw==}
|
||||||
engines: {node: '>=18.0.0'}
|
engines: {node: '>=18.0.0'}
|
||||||
hasBin: true
|
hasBin: true
|
||||||
|
|
||||||
@@ -3055,14 +3055,14 @@ snapshots:
|
|||||||
'@typescript/typescript-win32-x64@7.0.2':
|
'@typescript/typescript-win32-x64@7.0.2':
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
'@vitest/mocker@5.0.1(vite@8.1.5(@types/node@26.4.0)(esbuild@0.28.2)(tsx@4.23.13))':
|
'@vitest/mocker@5.0.1(vite@8.1.5(@types/node@26.4.0)(esbuild@0.28.2)(tsx@4.23.15))':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@jridgewell/trace-mapping': 0.3.31
|
'@jridgewell/trace-mapping': 0.3.31
|
||||||
'@vitest/spy': 5.0.1
|
'@vitest/spy': 5.0.1
|
||||||
estree-walker: 3.0.3
|
estree-walker: 3.0.3
|
||||||
magic-string: 1.2.3
|
magic-string: 1.2.3
|
||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
vite: 8.1.5(@types/node@26.4.0)(esbuild@0.28.2)(tsx@4.23.13)
|
vite: 8.1.5(@types/node@26.4.0)(esbuild@0.28.2)(tsx@4.23.15)
|
||||||
|
|
||||||
'@vitest/spy@5.0.1': {}
|
'@vitest/spy@5.0.1': {}
|
||||||
|
|
||||||
@@ -3230,7 +3230,7 @@ snapshots:
|
|||||||
'@drizzle-team/brocli': 0.10.2
|
'@drizzle-team/brocli': 0.10.2
|
||||||
'@esbuild-kit/esm-loader': 2.6.5
|
'@esbuild-kit/esm-loader': 2.6.5
|
||||||
esbuild: 0.25.12
|
esbuild: 0.25.12
|
||||||
tsx: 4.23.13
|
tsx: 4.23.15
|
||||||
|
|
||||||
drizzle-orm@0.45.2(@types/pg@8.23.1)(pg@8.23.0):
|
drizzle-orm@0.45.2(@types/pg@8.23.1)(pg@8.23.0):
|
||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
@@ -3960,7 +3960,7 @@ snapshots:
|
|||||||
|
|
||||||
tslib@2.8.1: {}
|
tslib@2.8.1: {}
|
||||||
|
|
||||||
tsx@4.23.13:
|
tsx@4.23.15:
|
||||||
dependencies:
|
dependencies:
|
||||||
esbuild: 0.28.2
|
esbuild: 0.28.2
|
||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
@@ -3996,7 +3996,7 @@ snapshots:
|
|||||||
util-deprecate@1.0.2:
|
util-deprecate@1.0.2:
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
vite@8.1.5(@types/node@26.4.0)(esbuild@0.28.2)(tsx@4.23.13):
|
vite@8.1.5(@types/node@26.4.0)(esbuild@0.28.2)(tsx@4.23.15):
|
||||||
dependencies:
|
dependencies:
|
||||||
lightningcss: 1.33.0
|
lightningcss: 1.33.0
|
||||||
picomatch: 4.0.7
|
picomatch: 4.0.7
|
||||||
@@ -4007,12 +4007,12 @@ snapshots:
|
|||||||
'@types/node': 26.4.0
|
'@types/node': 26.4.0
|
||||||
esbuild: 0.28.2
|
esbuild: 0.28.2
|
||||||
fsevents: 2.3.3
|
fsevents: 2.3.3
|
||||||
tsx: 4.23.13
|
tsx: 4.23.15
|
||||||
|
|
||||||
vitest@5.0.1(@types/node@26.4.0)(vite@8.1.5(@types/node@26.4.0)(esbuild@0.28.2)(tsx@4.23.13)):
|
vitest@5.0.1(@types/node@26.4.0)(vite@8.1.5(@types/node@26.4.0)(esbuild@0.28.2)(tsx@4.23.15)):
|
||||||
dependencies:
|
dependencies:
|
||||||
'@types/chai': 5.2.3
|
'@types/chai': 5.2.3
|
||||||
'@vitest/mocker': 5.0.1(vite@8.1.5(@types/node@26.4.0)(esbuild@0.28.2)(tsx@4.23.13))
|
'@vitest/mocker': 5.0.1(vite@8.1.5(@types/node@26.4.0)(esbuild@0.28.2)(tsx@4.23.15))
|
||||||
chai: 6.2.2
|
chai: 6.2.2
|
||||||
es-module-lexer: 2.3.2
|
es-module-lexer: 2.3.2
|
||||||
expect-type: 1.4.0
|
expect-type: 1.4.0
|
||||||
@@ -4023,7 +4023,7 @@ snapshots:
|
|||||||
tinybench: 6.1.4
|
tinybench: 6.1.4
|
||||||
tinyexec: 1.3.0
|
tinyexec: 1.3.0
|
||||||
tinyglobby: 0.2.17
|
tinyglobby: 0.2.17
|
||||||
vite: 8.1.5(@types/node@26.4.0)(esbuild@0.28.2)(tsx@4.23.13)
|
vite: 8.1.5(@types/node@26.4.0)(esbuild@0.28.2)(tsx@4.23.15)
|
||||||
why-is-node-running: 2.3.0
|
why-is-node-running: 2.3.0
|
||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
'@types/node': 26.4.0
|
'@types/node': 26.4.0
|
||||||
|
|||||||
@@ -6,7 +6,6 @@ import {
|
|||||||
ChevronRight,
|
ChevronRight,
|
||||||
Hash,
|
Hash,
|
||||||
MessageSquare,
|
MessageSquare,
|
||||||
Mic,
|
|
||||||
Shield,
|
Shield,
|
||||||
ShieldAlert,
|
ShieldAlert,
|
||||||
ShieldCheck,
|
ShieldCheck,
|
||||||
@@ -14,7 +13,7 @@ import {
|
|||||||
Zap,
|
Zap,
|
||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { useEffect } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { useAmbient } from "@/components/ambient/ambient-context";
|
import { useAmbient } from "@/components/ambient/ambient-context";
|
||||||
import { AreaActivity, RadialGauge } from "@/components/charts";
|
import { AreaActivity, RadialGauge } from "@/components/charts";
|
||||||
import { GlassPanel } from "@/components/primitives";
|
import { GlassPanel } from "@/components/primitives";
|
||||||
@@ -29,7 +28,7 @@ import {
|
|||||||
} from "@/components/shared";
|
} from "@/components/shared";
|
||||||
import { useActivity, useStats, useTopReactions } from "@/hooks";
|
import { useActivity, useStats, useTopReactions } from "@/hooks";
|
||||||
import { useStaggerReveal } from "@/hooks/use-gsap-animation";
|
import { useStaggerReveal } from "@/hooks/use-gsap-animation";
|
||||||
import { formatNumber } from "@/lib/format";
|
import { formatNumber, formatRelativeTime } from "@/lib/format";
|
||||||
import type { DashboardStats } from "@/lib/types";
|
import type { DashboardStats } from "@/lib/types";
|
||||||
import { staggerDelay } from "@/lib/utils";
|
import { staggerDelay } from "@/lib/utils";
|
||||||
|
|
||||||
@@ -66,6 +65,7 @@ export function DashboardView({
|
|||||||
} = useStats(initialStats);
|
} = useStats(initialStats);
|
||||||
const { data: activity } = useActivity(14, initialActivity);
|
const { data: activity } = useActivity(14, initialActivity);
|
||||||
const { data: reactions } = useTopReactions();
|
const { data: reactions } = useTopReactions();
|
||||||
|
const [showAllReactions, setShowAllReactions] = useState(false);
|
||||||
const ambient = useAmbient();
|
const ambient = useAmbient();
|
||||||
|
|
||||||
const hudRef = useStaggerReveal<HTMLDivElement>(".linear-tile", {
|
const hudRef = useStaggerReveal<HTMLDivElement>(".linear-tile", {
|
||||||
@@ -160,12 +160,6 @@ export function DashboardView({
|
|||||||
hint={`${formatNumber(s.total_users)} total profiles`}
|
hint={`${formatNumber(s.total_users)} total profiles`}
|
||||||
icon={<Users className="size-4" />}
|
icon={<Users className="size-4" />}
|
||||||
/>
|
/>
|
||||||
<MetricTile
|
|
||||||
label="Voice Recordings"
|
|
||||||
value={formatNumber(s.total_voice_recordings)}
|
|
||||||
hint="Archived audio sessions"
|
|
||||||
icon={<Mic className="size-4" />}
|
|
||||||
/>
|
|
||||||
<MetricTile
|
<MetricTile
|
||||||
label="Mod Queue"
|
label="Mod Queue"
|
||||||
value={formatNumber(mod.pending + mod.processing)}
|
value={formatNumber(mod.pending + mod.processing)}
|
||||||
@@ -366,44 +360,64 @@ export function DashboardView({
|
|||||||
<SectionHeader
|
<SectionHeader
|
||||||
eyebrow="Engagement"
|
eyebrow="Engagement"
|
||||||
title="Top Reacted Messages"
|
title="Top Reacted Messages"
|
||||||
|
action={
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => setShowAllReactions((v) => !v)}
|
||||||
|
className="font-mono text-[10px] text-ink-muted transition-colors hover:text-ink"
|
||||||
|
>
|
||||||
|
{showAllReactions
|
||||||
|
? "SHOW TOP 4"
|
||||||
|
: `VIEW ALL ${reactions.length}`}
|
||||||
|
</button>
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
<div className="mt-3 space-y-2">
|
<div className="mt-3 space-y-2">
|
||||||
{reactions.slice(0, 4).map((r) => (
|
{(showAllReactions ? reactions : reactions.slice(0, 4)).map(
|
||||||
<div
|
(r) => (
|
||||||
key={r.message_id}
|
<div
|
||||||
className="hud-card flex items-start justify-between gap-2 p-2.5"
|
key={r.message_id}
|
||||||
>
|
className="hud-card flex items-start justify-between gap-2 p-2.5"
|
||||||
<div className="min-w-0 flex-1">
|
>
|
||||||
<span className="line-clamp-1 block text-xs text-ink-soft">
|
<div className="min-w-0 flex-1">
|
||||||
{r.content || "[Media/Attachment]"}
|
<span className="line-clamp-1 block text-xs text-ink-soft">
|
||||||
</span>
|
{r.content || "[Media/Attachment]"}
|
||||||
{r.username && (
|
|
||||||
<span className="mt-0.5 block font-mono text-[10px] text-ink-muted">
|
|
||||||
@{r.username}
|
|
||||||
{r.channel_name && ` in #${r.channel_name}`}
|
|
||||||
</span>
|
</span>
|
||||||
)}
|
<span className="mt-0.5 block font-mono text-[10px] text-ink-muted">
|
||||||
|
@{r.username || "unknown"}
|
||||||
|
{r.channel_name && ` in #${r.channel_name}`}
|
||||||
|
{r.created_at != null && (
|
||||||
|
<>
|
||||||
|
{" "}
|
||||||
|
·{" "}
|
||||||
|
<span suppressHydrationWarning>
|
||||||
|
{formatRelativeTime(r.created_at)}
|
||||||
|
</span>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div className="shrink-0 text-right">
|
||||||
|
<span className="font-mono text-xs font-semibold text-signal">
|
||||||
|
{formatNumber(r.reaction_count)}
|
||||||
|
</span>
|
||||||
|
{r.top_emojis && r.top_emojis.length > 0 && (
|
||||||
|
<div className="mt-0.5 flex gap-0.5">
|
||||||
|
{r.top_emojis.slice(0, 2).map((e) => (
|
||||||
|
<span
|
||||||
|
key={e.emoji}
|
||||||
|
className="text-[10px]"
|
||||||
|
title={`${e.emoji} ×${e.count}`}
|
||||||
|
>
|
||||||
|
{e.emoji}
|
||||||
|
</span>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="shrink-0 text-right">
|
),
|
||||||
<span className="font-mono text-xs font-semibold text-signal">
|
)}
|
||||||
{formatNumber(r.reaction_count)}
|
|
||||||
</span>
|
|
||||||
{r.top_emojis && r.top_emojis.length > 0 && (
|
|
||||||
<div className="mt-0.5 flex gap-0.5">
|
|
||||||
{r.top_emojis.map((e) => (
|
|
||||||
<span
|
|
||||||
key={e.emoji}
|
|
||||||
className="text-[10px]"
|
|
||||||
title={`${e.emoji} ×${e.count}`}
|
|
||||||
>
|
|
||||||
{e.emoji}
|
|
||||||
</span>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
</div>
|
||||||
</GlassPanel>
|
</GlassPanel>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -567,11 +567,6 @@
|
|||||||
100% { background-position: -200% 0; }
|
100% { background-position: -200% 0; }
|
||||||
}
|
}
|
||||||
|
|
||||||
@keyframes eq {
|
|
||||||
0%, 100% { transform: scaleY(0.35); }
|
|
||||||
50% { transform: scaleY(1); }
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes fade-up {
|
@keyframes fade-up {
|
||||||
from { opacity: 0; transform: translateY(6px); }
|
from { opacity: 0; transform: translateY(6px); }
|
||||||
to { opacity: 1; transform: translateY(0); }
|
to { opacity: 1; transform: translateY(0); }
|
||||||
@@ -634,12 +629,6 @@
|
|||||||
animation: shimmer 1.6s ease-in-out infinite;
|
animation: shimmer 1.6s ease-in-out infinite;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Live equalizer bars (mini-player / recording player). */
|
|
||||||
.animate-eq {
|
|
||||||
transform-origin: bottom;
|
|
||||||
animation: eq 0.9s ease-in-out infinite;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Entrance for popovers, toasts, mini-player. */
|
/* Entrance for popovers, toasts, mini-player. */
|
||||||
.animate-fade-up { animation: fade-up 0.18s cubic-bezier(0.16, 1, 0.3, 1); }
|
.animate-fade-up { animation: fade-up 0.18s cubic-bezier(0.16, 1, 0.3, 1); }
|
||||||
.animate-toast-in { animation: toast-in 0.2s cubic-bezier(0.16, 1, 0.3, 1); }
|
.animate-toast-in { animation: toast-in 0.2s cubic-bezier(0.16, 1, 0.3, 1); }
|
||||||
@@ -694,7 +683,6 @@
|
|||||||
.animate-spin-disc,
|
.animate-spin-disc,
|
||||||
.animate-pulse-ring,
|
.animate-pulse-ring,
|
||||||
.animate-shimmer,
|
.animate-shimmer,
|
||||||
.animate-eq,
|
|
||||||
.animate-fade-up,
|
.animate-fade-up,
|
||||||
.animate-toast-in,
|
.animate-toast-in,
|
||||||
.animate-stagger,
|
.animate-stagger,
|
||||||
|
|||||||
@@ -25,8 +25,7 @@ const bricolage = Bricolage_Grotesque({
|
|||||||
|
|
||||||
export const metadata: Metadata = {
|
export const metadata: Metadata = {
|
||||||
title: "GMW — Discord Moderation Console",
|
title: "GMW — Discord Moderation Console",
|
||||||
description:
|
description: "AI-powered Discord moderation console",
|
||||||
"AI-powered Discord moderation, voice monitoring, and media control console",
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export const viewport: Viewport = {
|
export const viewport: Viewport = {
|
||||||
|
|||||||
@@ -49,6 +49,15 @@ function severityTone(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function confidenceTone(confidence?: number | null) {
|
||||||
|
if (confidence == null) return null;
|
||||||
|
return confidence >= 0.75
|
||||||
|
? "bg-signal"
|
||||||
|
: confidence >= 0.5
|
||||||
|
? "bg-amber"
|
||||||
|
: "bg-vermilion";
|
||||||
|
}
|
||||||
|
|
||||||
export function LiveModerationFeed({
|
export function LiveModerationFeed({
|
||||||
actions,
|
actions,
|
||||||
}: {
|
}: {
|
||||||
@@ -115,8 +124,27 @@ export function LiveModerationFeed({
|
|||||||
className="mod-feed-item hud-card animate-stagger flex items-start gap-3 p-3 transition-all"
|
className="mod-feed-item hud-card animate-stagger flex items-start gap-3 p-3 transition-all"
|
||||||
style={staggerDelay(i)}
|
style={staggerDelay(i)}
|
||||||
>
|
>
|
||||||
<span className="flex size-7 shrink-0 items-center justify-center rounded-[6px] border border-hairline bg-surface-2">
|
<span
|
||||||
|
className={`relative flex size-7 shrink-0 items-center justify-center rounded-[6px] border ${
|
||||||
|
a.status === "failed"
|
||||||
|
? "border-vermilion/40 bg-vermilion/10"
|
||||||
|
: a.status === "pending"
|
||||||
|
? "border-amber/40 bg-amber/10"
|
||||||
|
: "border-hairline bg-surface-2"
|
||||||
|
}`}
|
||||||
|
>
|
||||||
{actionIcon(a.action_type)}
|
{actionIcon(a.action_type)}
|
||||||
|
{a.status && (
|
||||||
|
<span
|
||||||
|
className={`absolute -right-0.5 -top-0.5 size-2 rounded-full ring-2 ring-surface ${
|
||||||
|
a.status === "failed"
|
||||||
|
? "bg-vermilion"
|
||||||
|
: a.status === "pending"
|
||||||
|
? "bg-amber"
|
||||||
|
: "bg-signal"
|
||||||
|
}`}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</span>
|
</span>
|
||||||
|
|
||||||
<div className="min-w-0 flex-1">
|
<div className="min-w-0 flex-1">
|
||||||
@@ -151,6 +179,56 @@ export function LiveModerationFeed({
|
|||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{(a.confidence != null || a.flags?.length) && (
|
||||||
|
<div className="mt-1.5 flex flex-wrap items-center gap-x-3 gap-y-1">
|
||||||
|
{a.confidence != null && (
|
||||||
|
<span
|
||||||
|
className="flex items-center gap-1.5"
|
||||||
|
title={`Model confidence ${Math.round(a.confidence * 100)}%`}
|
||||||
|
>
|
||||||
|
<span className="font-mono text-[9px] tracking-wider text-ink-faint uppercase">
|
||||||
|
conf
|
||||||
|
</span>
|
||||||
|
<span className="inline-flex h-1 w-14 overflow-hidden rounded-full bg-surface-2">
|
||||||
|
<span
|
||||||
|
className={`h-full rounded-full ${confidenceTone(a.confidence) ?? "bg-ink-faint"}`}
|
||||||
|
style={{
|
||||||
|
width: `${Math.min(100, Math.round(a.confidence * 100))}%`,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</span>
|
||||||
|
<span className="font-mono text-[9px] text-ink-faint">
|
||||||
|
{Math.round(a.confidence * 100)}%
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
{a.flags && a.flags.length > 0 && (
|
||||||
|
<span className="flex flex-wrap items-center gap-1">
|
||||||
|
{a.flags.slice(0, 2).map((flag) => (
|
||||||
|
<Badge
|
||||||
|
key={flag}
|
||||||
|
tone={tone ?? "neutral"}
|
||||||
|
size="sm"
|
||||||
|
className="uppercase"
|
||||||
|
>
|
||||||
|
{flag}
|
||||||
|
</Badge>
|
||||||
|
))}
|
||||||
|
{a.flags.length > 2 && (
|
||||||
|
<span className="font-mono text-[9px] text-ink-faint">
|
||||||
|
+{a.flags.length - 2}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
{a.error && (
|
||||||
|
<span className="font-mono text-[9px] text-vermilion/80">
|
||||||
|
{a.error}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
<div className="mt-1.5 flex items-center gap-2 font-mono text-[10px] text-ink-faint">
|
<div className="mt-1.5 flex items-center gap-2 font-mono text-[10px] text-ink-faint">
|
||||||
<span>TARGET:</span>
|
<span>TARGET:</span>
|
||||||
<span className="text-ink-soft">
|
<span className="text-ink-soft">
|
||||||
|
|||||||
@@ -2,4 +2,3 @@ export { AreaActivity } from "./area-activity";
|
|||||||
export { Donut } from "./donut";
|
export { Donut } from "./donut";
|
||||||
export { RadialGauge } from "./radial-gauge";
|
export { RadialGauge } from "./radial-gauge";
|
||||||
export { Sparkline } from "./sparkline";
|
export { Sparkline } from "./sparkline";
|
||||||
export { Equalizer } from "./waveform";
|
|
||||||
@@ -1,41 +0,0 @@
|
|||||||
import { cn } from "@/lib/utils";
|
|
||||||
|
|
||||||
/** Live equalizer bars. `bars` are 0..1 levels. */
|
|
||||||
export function Equalizer({
|
|
||||||
bars,
|
|
||||||
color = "var(--color-signal)",
|
|
||||||
className,
|
|
||||||
}: {
|
|
||||||
bars: number[];
|
|
||||||
color?: string;
|
|
||||||
className?: string;
|
|
||||||
}) {
|
|
||||||
return (
|
|
||||||
<div className={cn("flex h-10 items-end gap-[3px]", className)}>
|
|
||||||
{bars.length === 0 ? (
|
|
||||||
<div className="flex w-full items-end gap-[3px]">
|
|
||||||
{Array.from({ length: 28 }).map((_, i) => (
|
|
||||||
<span
|
|
||||||
key={`eq-${i}`}
|
|
||||||
className="flex-1 rounded-full bg-hairline"
|
|
||||||
style={{ height: "12%" }}
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
bars.map((b, i) => (
|
|
||||||
<span
|
|
||||||
key={`bar-${i}`}
|
|
||||||
className="flex-1 rounded-full"
|
|
||||||
style={{
|
|
||||||
height: `${Math.max(6, b * 100)}%`,
|
|
||||||
background: color,
|
|
||||||
boxShadow: b > 0.05 ? `0 0 8px ${color}` : "none",
|
|
||||||
transition: "height 90ms linear",
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
))
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -75,7 +75,7 @@ export function SkeletonRows({
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Hero block placeholder (dashboard / media "now playing"). */
|
/** Hero block placeholder (dashboard headline). */
|
||||||
export function SkeletonHero({ className }: { className?: string }) {
|
export function SkeletonHero({ className }: { className?: string }) {
|
||||||
return (
|
return (
|
||||||
<GlassPanel glow className={cn("relative overflow-hidden", className)}>
|
<GlassPanel glow className={cn("relative overflow-hidden", className)}>
|
||||||
|
|||||||
@@ -1,10 +1,7 @@
|
|||||||
import {
|
import {
|
||||||
Headphones,
|
|
||||||
LayoutDashboard,
|
LayoutDashboard,
|
||||||
type LucideIcon,
|
type LucideIcon,
|
||||||
MessageSquare,
|
MessageSquare,
|
||||||
Mic,
|
|
||||||
Music,
|
|
||||||
Search,
|
Search,
|
||||||
Shield,
|
Shield,
|
||||||
Users,
|
Users,
|
||||||
@@ -40,24 +37,6 @@ export const navItems: NavItem[] = [
|
|||||||
icon: Users,
|
icon: Users,
|
||||||
matchPrefix: "/users",
|
matchPrefix: "/users",
|
||||||
},
|
},
|
||||||
{
|
|
||||||
href: "/voice",
|
|
||||||
label: "Voice",
|
|
||||||
icon: Mic,
|
|
||||||
matchPrefix: "/voice",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
href: "/media",
|
|
||||||
label: "Media",
|
|
||||||
icon: Music,
|
|
||||||
matchPrefix: "/media",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
href: "/recordings",
|
|
||||||
label: "Recordings",
|
|
||||||
icon: Headphones,
|
|
||||||
matchPrefix: "/recordings",
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
href: "/moderation",
|
href: "/moderation",
|
||||||
label: "Moderation",
|
label: "Moderation",
|
||||||
|
|||||||
@@ -7,7 +7,6 @@ export interface DashboardStats {
|
|||||||
total_clean: number;
|
total_clean: number;
|
||||||
total_warned: number;
|
total_warned: number;
|
||||||
total_error: number;
|
total_error: number;
|
||||||
total_voice_recordings: number;
|
|
||||||
total_profiles: number;
|
total_profiles: number;
|
||||||
today_messages: number;
|
today_messages: number;
|
||||||
today_flagged: number;
|
today_flagged: number;
|
||||||
|
|||||||
@@ -2,13 +2,9 @@ export type DashboardTab = "messages" | "live" | "dashboard";
|
|||||||
|
|
||||||
export interface UiState {
|
export interface UiState {
|
||||||
selected_guild?: string | null;
|
selected_guild?: string | null;
|
||||||
selected_voice_guild?: string | null;
|
|
||||||
selected_voice_channel?: string | null;
|
|
||||||
selected_text_guild?: string | null;
|
selected_text_guild?: string | null;
|
||||||
selected_text_channel?: string | null;
|
selected_text_channel?: string | null;
|
||||||
active_tab?: DashboardTab | null;
|
active_tab?: DashboardTab | null;
|
||||||
is_listening?: boolean | null;
|
|
||||||
is_streaming?: boolean | null;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ChatbotResponse {
|
export interface ChatbotResponse {
|
||||||
|
|||||||
@@ -9,24 +9,20 @@ import {
|
|||||||
useRef,
|
useRef,
|
||||||
useState,
|
useState,
|
||||||
} from "react";
|
} from "react";
|
||||||
import { useSWRConfig } from "swr";
|
|
||||||
import { toast } from "@/components/primitives";
|
import { toast } from "@/components/primitives";
|
||||||
import { WsConnection } from "./connection";
|
import { WsConnection } from "./connection";
|
||||||
import type { PcmChunk, WsEventHandler, WsEventType, WsStatus } from "./types";
|
import type { WsEventHandler, WsEventType, WsStatus } from "./types";
|
||||||
|
|
||||||
interface WsContextValue {
|
interface WsContextValue {
|
||||||
status: WsStatus;
|
status: WsStatus;
|
||||||
connect: () => void;
|
connect: () => void;
|
||||||
disconnect: () => void;
|
disconnect: () => void;
|
||||||
sendText: (text: string) => void;
|
sendText: (text: string) => void;
|
||||||
sendBinary: (data: ArrayBufferLike) => void;
|
|
||||||
/** Subscribe to a typed WS event. Returns unsubscribe function. */
|
/** Subscribe to a typed WS event. Returns unsubscribe function. */
|
||||||
on: <E extends WsEventType>(
|
on: <E extends WsEventType>(
|
||||||
eventType: E,
|
eventType: E,
|
||||||
handler: WsEventHandler<E>,
|
handler: WsEventHandler<E>,
|
||||||
) => () => void;
|
) => () => void;
|
||||||
/** Subscribe to binary PCM events. Returns unsubscribe function. */
|
|
||||||
onPcm: (handler: (chunk: PcmChunk) => void) => () => void;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const WsContext = createContext<WsContextValue | null>(null);
|
const WsContext = createContext<WsContextValue | null>(null);
|
||||||
@@ -40,7 +36,6 @@ export function WsProvider({
|
|||||||
}) {
|
}) {
|
||||||
const connRef = useRef<WsConnection | null>(null);
|
const connRef = useRef<WsConnection | null>(null);
|
||||||
const [status, setStatus] = useState<WsStatus>("disconnected");
|
const [status, setStatus] = useState<WsStatus>("disconnected");
|
||||||
const { mutate } = useSWRConfig();
|
|
||||||
// Tracks whether we've ever been connected — used to suppress the
|
// Tracks whether we've ever been connected — used to suppress the
|
||||||
// "reconnecting" toast on initial page load.
|
// "reconnecting" toast on initial page load.
|
||||||
const wasConnected = useRef(false);
|
const wasConnected = useRef(false);
|
||||||
@@ -48,7 +43,6 @@ export function WsProvider({
|
|||||||
// Event handler registry — Ref so listeners survive re-renders without reconnect
|
// Event handler registry — Ref so listeners survive re-renders without reconnect
|
||||||
// Using unknown as internal store; typed at the subscribe interface
|
// Using unknown as internal store; typed at the subscribe interface
|
||||||
const handlersRef = useRef<Record<string, Set<(data: unknown) => void>>>({});
|
const handlersRef = useRef<Record<string, Set<(data: unknown) => void>>>({});
|
||||||
const pcmHandlersRef = useRef<Set<(chunk: PcmChunk) => void>>(new Set());
|
|
||||||
|
|
||||||
const handleJsonEvent = useCallback((json: string) => {
|
const handleJsonEvent = useCallback((json: string) => {
|
||||||
try {
|
try {
|
||||||
@@ -66,17 +60,6 @@ export function WsProvider({
|
|||||||
}
|
}
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const handleBinaryEvent = useCallback((buffer: ArrayBuffer) => {
|
|
||||||
if (buffer.byteLength < 4 || pcmHandlersRef.current.size === 0) return;
|
|
||||||
|
|
||||||
const view = new DataView(buffer);
|
|
||||||
const userIdHash = view.getUint32(0, true);
|
|
||||||
const samples = new Int16Array(buffer, 4);
|
|
||||||
|
|
||||||
const chunk: PcmChunk = { userIdHash, samples };
|
|
||||||
pcmHandlersRef.current.forEach((h) => h(chunk));
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const conn = new WsConnection(url);
|
const conn = new WsConnection(url);
|
||||||
connRef.current = conn;
|
connRef.current = conn;
|
||||||
@@ -97,9 +80,6 @@ export function WsProvider({
|
|||||||
wasConnected.current = false;
|
wasConnected.current = false;
|
||||||
} else if (s === "connected") {
|
} else if (s === "connected") {
|
||||||
wasConnected.current = true;
|
wasConnected.current = true;
|
||||||
// After reconnect, force-refetch voice status immediately so
|
|
||||||
// the UI converges faster instead of waiting up to 4s for SWR poll.
|
|
||||||
void mutate(["voice-status"]);
|
|
||||||
} else if (s === "error" && !wasConnected.current) {
|
} else if (s === "error" && !wasConnected.current) {
|
||||||
toast({
|
toast({
|
||||||
title: "Connection error",
|
title: "Connection error",
|
||||||
@@ -112,8 +92,6 @@ export function WsProvider({
|
|||||||
const unsubEvent = conn.onEvent((event) => {
|
const unsubEvent = conn.onEvent((event) => {
|
||||||
if (event.type === "text") {
|
if (event.type === "text") {
|
||||||
handleJsonEvent(event.data);
|
handleJsonEvent(event.data);
|
||||||
} else {
|
|
||||||
handleBinaryEvent(event.data);
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -126,7 +104,7 @@ export function WsProvider({
|
|||||||
unsubEvent();
|
unsubEvent();
|
||||||
};
|
};
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
}, [url, handleBinaryEvent, handleJsonEvent, mutate]);
|
}, [url, handleJsonEvent]);
|
||||||
|
|
||||||
const subscribe = useCallback(
|
const subscribe = useCallback(
|
||||||
<E extends WsEventType>(_eventType: E, handler: WsEventHandler<E>) => {
|
<E extends WsEventType>(_eventType: E, handler: WsEventHandler<E>) => {
|
||||||
@@ -144,23 +122,12 @@ export function WsProvider({
|
|||||||
[],
|
[],
|
||||||
);
|
);
|
||||||
|
|
||||||
const subscribePcm = useCallback((handler: (chunk: PcmChunk) => void) => {
|
|
||||||
pcmHandlersRef.current.add(handler);
|
|
||||||
return () => {
|
|
||||||
pcmHandlersRef.current.delete(handler);
|
|
||||||
};
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const connect = useCallback(() => connRef.current?.connect(), []);
|
const connect = useCallback(() => connRef.current?.connect(), []);
|
||||||
const disconnect = useCallback(() => connRef.current?.disconnect(), []);
|
const disconnect = useCallback(() => connRef.current?.disconnect(), []);
|
||||||
const sendText = useCallback(
|
const sendText = useCallback(
|
||||||
(text: string) => connRef.current?.sendText(text),
|
(text: string) => connRef.current?.sendText(text),
|
||||||
[],
|
[],
|
||||||
);
|
);
|
||||||
const sendBinary = useCallback(
|
|
||||||
(data: ArrayBufferLike) => connRef.current?.sendBinary(data),
|
|
||||||
[],
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<WsContext.Provider
|
<WsContext.Provider
|
||||||
@@ -169,9 +136,7 @@ export function WsProvider({
|
|||||||
connect,
|
connect,
|
||||||
disconnect,
|
disconnect,
|
||||||
sendText,
|
sendText,
|
||||||
sendBinary,
|
|
||||||
on: subscribe,
|
on: subscribe,
|
||||||
onPcm: subscribePcm,
|
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{children}
|
{children}
|
||||||
|
|||||||
@@ -62,10 +62,3 @@ export type WsEventType = keyof WsEventMap;
|
|||||||
export type WsEventHandler<E extends WsEventType = WsEventType> = (
|
export type WsEventHandler<E extends WsEventType = WsEventType> = (
|
||||||
data: WsEventMap[E],
|
data: WsEventMap[E],
|
||||||
) => void;
|
) => void;
|
||||||
|
|
||||||
// ── Binary PCM ─────────────────────────────────────────────
|
|
||||||
|
|
||||||
export interface PcmChunk {
|
|
||||||
userIdHash: number;
|
|
||||||
samples: Int16Array;
|
|
||||||
}
|
|
||||||
Reference in new issue
Block a user