'use client' import React, { useState, useEffect } from 'react' import Window from './Window' import { MusicNote, FileAudio, BookOpen, Play, Stop, Pause, DownloadSimple, ArrowClockwise, SpinnerGap } from '@phosphor-icons/react' interface VoiceAppProps { onClose: () => void onMinimize?: () => void onMaximize?: () => void onFocus?: () => void zIndex?: number } interface VoiceContent { id: string type: 'song' | 'story' title: string style?: string lyrics?: string storyContent?: string audioUrl?: string timestamp: number isProcessing: boolean } export function VoiceApp({ onClose, onMinimize, onMaximize, onFocus, zIndex }: VoiceAppProps) { const [voiceContents, setVoiceContents] = useState([]) const [currentlyPlaying, setCurrentlyPlaying] = useState(null) const [audioElement, setAudioElement] = useState(null) const [isPlaying, setIsPlaying] = useState(false) const [currentTime, setCurrentTime] = useState(0) const [duration, setDuration] = useState(0) // Cleanup audio on unmount useEffect(() => { return () => { if (audioElement) { audioElement.pause() audioElement.currentTime = 0 } } }, [audioElement]) // Handle close with audio cleanup const handleClose = () => { if (audioElement) { audioElement.pause() audioElement.currentTime = 0 setAudioElement(null) setCurrentlyPlaying(null) setIsPlaying(false) setCurrentTime(0) } onClose() } // Load saved content from server and localStorage useEffect(() => { // Clear any existing problematic localStorage data on first load try { const saved = localStorage.getItem('voice-app-contents') if (saved) { const parsed = JSON.parse(saved) // If the data contains audio URLs, clear it (this is old format) if (parsed.some((item: VoiceContent) => item.audioUrl && item.audioUrl.length > 1000)) { console.log('Clearing old localStorage data with embedded audio URLs') localStorage.removeItem('voice-app-contents') } else { // Load localStorage content immediately for instant display setVoiceContents(parsed) } } } catch (error) { console.warn('Error checking localStorage, clearing it:', error) localStorage.removeItem('voice-app-contents') } // Load fresh content from server (will update the UI when ready) loadContent() // Poll for updates const pollInterval = setInterval(() => { loadContent() }, 5000) return () => clearInterval(pollInterval) }, []) const loadContent = async () => { try { // Load all content from server (no passkey required) const response = await fetch(`/api/voice/save`) if (response.ok) { const data = await response.json() if (data.success && data.content) { // Only update if we have valid content from server setVoiceContents(data.content) // Also update localStorage with the latest data (without audio URLs) try { const contentsForStorage = data.content.map((content: VoiceContent) => ({ ...content, audioUrl: undefined // Remove audio URL to save space })) localStorage.setItem('voice-app-contents', JSON.stringify(contentsForStorage)) } catch (storageError) { console.warn('Failed to update localStorage:', storageError) } } } // Don't fallback to localStorage - we already loaded it on mount // This prevents overwriting with stale data } catch (error) { console.error('Failed to load voice contents from server:', error) // Keep existing content on error } } // Removed duplicate localStorage saving - now handled in loadContent const checkForNewContent = async () => { await loadContent() } const formatTime = (time: number) => { const minutes = Math.floor(time / 60) const seconds = Math.floor(time % 60) return `${minutes}:${seconds.toString().padStart(2, '0')}` } const handlePlay = (content: VoiceContent) => { if (!content.audioUrl) return if (currentlyPlaying === content.id && audioElement) { if (isPlaying) { audioElement.pause() setIsPlaying(false) } else { audioElement.play() setIsPlaying(true) } return } // Stop previous if (audioElement) { audioElement.pause() audioElement.currentTime = 0 } const audio = new Audio(content.audioUrl) audio.addEventListener('loadedmetadata', () => { setDuration(audio.duration) }) audio.addEventListener('timeupdate', () => { setCurrentTime(audio.currentTime) }) audio.addEventListener('ended', () => { setIsPlaying(false) setCurrentTime(0) }) audio.play() setAudioElement(audio) setCurrentlyPlaying(content.id) setIsPlaying(true) } const handleSeek = (e: React.ChangeEvent) => { const time = parseFloat(e.target.value) setCurrentTime(time) if (audioElement) { audioElement.currentTime = time } } const handleStop = () => { if (audioElement) { audioElement.pause() audioElement.currentTime = 0 setAudioElement(null) setCurrentlyPlaying(null) setIsPlaying(false) setCurrentTime(0) } } const handleDownload = async (content: VoiceContent) => { if (!content.audioUrl) return try { const response = await fetch(content.audioUrl) const blob = await response.blob() const url = window.URL.createObjectURL(blob) const link = document.createElement('a') link.href = url link.download = `${content.title.replace(/\s+/g, '_')}.mp3` document.body.appendChild(link) link.click() document.body.removeChild(link) window.URL.revokeObjectURL(url) } catch (error) { console.error('Download failed:', error) } } const handleRefresh = () => { checkForNewContent() } return (
{/* macOS Toolbar */}

Voice Studio

AI Audio Generation

{/* Content Area */}
{voiceContents.length === 0 ? (

No Audio Content

Ask Claude to generate song lyrics or write a story, and your audio will appear here automatically.

Try asking Claude:

  • "Generate a pop song about coding"
  • "Write a bedtime story and narrate it"
) : (
{voiceContents.map((content) => (
{content.type === 'song' ? ( <> ) : ( <> )}

{content.title}

{content.type} {new Date(content.timestamp).toLocaleDateString()} {content.style && ( <> {content.style} )}
{content.audioUrl && (
)}
{content.isProcessing ? (
Generating audio...
) : (
{(content.lyrics || content.storyContent) && (

{content.lyrics || content.storyContent}

)} {content.audioUrl && (
{currentlyPlaying === content.id ? (
{formatTime(currentTime)} {formatTime(duration)}
) : ( )}
)}
)}
))}
)}
) }