diff --git a/services/frontend/src/App.tsx b/services/frontend/src/App.tsx index 53db327..e521746 100644 --- a/services/frontend/src/App.tsx +++ b/services/frontend/src/App.tsx @@ -49,20 +49,25 @@ export default function App() { // Update speaker list from incremental voice_active_user events const updateSpeakerList = ( prev: (ActiveSpeaker & { heardAt?: number })[], - data: Partial & { userId?: string; id?: string; speaking: boolean }, + data: Partial & { + userId?: string; + id?: string; + speaking: boolean; + }, ): (ActiveSpeaker & { heardAt?: number })[] => { const key = data.userId ?? data.id; if (!key) return prev; const now = Date.now(); - const idx = prev.findIndex( - (s) => (s.userId ?? s.id) === key, - ); + const idx = prev.findIndex((s) => (s.userId ?? s.id) === key); if (idx >= 0) { const next = [...prev]; next[idx] = { ...next[idx], ...data, heardAt: now }; return next; } - return [...prev, { ...data, heardAt: now } as ActiveSpeaker & { heardAt?: number }]; + return [ + ...prev, + { ...data, heardAt: now } as ActiveSpeaker & { heardAt?: number }, + ]; }; const socket = useDashboardSocket({ @@ -75,7 +80,13 @@ export default function App() { })), ), onVoiceActiveUser: (data) => { - const d = data as { userId?: string; id?: string; username: string; avatar: string; speaking: boolean }; + const d = data as { + userId?: string; + id?: string; + username: string; + avatar: string; + speaking: boolean; + }; if (d.userId) audio.registerUserId(d.userId); setActiveSpeakers((prev) => updateSpeakerList( @@ -191,7 +202,12 @@ export default function App() { // Push-to-Talk — hold Space to transmit useEffect(() => { const handleKeyDown = (e: KeyboardEvent) => { - if (e.target instanceof HTMLInputElement || e.target instanceof HTMLTextAreaElement || e.target instanceof HTMLSelectElement) return; + if ( + e.target instanceof HTMLInputElement || + e.target instanceof HTMLTextAreaElement || + e.target instanceof HTMLSelectElement + ) + return; if (e.code === "Space" && !transmit.isStreaming && e.repeat === false) { e.preventDefault(); transmit.startTransmit().catch(() => undefined); diff --git a/services/frontend/src/features/dashboard/components/ChannelSummaryList.tsx b/services/frontend/src/features/dashboard/components/ChannelSummaryList.tsx index e306a67..72998b1 100644 --- a/services/frontend/src/features/dashboard/components/ChannelSummaryList.tsx +++ b/services/frontend/src/features/dashboard/components/ChannelSummaryList.tsx @@ -1,7 +1,7 @@ import { Hash } from "lucide-react"; import type { DashboardChannel } from "../../../shared/api/client"; -import { SummaryList } from "../../../shared/ui"; import type { SummaryItem } from "../../../shared/ui"; +import { SummaryList } from "../../../shared/ui"; interface ChannelSummaryListProps { channels: DashboardChannel[]; diff --git a/services/frontend/src/features/dashboard/components/DashboardStats.tsx b/services/frontend/src/features/dashboard/components/DashboardStats.tsx index a63b4cc..b5fe7c0 100644 --- a/services/frontend/src/features/dashboard/components/DashboardStats.tsx +++ b/services/frontend/src/features/dashboard/components/DashboardStats.tsx @@ -10,6 +10,7 @@ import { Users, } from "lucide-react"; import { cardItem, cardStagger } from "../../../shared/hooks/useFramerStagger"; +import { useUIState } from "../../../shared/hooks/useUIState"; import { cn } from "../../../shared/lib/utils"; import { Card, @@ -20,7 +21,6 @@ import { StatusBadge, } from "../../../shared/ui"; import { useDashboardStats } from "../hooks/useDashboard"; -import { useUIState } from "../../../shared/hooks/useUIState"; export function DashboardStatsContent() { const { stats, loading, error, refetch } = useDashboardStats(); @@ -129,7 +129,11 @@ export function DashboardStatsContent() { {cards.map((card) => ( diff --git a/services/frontend/src/features/dashboard/components/UserSummaryList.tsx b/services/frontend/src/features/dashboard/components/UserSummaryList.tsx index 06cfdd9..97182dd 100644 --- a/services/frontend/src/features/dashboard/components/UserSummaryList.tsx +++ b/services/frontend/src/features/dashboard/components/UserSummaryList.tsx @@ -1,7 +1,7 @@ import { User } from "lucide-react"; import type { DashboardUser } from "../../../shared/api/client"; -import { SummaryList } from "../../../shared/ui"; import type { SummaryItem } from "../../../shared/ui"; +import { SummaryList } from "../../../shared/ui"; interface UserSummaryListProps { users: DashboardUser[]; diff --git a/services/frontend/src/features/live/components/MusicSubPanel.tsx b/services/frontend/src/features/live/components/MusicSubPanel.tsx index ab6bd35..1b19855 100644 --- a/services/frontend/src/features/live/components/MusicSubPanel.tsx +++ b/services/frontend/src/features/live/components/MusicSubPanel.tsx @@ -78,7 +78,11 @@ export function MusicSubPanel({ onClick={handleMute} className="shrink-0 text-muted-foreground hover:text-foreground" > - {muted ? : } + {muted ? ( + + ) : ( + + )} (null); const [deletingIds, setDeletingIds] = useState>(new Set()); - const loadRecordings = useCallback(async ( - opts?: { signal?: AbortSignal }, - ) => { - try { - setLoading(true); - setError(null); - const data = await listRecordings({ limit: 50 }); - if (!opts?.signal?.aborted) { - setRecordings(data.items); - setNextCursor(data.nextCursor); - setHasMore(data.hasMore); + const loadRecordings = useCallback( + async (opts?: { signal?: AbortSignal }) => { + try { + setLoading(true); + setError(null); + const data = await listRecordings({ limit: 50 }); + if (!opts?.signal?.aborted) { + setRecordings(data.items); + setNextCursor(data.nextCursor); + setHasMore(data.hasMore); + } + } catch (err) { + if (!opts?.signal?.aborted) + setError(err instanceof Error ? err.message : String(err)); + } finally { + if (!opts?.signal?.aborted) setLoading(false); } - } catch (err) { - if (!opts?.signal?.aborted) - setError(err instanceof Error ? err.message : String(err)); - } finally { - if (!opts?.signal?.aborted) setLoading(false); - } - }, []); + }, + [], + ); const loadMore = useCallback(async () => { if (!nextCursor || loadingMore) return; @@ -105,11 +106,7 @@ export function RecordingsSubPanel() {
{error}
-
@@ -185,23 +182,26 @@ export function RecordingsSubPanel() {
{rec.download_url && (
- +
)} ))} - {hasMore && ( -
- -
- )} + {hasMore && ( +
+ +
+ )} ); } diff --git a/services/frontend/src/features/live/components/WaveformPlayer.tsx b/services/frontend/src/features/live/components/WaveformPlayer.tsx index 88dddd4..5a7418b 100644 --- a/services/frontend/src/features/live/components/WaveformPlayer.tsx +++ b/services/frontend/src/features/live/components/WaveformPlayer.tsx @@ -126,8 +126,9 @@ export function WaveformPlayer({ downloadUrl, filename }: WaveformPlayerProps) { if (!playing || !decodedRef.current) return; const tick = () => { + if (!audioContextRef.current) return; const elapsed = - audioContextRef.current!.currentTime - startTimeRef.current; + audioContextRef.current.currentTime - startTimeRef.current; const progress = (elapsed + startOffsetRef.current) / durationRef.current; drawWaveform(Math.min(1, Math.max(0, progress))); @@ -214,9 +215,7 @@ export function WaveformPlayer({ downloadUrl, filename }: WaveformPlayerProps) { ); if (loading) { - return ( -
- ); + return
; } if (error) { @@ -236,7 +235,11 @@ export function WaveformPlayer({ downloadUrl, filename }: WaveformPlayerProps) { onClick={handleTogglePlay} className="shrink-0 rounded-full bg-primary p-1.5 text-primary-foreground hover:bg-primary/90" > - {playing ? : } + {playing ? ( + + ) : ( + + )}
- {(["all", "analyzed", "clean", "flagged", "error", "pending"] as AiFilter[]).map( - (f) => ( - - ), - )} + {( + [ + "all", + "analyzed", + "clean", + "flagged", + "error", + "pending", + ] as AiFilter[] + ).map((f) => ( + + ))}
diff --git a/services/frontend/src/shared/hooks/useAudioPlayback.ts b/services/frontend/src/shared/hooks/useAudioPlayback.ts index f13b615..7377a8e 100644 --- a/services/frontend/src/shared/hooks/useAudioPlayback.ts +++ b/services/frontend/src/shared/hooks/useAudioPlayback.ts @@ -1,5 +1,11 @@ // ─── Audio playback hook — receives PCM from WebSocket and plays through Web Audio API ── -import { useCallback, useEffect, useRef, useState, type RefObject } from "react"; +import { + type RefObject, + useCallback, + useEffect, + useRef, + useState, +} from "react"; import { createLogger } from "../lib/logger.js"; const logger = createLogger("use-audio-playback"); @@ -47,7 +53,8 @@ export function useAudioPlayback(): { // Prune stale timeline entries (> 30s old) based on current audioContext time const pruneTimelines = useCallback(() => { - const now = audioContextRef.current?.currentTime ?? performance.now() / 1000; + const now = + audioContextRef.current?.currentTime ?? performance.now() / 1000; for (const [userId, endTime] of userTimelinesRef.current) { if (endTime + 30 < now) userTimelinesRef.current.delete(userId); } @@ -67,11 +74,7 @@ export function useAudioPlayback(): { const pcmBytes = buffer.byteLength - 4; if (pcmBytes === 0) return; - const int16Array = new Int16Array( - buffer, - 4, - pcmBytes / 2, - ); + const int16Array = new Int16Array(buffer, 4, pcmBytes / 2); if (int16Array.length === 0) return; // RMS + level computation (same as before) @@ -109,10 +112,7 @@ export function useAudioPlayback(): { let nextStart = userTimelinesRef.current.get(userId) || 0; if (nextStart < currentTime) nextStart = currentTime + 0.05; source.start(nextStart); - userTimelinesRef.current.set( - userId, - nextStart + audioBuffer.duration, - ); + userTimelinesRef.current.set(userId, nextStart + audioBuffer.duration); pruneTimelines(); }, [isListening, pruneTimelines], @@ -233,4 +233,3 @@ function fnv1a32(str: string): number { } return hash >>> 0; } - diff --git a/services/frontend/src/shared/hooks/useAudioTransmit.ts b/services/frontend/src/shared/hooks/useAudioTransmit.ts index 5fada86..92d238c 100644 --- a/services/frontend/src/shared/hooks/useAudioTransmit.ts +++ b/services/frontend/src/shared/hooks/useAudioTransmit.ts @@ -41,11 +41,9 @@ function sendWsCommand( return false; } -export function useAudioTransmit( - socketRef: { - readonly current: WebSocket | null; - }, -): { +export function useAudioTransmit(socketRef: { + readonly current: WebSocket | null; +}): { isStreaming: boolean; micError: string | null; micLevel: number; @@ -195,5 +193,14 @@ export function useAudioTransmit( } }, [isStreaming, startTransmit, stopTransmit, start]); - return { isStreaming, micError, micLevel, toggle, stopTransmit, startTransmit, stop, start }; + return { + isStreaming, + micError, + micLevel, + toggle, + stopTransmit, + startTransmit, + stop, + start, + }; } diff --git a/services/frontend/src/widgets/DashboardLayout.tsx b/services/frontend/src/widgets/DashboardLayout.tsx index b8d2820..8741ab3 100644 --- a/services/frontend/src/widgets/DashboardLayout.tsx +++ b/services/frontend/src/widgets/DashboardLayout.tsx @@ -1,7 +1,10 @@ import { motion } from "framer-motion"; import type { ReactNode } from "react"; -import type { DashboardTab } from "../shared/api/client"; -import type { MessageRecord, VoiceStatus } from "../shared/api/client"; +import type { + DashboardTab, + MessageRecord, + VoiceStatus, +} from "../shared/api/client"; import { fadeSlideUp } from "../shared/hooks/useFramerStagger"; import type { WsStatus } from "../shared/ws/socket"; import { Header } from "./Header"; diff --git a/services/frontend/src/widgets/Header.tsx b/services/frontend/src/widgets/Header.tsx index 1c052e4..428e664 100644 --- a/services/frontend/src/widgets/Header.tsx +++ b/services/frontend/src/widgets/Header.tsx @@ -1,7 +1,6 @@ import { motion } from "framer-motion"; import { Wifi, WifiOff } from "lucide-react"; -import type { DashboardTab } from "../shared/api/client"; -import type { VoiceStatus } from "../shared/api/client"; +import type { DashboardTab, VoiceStatus } from "../shared/api/client"; import { fadeSlideUp } from "../shared/hooks/useFramerStagger"; import { cn } from "../shared/lib/utils"; import { Badge } from "../shared/ui"; diff --git a/services/frontend/src/widgets/Sidebar.tsx b/services/frontend/src/widgets/Sidebar.tsx index 3821041..b2f2342 100644 --- a/services/frontend/src/widgets/Sidebar.tsx +++ b/services/frontend/src/widgets/Sidebar.tsx @@ -1,7 +1,6 @@ import { motion } from "framer-motion"; import { LayoutDashboard, MessageSquare, Radio } from "lucide-react"; -import type { DashboardTab } from "../shared/api/client"; -import type { MessageRecord } from "../shared/api/client"; +import type { DashboardTab, MessageRecord } from "../shared/api/client"; import { useMascotChat } from "../shared/hooks/useMascotChat"; import { cn } from "../shared/lib/utils"; import { MascotChatbot } from "./mascot/MascotChatbot";