diff --git a/apps/x/apps/renderer/src/components/sidebar-content.tsx b/apps/x/apps/renderer/src/components/sidebar-content.tsx index b82cb95a..f42e64d9 100644 --- a/apps/x/apps/renderer/src/components/sidebar-content.tsx +++ b/apps/x/apps/renderer/src/components/sidebar-content.tsx @@ -12,8 +12,10 @@ import { Folder, FolderPlus, MessageSquare, + Mic, Network, Pencil, + Square, SquarePen, Trash2, } from "lucide-react" @@ -141,6 +143,94 @@ export function SidebarContentPanel({ ) } +// Voice Note Recording Button +function VoiceNoteButton() { + const [isRecording, setIsRecording] = React.useState(false) + const mediaRecorderRef = React.useRef(null) + const chunksRef = React.useRef([]) + + const startRecording = async () => { + try { + const stream = await navigator.mediaDevices.getUserMedia({ audio: true }) + const mimeType = MediaRecorder.isTypeSupported('audio/mp4') + ? 'audio/mp4' + : 'audio/webm' + const recorder = new MediaRecorder(stream, { mimeType }) + chunksRef.current = [] + + recorder.ondataavailable = (e) => { + if (e.data.size > 0) chunksRef.current.push(e.data) + } + + recorder.onstop = async () => { + stream.getTracks().forEach((t) => t.stop()) + const blob = new Blob(chunksRef.current, { type: mimeType }) + const ext = mimeType === 'audio/mp4' ? 'm4a' : 'webm' + const timestamp = new Date().toISOString().replace(/[:.]/g, '-') + const filename = `voice-memo-${timestamp}.${ext}` + + try { + await window.ipc.invoke('workspace:mkdir', { + path: 'voice_memos', + recursive: true, + }) + + const arrayBuffer = await blob.arrayBuffer() + const base64 = btoa( + new Uint8Array(arrayBuffer).reduce( + (data, byte) => data + String.fromCharCode(byte), + '', + ), + ) + + await window.ipc.invoke('workspace:writeFile', { + path: `voice_memos/${filename}`, + data: base64, + opts: { encoding: 'base64' }, + }) + toast('Voice memo saved', 'success') + } catch { + toast('Failed to save voice memo', 'error') + } + } + + recorder.start() + mediaRecorderRef.current = recorder + setIsRecording(true) + } catch { + toast('Could not access microphone', 'error') + } + } + + const stopRecording = () => { + if (mediaRecorderRef.current && mediaRecorderRef.current.state !== 'inactive') { + mediaRecorderRef.current.stop() + } + mediaRecorderRef.current = null + setIsRecording(false) + } + + return ( + + + + + + {isRecording ? 'Stop Recording' : 'Voice Note'} + + + ) +} + // Knowledge Section function KnowledgeSection({ tree, @@ -181,6 +271,7 @@ function KnowledgeSection({ {action.label} ))} +