import { useCallback, useEffect, useMemo, useRef, useState } from "react"; import { DashboardLayout } from "./components/layout/DashboardLayout"; import { MediaPanel } from "./components/media/MediaPanel"; import { MessagesPanel } from "./components/messages/MessagesPanel"; import { ReviewPanel } from "./components/review/ReviewPanel"; import { Tabs, TabsContent } from "./components/ui/tabs"; import { VoicePanel } from "./components/voice/VoicePanel"; import { useDashboardSocket } from "./hooks/useDashboardSocket"; import { mergeMessages, useMessages } from "./hooks/useMessages"; import { useMediaControl } from "./hooks/useMediaControl"; import { useUIState } from "./hooks/useUIState"; import { useVoiceControl } from "./hooks/useVoiceControl"; import type { MessageRecord } from "./types/messages"; import type { DashboardTab } from "./types/ui"; import type { ActiveSpeaker } from "./types/voice"; const SAMPLE_RATE = 24000; const CHANNELS = 1; export default function App() { const { uiState, setUIState, patchUIState } = useUIState(); const voice = useVoiceControl(); const media = useMediaControl(); const messages = useMessages(); const [activeSpeakers, setActiveSpeakers] = useState([]); const [levels, setLevels] = useState(Array.from({ length: 32 }, () => 0.04)); const [isListening, setIsListening] = useState(false); const [isStreaming, setIsStreaming] = useState(false); const audioContextListenRef = useRef(null); const audioContextTransmitRef = useRef(null); const streamRef = useRef(null); const processorRef = useRef(null); const userTimelinesRef = useRef(new Map()); const activeTab = uiState.activeTab || "voice"; const selectedVoiceGuild = uiState.selectedVoiceGuild || uiState.selectedGuild || ""; const selectedVoiceChannel = uiState.selectedVoiceChannel || ""; const selectedTextGuild = uiState.selectedTextGuild || uiState.selectedGuild || ""; const selectedTextChannel = uiState.selectedTextChannel || ""; const handleIncomingPcm = useCallback((data: ArrayBuffer) => { const headerView = new DataView(data, 0, 4); const userIdHash = headerView.getInt32(0, true); const audioData = data.slice(4); const int16Array = new Int16Array(audioData); let sum = 0; for (const sample of int16Array) sum += Math.abs(sample / 32768); const average = int16Array.length ? sum / int16Array.length : 0; setLevels((prev) => prev.map((_, index) => Math.max(0.04, average * (0.5 + Math.sin(index * 0.6 + Date.now() / 140) * 0.35 + 0.65) * 5))); const audioContext = audioContextListenRef.current; if (!isListening || !audioContext) return; const float32Array = new Float32Array(int16Array.length); for (let i = 0; i < int16Array.length; i++) float32Array[i] = int16Array[i] / 32768; const audioBuffer = audioContext.createBuffer(CHANNELS, float32Array.length / CHANNELS, SAMPLE_RATE); audioBuffer.getChannelData(0).set(float32Array); const source = audioContext.createBufferSource(); source.buffer = audioBuffer; source.connect(audioContext.destination); const currentTime = audioContext.currentTime; let nextStart = userTimelinesRef.current.get(userIdHash) || 0; if (nextStart < currentTime) nextStart = currentTime + 0.05; source.start(nextStart); userTimelinesRef.current.set(userIdHash, nextStart + audioBuffer.duration); }, [isListening]); const socket = useDashboardSocket({ onUIState: (state) => setUIState((prev) => ({ ...prev, ...state })), onUserState: setActiveSpeakers, onMessageCreated: (message) => messages.setMessages((prev) => mergeMessages(prev, [message])), onMessageUpdated: (message) => messages.setMessages((prev) => prev.map((item) => (item.id === message.id ? { ...item, ...message } as MessageRecord : item))), onMessageDeleted: (message) => messages.setMessages((prev) => prev.map((item) => (item.id === message.id ? { ...item, type: "deleted" } : item))), onMessageAnalyzed: (message) => messages.setMessages((prev) => mergeMessages(prev, [message])), onAttachmentUploaded: () => messages.fetchMessages(selectedTextChannel).catch(() => undefined), onMediaState: media.setMediaState, onPcm: handleIncomingPcm, }); const stopStreamingLocal = useCallback(() => { setIsStreaming(false); if (processorRef.current) { processorRef.current.disconnect(); processorRef.current = null; } if (audioContextTransmitRef.current) { audioContextTransmitRef.current.close(); audioContextTransmitRef.current = null; } if (streamRef.current) { for (const track of streamRef.current.getTracks()) track.stop(); streamRef.current = null; } setLevels(Array.from({ length: 32 }, () => 0.04)); }, []); const startStreamingLocal = useCallback(async () => { try { const stream = await navigator.mediaDevices.getUserMedia({ audio: true }); streamRef.current = stream; setIsStreaming(true); const AudioContextCtor = window.AudioContext || window.webkitAudioContext; const audioContext = new AudioContextCtor({ sampleRate: SAMPLE_RATE }); audioContextTransmitRef.current = audioContext; const source = audioContext.createMediaStreamSource(stream); const processor = audioContext.createScriptProcessor(4096, 1, 1); processorRef.current = processor; source.connect(processor); processor.connect(audioContext.destination); processor.onaudioprocess = (event) => { if (!socket.socketRef.current || socket.socketRef.current.readyState !== WebSocket.OPEN) return; const inputData = event.inputBuffer.getChannelData(0); const pcmData = new Int16Array(inputData.length); for (let i = 0; i < inputData.length; i++) { pcmData[i] = Math.max(-1, Math.min(1, inputData[i])) * 32767; } socket.socketRef.current.send(pcmData.buffer); // Update local levels from mic let sum = 0; for (let i = 0; i < inputData.length; i++) sum += Math.abs(inputData[i]); const average = inputData.length ? sum / inputData.length : 0; setLevels((prev) => prev.map((_, index) => Math.max(0.04, average * (0.5 + Math.sin(index * 0.6 + Date.now() / 140) * 0.35 + 0.65) * 5))); }; } catch (err) { console.error("Microphone access failed:", err); setIsStreaming(false); throw err; } }, [socket.socketRef]); const toggleStreaming = useCallback(async () => { if (isStreaming) { stopStreamingLocal(); await patchUIState({ isStreaming: false }); } else { await startStreamingLocal(); await patchUIState({ isStreaming: true }); } }, [isStreaming, startStreamingLocal, stopStreamingLocal, patchUIState]); useEffect(() => { if (selectedVoiceGuild) { voice.loadVoiceChannels(selectedVoiceGuild).catch(() => undefined); } }, [selectedVoiceGuild]); useEffect(() => { if (selectedTextGuild) { voice.loadTextTargets(selectedTextGuild).catch(() => undefined); } }, [selectedTextGuild]); useEffect(() => { if (selectedTextChannel) { messages.fetchMessages(selectedTextChannel).catch(() => undefined); } }, [selectedTextChannel]); const toggleListening = useCallback(async () => { if (isListening) { await audioContextListenRef.current?.suspend(); userTimelinesRef.current.clear(); setIsListening(false); await patchUIState({ isListening: false }); return; } const AudioContextCtor = window.AudioContext || window.webkitAudioContext; audioContextListenRef.current ??= new AudioContextCtor({ sampleRate: SAMPLE_RATE }); await audioContextListenRef.current.resume(); setIsListening(true); await patchUIState({ isListening: true }); }, [isListening, patchUIState]); const tabs = useMemo(() => ["voice", "media", "messages", "review"] as DashboardTab[], []); return ( patchUIState({ activeTab: tab })} >
patchUIState({ activeTab: value as DashboardTab })}>
{tabs.map((tab) => ( ))}
patchUIState({ activeTab: value as DashboardTab })}> patchUIState({ selectedVoiceGuild: guildId, selectedVoiceChannel: "" })} onChannelChange={(channelId) => patchUIState({ selectedVoiceChannel: channelId })} onJoin={() => voice.joinVoice(selectedVoiceGuild, selectedVoiceChannel)} onDisconnect={() => voice.leaveVoice()} onListenToggle={toggleListening} onStreamingToggle={toggleStreaming} /> media.enqueue(source, "music")} onStartScreen={(source) => media.enqueue(source, "screen")} onSkip={media.skip} onStop={media.stop} /> patchUIState({ selectedTextGuild: guildId, selectedTextChannel: "" })} onChannelChange={(channelId) => patchUIState({ selectedTextChannel: channelId })} onReanalyze={messages.reanalyze} />
); }