fix(voice): activity tab rendered a permanently empty chart

VoiceActivityTimeline was never given a data prop — the Activity tab always
showed an empty Recharts bar chart while the connection tab already had live
speaker state. Replace the dead chart with a live speaker/activity list fed
from the same WebSocket data, so the tab reflects real state instead of
misleading empty bars.
This commit is contained in:
asepharyana
2026-08-02 10:38:02 +07:00
parent 25f6609a9f
commit ef4281cd1f
2 changed files with 66 additions and 59 deletions
@@ -148,7 +148,7 @@ export default function VoicePage() {
</div> </div>
)} )}
{tab === "activity" && <VoiceActivityTimeline />} {tab === "activity" && <VoiceActivityTimeline data={speakers} />}
</div> </div>
); );
} }
@@ -1,22 +1,30 @@
"use client"; "use client";
import { import { Mic, MicOff } from "lucide-react";
Bar,
BarChart,
ResponsiveContainer,
Tooltip,
XAxis,
YAxis,
} from "recharts";
import { GlassCard } from "@/components/glass/card"; import { GlassCard } from "@/components/glass/card";
import { useMounted } from "@/lib/hooks/use-mounted"; import type { ActiveSpeaker } from "@/lib/types";
interface ActivityTimelineProps { interface ActivityTimelineProps {
data?: { user: string; duration: number }[]; data?: ActiveSpeaker[];
} }
/**
* Voice Activity — live view of everyone currently in the monitored voice
* channel and whether they are speaking right now.
*
* Previously this rendered a Recharts bar chart fed from a `{user, duration}`
* prop that NO caller ever supplied, so the Activity tab always showed an
* empty, misleading chart. It now renders real live speaker state from the
* WebSocket (same source as the Connection tab's waveform).
*/
export function VoiceActivityTimeline({ data = [] }: ActivityTimelineProps) { export function VoiceActivityTimeline({ data = [] }: ActivityTimelineProps) {
const mounted = useMounted(); const sorted = [...data].sort((a, b) =>
a.speaking === b.speaking
? String(a.username).localeCompare(b.username)
: a.speaking
? -1
: 1,
);
return ( return (
<GlassCard variant="base"> <GlassCard variant="base">
@@ -24,54 +32,53 @@ export function VoiceActivityTimeline({ data = [] }: ActivityTimelineProps) {
<span className="text-xs font-semibold tracking-wide uppercase text-text-secondary"> <span className="text-xs font-semibold tracking-wide uppercase text-text-secondary">
Voice Activity Voice Activity
</span> </span>
<span className="text-[10px] text-text-secondary/50 ml-auto">
{sorted.length} speaker{sorted.length !== 1 ? "s" : ""} · live
</span>
</div> </div>
<div className="h-40">
{mounted ? ( {sorted.length === 0 ? (
<ResponsiveContainer <div className="flex flex-col items-center justify-center py-12 text-center">
width="100%" <MicOff className="size-8 text-text-secondary/30 mb-2" />
height={160} <p className="text-xs text-text-secondary/60">
minWidth={0} No speakers in the monitored voice channel.
minHeight={0} </p>
> <p className="mt-1 text-[10px] text-text-secondary/40">
<BarChart data={data} layout="vertical"> Connect to a voice channel to see live activity here.
<XAxis </p>
type="number" </div>
axisLine={false} ) : (
tickLine={false} <div className="space-y-1.5">
tick={{ fill: "oklch(0.55 0.02 245)", fontSize: 10 }} {sorted.map((s) => (
/> <div
<YAxis key={s.userId}
type="category" className="flex items-center gap-2 rounded-lg border border-border/40 bg-card/40 px-3 py-2"
dataKey="user" >
axisLine={false} {s.speaking ? (
tickLine={false} <Mic className="size-3.5 text-primary shrink-0" />
tick={{ fill: "oklch(0.55 0.02 245)", fontSize: 10 }} ) : (
width={80} <MicOff className="size-3.5 text-text-secondary/40 shrink-0" />
/> )}
<Tooltip <span
contentStyle={{ className={`truncate text-sm ${
background: "oklch(0.11 0.02 245 / 0.9)", s.speaking
border: "1px solid oklch(1 0 0 / 0.08)", ? "text-text-primary font-medium"
borderRadius: 8, : "text-text-secondary/70"
fontSize: 12, }`}
color: "oklch(0.93 0.01 245)", >
}} {s.username}
formatter={(value) => [ </span>
`${(Number(value) / 60).toFixed(1)}m`, <span
"Duration", className={`ml-auto shrink-0 text-[9px] font-semibold uppercase tracking-widest ${
]} s.speaking ? "text-primary" : "text-text-secondary/40"
/> }`}
<Bar >
dataKey="duration" {s.speaking ? "Speaking" : "Listening"}
fill="var(--color-primary)" </span>
radius={[0, 4, 4, 0]} </div>
/> ))}
</BarChart> </div>
</ResponsiveContainer> )}
) : (
<div className="h-full w-full animate-pulse rounded-md bg-card/40" />
)}
</div>
</GlassCard> </GlassCard>
); );
} }