import { Component, lazy, Suspense, useEffect, useMemo, useState } from "react"; import { AuthOverlay } from "./features/auth"; import { LivePanel } from "./features/live"; import { useMediaControl } from "./features/live/hooks/useMediaControl"; import { useVoiceControl } from "./features/live/hooks/useVoiceControl"; import { MessagesPanel } from "./features/messages"; import { mergeMessages, useMessages, } from "./features/messages/hooks/useMessages"; import { type ActiveSpeaker, getAppConfig, type MediaState, type MessageRecord, } from "./shared/api/client"; import { useAudioPlayback } from "./shared/hooks/useAudioPlayback"; import { useAudioTransmit } from "./shared/hooks/useAudioTransmit"; import { useUIState } from "./shared/hooks/useUIState"; import { Skeleton } from "./shared/ui"; import { MobileTabBar } from "./shared/ui/MobileTabBar"; import { useDashboardSocket } from "./shared/ws/socket"; import { DashboardLayout } from "./widgets/DashboardLayout"; const AnalyticsPanel = lazy(() => import("./features/analytics").then((module) => ({ default: module.AnalyticsPanel, })), ); class AnalyticsErrorBoundary extends Component< { children: React.ReactNode }, { hasError: boolean } > { state = { hasError: false }; static getDerivedStateFromError() { return { hasError: true }; } override render() { if (this.state.hasError) { return (
Analytics failed to load. The rest of the dashboard is still available.
); } return this.props.children; } } export default function App() { const { uiState, patchUIState } = useUIState(); const voice = useVoiceControl(); const media = useMediaControl(); const messages = useMessages(); const [activeSpeakers, setActiveSpeakers] = useState([]); const [isAuthenticated, setIsAuthenticated] = useState( !!localStorage.getItem("admin-password"), ); const [monitorGuildId, setMonitorGuildId] = useState(""); const audio = useAudioPlayback(); const activeTab = uiState.activeTab || "live"; const selectedVoiceGuild = uiState.selectedVoiceGuild || uiState.selectedGuild || ""; const selectedTextGuild = monitorGuildId || uiState.selectedTextGuild || uiState.selectedGuild || ""; const selectedTextChannel = uiState.selectedTextChannel || ""; const monitorGuild = useMemo( () => monitorGuildId ? voice.guilds.find((g) => g.id === monitorGuildId) : undefined, [monitorGuildId, voice.guilds], ); const socket = useDashboardSocket({ onBinary: audio.handleIncomingPcm, onUserState: (users) => setActiveSpeakers(users as ActiveSpeaker[]), onMessageCreated: (m) => messages.setMessages((prev) => mergeMessages(prev, [m as MessageRecord])), onMessageUpdated: (m) => { const d = m as Partial & { id: string }; messages.setMessages((prev) => prev.map((i) => (i.id === d.id ? { ...i, ...d } : i)), ); }, onMessageDeleted: (m) => { const d = m as { id: string }; messages.setMessages((prev) => prev.map((i) => i.id === d.id ? { ...i, type: "deleted" as const } : i, ), ); }, onMessageAnalyzed: (m) => messages.setMessages((prev) => mergeMessages(prev, [m as MessageRecord])), onAttachmentUploaded: () => messages.fetchMessages(selectedTextChannel).catch(() => undefined), onMediaState: (state) => media.setMediaState(state as MediaState), onVoiceRecordingUploaded: (d) => window.dispatchEvent( new CustomEvent("voice_recording_uploaded", { detail: d }), ), }); const transmit = useAudioTransmit(socket.socketRef); useEffect(() => { getAppConfig() .then((c) => { if (c.monitorGuildId) { setMonitorGuildId(c.monitorGuildId); patchUIState({ selectedTextGuild: c.monitorGuildId, selectedAnalyticsGuild: c.monitorGuildId, selectedTextChannel: "", selectedAnalyticsChannel: "", }); } }) .catch(() => undefined); }, [patchUIState]); useEffect(() => { if (selectedVoiceGuild) voice.loadVoiceChannels(selectedVoiceGuild).catch(() => undefined); }, [selectedVoiceGuild, voice.loadVoiceChannels]); useEffect(() => { if (monitorGuildId) voice.loadTextTargets(monitorGuildId).catch(() => undefined); }, [monitorGuildId, voice.loadTextTargets]); useEffect(() => { if (selectedTextChannel) messages.fetchMessages(selectedTextChannel).catch(() => undefined); }, [selectedTextChannel, messages.fetchMessages]); // Periodic refetch — ensures dashboard stays in sync even if WS events were missed useEffect(() => { if (!selectedTextChannel) return; const interval = setInterval(() => { messages.fetchMessages(selectedTextChannel).catch(() => undefined); }, 15_000); // every 15s (longer than WS, shorter than stale cache) return () => clearInterval(interval); }, [selectedTextChannel, messages.fetchMessages]); return ( patchUIState({ activeTab: tab })} > {activeTab === "live" ? ( !isAuthenticated ? ( setIsAuthenticated(true)} /> ) : ( patchUIState({ selectedVoiceGuild: id, selectedVoiceChannel: "" }) } onChannelChange={(id) => patchUIState({ selectedVoiceChannel: id })} onJoin={() => voice.joinVoice( selectedVoiceGuild, uiState.selectedVoiceChannel || "", ) } onDisconnect={() => voice.leaveVoice()} onListenToggle={audio.toggleListening} onStreamingToggle={transmit.toggle} onQueueMusic={(s) => media.enqueue(s, "music")} onStartScreen={(s) => media.enqueue(s, "screen")} onSkip={media.skip} onStop={media.stop} onVolumeChange={media.setVolume} /> ) ) : activeTab === "messages" ? ( patchUIState({ selectedTextGuild: id, selectedTextChannel: "" }) } onChannelChange={(id) => patchUIState({ selectedTextChannel: id })} onReanalyze={messages.reanalyze} onLoadMore={messages.loadMore} hasMore={messages.hasMore} loadingMore={messages.loadingMore} /> ) : ( {Array.from({ length: 8 }).map((_, i) => ( ))} } > patchUIState({ selectedAnalyticsGuild: id, selectedAnalyticsChannel: "", }) } onChannelChange={(id) => patchUIState({ selectedAnalyticsChannel: id }) } /> )} patchUIState({ activeTab: tab })} /> ); }