import { useEffect, useMemo, useState } from "react"; import { Component, Suspense, lazy } from "react"; import { DashboardLayout } from "./widgets/DashboardLayout"; import { MobileTabBar } from "./shared/ui/MobileTabBar"; import { AuthOverlay } from "./features/auth"; import { LivePanel } from "./features/live"; import { MessagesPanel } from "./features/messages"; import { useDashboardSocket } from "./shared/ws/socket"; import { mergeMessages, useMessages } from "./features/messages/hooks/useMessages"; import { useMediaControl } from "./features/live/hooks/useMediaControl"; import { useUIState } from "./shared/hooks/useUIState"; import { useVoiceControl } from "./features/live/hooks/useVoiceControl"; import { useAudioPlayback } from "./shared/hooks/useAudioPlayback"; import { useAudioTransmit } from "./shared/hooks/useAudioTransmit"; import { getAppConfig, type MessageRecord, type ActiveSpeaker, type MediaState } from "./shared/api/client"; import { Skeleton } from "./shared/ui"; 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 })} /> ); }