import React, { useState, useCallback, useRef, useEffect } from "react"; import { createRoot } from "react-dom/client"; import { Waveform } from "../components/Waveform"; // Import Waveform import { useRecording, RecordingStatus } from "../hooks/useRecording"; // Import the hook and type import "@/styles/globals.css"; const NUM_WAVEFORM_BARS = 8; // Fewer bars for a smaller button const DEBOUNCE_DELAY = 100; // milliseconds const FloatingButtonApp: React.FC = () => { const [isHovered, setIsHovered] = useState(false); const fabRef = useRef(null); const leaveTimeoutRef = useRef(null); // Ref for debounce timeout const handleAudioChunk = useCallback( async (audioChunk: ArrayBuffer, isFinalChunk: boolean) => { try { // Send the audio chunk regardless of whether it's final or not await window.electronAPI.sendAudioChunk(audioChunk, isFinalChunk); console.log(`FAB: Sent audio chunk. isFinalChunk: ${isFinalChunk}`); if (isFinalChunk) { console.log( "FAB: This was the final chunk. Informing main process to finalize transcription.", ); // You might want to add a specific IPC call here if the main process needs an explicit signal // to finalize transcription, e.g., window.electronAPI.finalizeTranscription(); // For now, we assume sendAudioChunk is enough and the main process handles the stream end. } } catch (error) { console.error("FAB: Error sending audio chunk:", error); } }, [], ); const { recordingStatus, startRecording, stopRecording, voiceDetected } = useRecording({ onAudioChunk: handleAudioChunk, onRecordingStartCallback: async () => await window.electronAPI.onRecordingStarting(), onRecordingStopCallback: async () => await window.electronAPI.onRecordingStopping(), // Optionally, set chunkDurationMs here if needed, e.g., chunkDurationMs: 250 }); const isRecording = recordingStatus === "recording" || recordingStatus === "starting"; const isAwaitingFinalChunk = recordingStatus === "stopping"; console.log("FAB: recordingStatus:", recordingStatus); useEffect(() => { const cleanup = window.electronAPI.onRecordingStateChanged( (newState: boolean) => { console.log("FAB: Received new recording state:", newState); if (newState) { startRecording(); } else { stopRecording(); } }, ); return cleanup; // Cleanup the listener when the component unmounts }, [startRecording, stopRecording]); // This handler is for the button click. // It now uses the toggleRecording from the hook. const handleButtonClickToggleRecording = () => { console.log("FAB: Invoking toggleRecording from hook."); // The hook internally manages starting/stopping MediaRecorder and VAD. // The hook also listens for global state changes from the main process. }; // Function to send the FAB's size to Electron const updateWindowSizeToFab = () => { if (isHovered || isRecording) { //window.electronAPI.resizeWindow(96, 32); } else { //window.electronAPI.resizeWindow(48, 16); } }; // Update window size when recording or hover state changes useEffect(() => { console.log("is hovered", isHovered); updateWindowSizeToFab(); // eslint-disable-next-line react-hooks/exhaustive-deps }, [isRecording, isHovered]); // Debounced mouse leave handler const handleMouseLeave = () => { if (leaveTimeoutRef.current) { clearTimeout(leaveTimeoutRef.current); } leaveTimeoutRef.current = setTimeout(() => { setIsHovered(false); }, DEBOUNCE_DELAY); }; // Mouse enter handler - clears any pending leave timeout const handleMouseEnter = () => { if (leaveTimeoutRef.current) { clearTimeout(leaveTimeoutRef.current); leaveTimeoutRef.current = null; } setIsHovered(true); }; // Clear timeout on unmount useEffect(() => { return () => { if (leaveTimeoutRef.current) { clearTimeout(leaveTimeoutRef.current); } }; }, []); const expanded = recordingStatus === "recording" || recordingStatus === "starting" || recordingStatus === "stopping" || isHovered; return ( ); }; const container = document.getElementById("root"); if (container) { const root = createRoot(container); root.render(); } else { console.error( "FloatingButton: Root element not found in floating-button.html", ); }