Reubencf Claude commited on
Commit
d7409d2
·
1 Parent(s): f68b107

Improve VoiceApp responsiveness and stop audio on close

Browse files

- Add audio cleanup when VoiceApp window is closed
- Add responsive breakpoints for mobile/tablet/desktop
- Adaptive toolbar with icon-only refresh button on small screens
- Compact content cards with smaller icons and spacing on mobile
- Responsive audio player controls
- Add xs breakpoint (480px) to Tailwind theme

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>

Files changed (2) hide show
  1. app/components/VoiceApp.tsx +92 -53
  2. app/globals.css +3 -0
app/components/VoiceApp.tsx CHANGED
@@ -42,6 +42,29 @@ export function VoiceApp({ onClose, onMinimize, onMaximize, onFocus, zIndex }: V
42
  const [currentTime, setCurrentTime] = useState(0)
43
  const [duration, setDuration] = useState(0)
44
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
45
  // Load saved content from server and localStorage
46
  useEffect(() => {
47
  // Clear any existing problematic localStorage data on first load
@@ -204,7 +227,7 @@ export function VoiceApp({ onClose, onMinimize, onMaximize, onFocus, zIndex }: V
204
  id="voice-app"
205
  title="Voice Studio"
206
  isOpen={true}
207
- onClose={onClose}
208
  onMinimize={onMinimize}
209
  onMaximize={onMaximize}
210
  onFocus={onFocus}
@@ -218,80 +241,88 @@ export function VoiceApp({ onClose, onMinimize, onMaximize, onFocus, zIndex }: V
218
  >
219
  <div className="flex flex-col h-full bg-[#F5F5F7]">
220
  {/* macOS Toolbar */}
221
- <div className="px-4 py-3 bg-white/50 backdrop-blur-md border-b border-gray-200/50 flex items-center justify-between sticky top-0 z-10">
222
- <div className="flex items-center gap-3">
223
- <div className="w-8 h-8 rounded-lg bg-gradient-to-br from-purple-500 to-pink-500 flex items-center justify-center shadow-sm">
224
- <MusicNote size={18} weight="fill" className="text-white" />
 
225
  </div>
226
- <div>
227
- <h2 className="text-sm font-semibold text-gray-900 leading-none">Voice Studio</h2>
228
- <p className="text-[11px] text-gray-500 mt-0.5">AI Audio Generation</p>
229
  </div>
230
  </div>
231
 
232
  <button
233
  onClick={handleRefresh}
234
- className="px-3 py-1.5 bg-white hover:bg-gray-50 active:bg-gray-100 text-gray-700 rounded-md text-xs font-medium border border-gray-200 shadow-sm transition-all flex items-center gap-1.5"
235
  >
236
  <ArrowClockwise size={14} />
237
- Refresh
238
  </button>
239
  </div>
240
 
241
  {/* Content Area */}
242
- <div className="flex-1 overflow-y-auto p-5">
243
  {voiceContents.length === 0 ? (
244
- <div className="flex flex-col items-center justify-center h-full text-center py-10">
245
- <div className="w-20 h-20 rounded-2xl bg-gradient-to-br from-purple-100 to-pink-100 flex items-center justify-center mb-6 shadow-inner">
246
- <FileAudio size={40} weight="duotone" className="text-purple-500/80" />
 
247
  </div>
248
- <h3 className="text-lg font-semibold text-gray-900 mb-2">No Audio Content</h3>
249
- <p className="text-sm text-gray-500 max-w-sm mb-6 leading-relaxed">
250
  Ask Claude to generate song lyrics or write a story, and your audio will appear here automatically.
251
  </p>
252
- <div className="bg-white/60 backdrop-blur-sm rounded-xl p-4 max-w-sm text-left border border-gray-200/50 shadow-sm">
253
- <p className="text-xs font-semibold text-gray-500 mb-2 uppercase tracking-wide">Try asking Claude:</p>
254
- <ul className="space-y-2 text-sm text-gray-700">
255
  <li className="flex items-start gap-2">
256
  <span className="text-purple-500">•</span>
257
- "Generate a pop song about coding"
258
  </li>
259
  <li className="flex items-start gap-2">
260
  <span className="text-purple-500">•</span>
261
- "Write a bedtime story and narrate it"
262
  </li>
263
  </ul>
264
  </div>
265
  </div>
266
  ) : (
267
- <div className="grid grid-cols-1 gap-3">
268
  {voiceContents.map((content) => (
269
  <div
270
  key={content.id}
271
- className="bg-white rounded-xl p-4 shadow-sm border border-gray-200/60 hover:shadow-md transition-all duration-200 group"
272
  >
273
- <div className="flex items-start justify-between mb-3">
274
- <div className="flex items-center gap-3">
275
- <div className={`w-10 h-10 rounded-lg flex items-center justify-center ${content.type === 'song'
276
  ? 'bg-purple-100 text-purple-600'
277
  : 'bg-blue-100 text-blue-600'
278
  }`}>
279
  {content.type === 'song' ? (
280
- <MusicNote size={20} weight="fill" />
 
 
 
281
  ) : (
282
- <BookOpen size={20} weight="fill" />
 
 
 
283
  )}
284
  </div>
285
- <div>
286
- <h3 className="font-semibold text-gray-900 text-sm">{content.title}</h3>
287
- <div className="flex items-center gap-2 text-xs text-gray-500">
288
  <span className="capitalize">{content.type}</span>
289
- <span>•</span>
290
- <span>{new Date(content.timestamp).toLocaleDateString()}</span>
291
  {content.style && (
292
  <>
293
- <span>•</span>
294
- <span className="truncate max-w-[150px]">{content.style}</span>
295
  </>
296
  )}
297
  </div>
@@ -299,47 +330,54 @@ export function VoiceApp({ onClose, onMinimize, onMaximize, onFocus, zIndex }: V
299
  </div>
300
 
301
  {content.audioUrl && (
302
- <div className="flex items-center gap-2">
303
  <button
304
  onClick={() => handleDownload(content)}
305
- className="p-1.5 text-gray-400 hover:text-gray-600 hover:bg-gray-100 rounded-md transition-colors"
306
  title="Download"
307
  >
308
- <DownloadSimple size={18} />
 
309
  </button>
310
  </div>
311
  )}
312
  </div>
313
 
314
  {content.isProcessing ? (
315
- <div className="flex items-center justify-center py-4 bg-gray-50/50 rounded-lg border border-dashed border-gray-200">
316
- <SpinnerGap size={20} className="text-purple-500 animate-spin" />
317
- <span className="ml-2 text-sm text-gray-500">Generating audio...</span>
 
318
  </div>
319
  ) : (
320
- <div className="space-y-3">
321
  {(content.lyrics || content.storyContent) && (
322
- <div className="bg-gray-50/80 rounded-lg p-3 max-h-24 overflow-y-auto text-xs text-gray-600 leading-relaxed border border-gray-100">
323
  <p className="whitespace-pre-line">{content.lyrics || content.storyContent}</p>
324
  </div>
325
  )}
326
 
327
  {content.audioUrl && (
328
- <div className="mt-3">
329
  {currentlyPlaying === content.id ? (
330
- <div className="bg-white rounded-lg border border-gray-200 p-3 space-y-2">
331
- <div className="flex items-center gap-3">
332
  <button
333
  onClick={() => handlePlay(content)}
334
- className="w-8 h-8 flex items-center justify-center rounded-full bg-gray-900 text-white hover:bg-gray-800 transition-colors"
335
  >
336
  {isPlaying ? (
337
- <Pause size={14} weight="fill" />
 
 
 
 
 
338
  ) : (
339
- <Play size={14} weight="fill" />
340
  )}
341
  </button>
342
- <div className="flex-1">
343
  <input
344
  type="range"
345
  min="0"
@@ -348,7 +386,7 @@ export function VoiceApp({ onClose, onMinimize, onMaximize, onFocus, zIndex }: V
348
  onChange={handleSeek}
349
  className="w-full h-1 bg-gray-200 rounded-lg appearance-none cursor-pointer [&::-webkit-slider-thumb]:appearance-none [&::-webkit-slider-thumb]:w-3 [&::-webkit-slider-thumb]:h-3 [&::-webkit-slider-thumb]:bg-gray-900 [&::-webkit-slider-thumb]:rounded-full"
350
  />
351
- <div className="flex justify-between text-[10px] text-gray-500 mt-1 font-medium">
352
  <span>{formatTime(currentTime)}</span>
353
  <span>{formatTime(duration)}</span>
354
  </div>
@@ -358,9 +396,10 @@ export function VoiceApp({ onClose, onMinimize, onMaximize, onFocus, zIndex }: V
358
  ) : (
359
  <button
360
  onClick={() => handlePlay(content)}
361
- className="w-full flex items-center justify-center gap-2 py-2.5 rounded-lg font-medium text-sm bg-[#F5F5F7] text-gray-700 border border-gray-200 hover:bg-gray-200 hover:border-gray-300 transition-all"
362
  >
363
- <Play size={16} weight="fill" />
 
364
  Play Audio
365
  </button>
366
  )}
 
42
  const [currentTime, setCurrentTime] = useState(0)
43
  const [duration, setDuration] = useState(0)
44
 
45
+ // Cleanup audio on unmount
46
+ useEffect(() => {
47
+ return () => {
48
+ if (audioElement) {
49
+ audioElement.pause()
50
+ audioElement.currentTime = 0
51
+ }
52
+ }
53
+ }, [audioElement])
54
+
55
+ // Handle close with audio cleanup
56
+ const handleClose = () => {
57
+ if (audioElement) {
58
+ audioElement.pause()
59
+ audioElement.currentTime = 0
60
+ setAudioElement(null)
61
+ setCurrentlyPlaying(null)
62
+ setIsPlaying(false)
63
+ setCurrentTime(0)
64
+ }
65
+ onClose()
66
+ }
67
+
68
  // Load saved content from server and localStorage
69
  useEffect(() => {
70
  // Clear any existing problematic localStorage data on first load
 
227
  id="voice-app"
228
  title="Voice Studio"
229
  isOpen={true}
230
+ onClose={handleClose}
231
  onMinimize={onMinimize}
232
  onMaximize={onMaximize}
233
  onFocus={onFocus}
 
241
  >
242
  <div className="flex flex-col h-full bg-[#F5F5F7]">
243
  {/* macOS Toolbar */}
244
+ <div className="px-2 sm:px-4 py-2 sm:py-3 bg-white/50 backdrop-blur-md border-b border-gray-200/50 flex items-center justify-between sticky top-0 z-10">
245
+ <div className="flex items-center gap-2 sm:gap-3 min-w-0">
246
+ <div className="w-7 h-7 sm:w-8 sm:h-8 rounded-lg bg-gradient-to-br from-purple-500 to-pink-500 flex items-center justify-center shadow-sm flex-shrink-0">
247
+ <MusicNote size={16} weight="fill" className="text-white sm:hidden" />
248
+ <MusicNote size={18} weight="fill" className="text-white hidden sm:block" />
249
  </div>
250
+ <div className="min-w-0">
251
+ <h2 className="text-xs sm:text-sm font-semibold text-gray-900 leading-none truncate">Voice Studio</h2>
252
+ <p className="text-[10px] sm:text-[11px] text-gray-500 mt-0.5 hidden xs:block">AI Audio Generation</p>
253
  </div>
254
  </div>
255
 
256
  <button
257
  onClick={handleRefresh}
258
+ className="p-1.5 sm:px-3 sm:py-1.5 bg-white hover:bg-gray-50 active:bg-gray-100 text-gray-700 rounded-md text-xs font-medium border border-gray-200 shadow-sm transition-all flex items-center gap-1.5 flex-shrink-0"
259
  >
260
  <ArrowClockwise size={14} />
261
+ <span className="hidden sm:inline">Refresh</span>
262
  </button>
263
  </div>
264
 
265
  {/* Content Area */}
266
+ <div className="flex-1 overflow-y-auto p-3 sm:p-5">
267
  {voiceContents.length === 0 ? (
268
+ <div className="flex flex-col items-center justify-center h-full text-center py-6 sm:py-10 px-2">
269
+ <div className="w-16 h-16 sm:w-20 sm:h-20 rounded-2xl bg-gradient-to-br from-purple-100 to-pink-100 flex items-center justify-center mb-4 sm:mb-6 shadow-inner">
270
+ <FileAudio size={32} weight="duotone" className="text-purple-500/80 sm:hidden" />
271
+ <FileAudio size={40} weight="duotone" className="text-purple-500/80 hidden sm:block" />
272
  </div>
273
+ <h3 className="text-base sm:text-lg font-semibold text-gray-900 mb-2">No Audio Content</h3>
274
+ <p className="text-xs sm:text-sm text-gray-500 max-w-sm mb-4 sm:mb-6 leading-relaxed px-2">
275
  Ask Claude to generate song lyrics or write a story, and your audio will appear here automatically.
276
  </p>
277
+ <div className="bg-white/60 backdrop-blur-sm rounded-xl p-3 sm:p-4 max-w-sm text-left border border-gray-200/50 shadow-sm w-full mx-2">
278
+ <p className="text-[10px] sm:text-xs font-semibold text-gray-500 mb-2 uppercase tracking-wide">Try asking Claude:</p>
279
+ <ul className="space-y-1.5 sm:space-y-2 text-xs sm:text-sm text-gray-700">
280
  <li className="flex items-start gap-2">
281
  <span className="text-purple-500">•</span>
282
+ <span>"Generate a pop song about coding"</span>
283
  </li>
284
  <li className="flex items-start gap-2">
285
  <span className="text-purple-500">•</span>
286
+ <span>"Write a bedtime story and narrate it"</span>
287
  </li>
288
  </ul>
289
  </div>
290
  </div>
291
  ) : (
292
+ <div className="grid grid-cols-1 gap-2 sm:gap-3">
293
  {voiceContents.map((content) => (
294
  <div
295
  key={content.id}
296
+ className="bg-white rounded-lg sm:rounded-xl p-3 sm:p-4 shadow-sm border border-gray-200/60 hover:shadow-md transition-all duration-200 group"
297
  >
298
+ <div className="flex items-start justify-between mb-2 sm:mb-3 gap-2">
299
+ <div className="flex items-center gap-2 sm:gap-3 min-w-0 flex-1">
300
+ <div className={`w-8 h-8 sm:w-10 sm:h-10 rounded-lg flex items-center justify-center flex-shrink-0 ${content.type === 'song'
301
  ? 'bg-purple-100 text-purple-600'
302
  : 'bg-blue-100 text-blue-600'
303
  }`}>
304
  {content.type === 'song' ? (
305
+ <>
306
+ <MusicNote size={16} weight="fill" className="sm:hidden" />
307
+ <MusicNote size={20} weight="fill" className="hidden sm:block" />
308
+ </>
309
  ) : (
310
+ <>
311
+ <BookOpen size={16} weight="fill" className="sm:hidden" />
312
+ <BookOpen size={20} weight="fill" className="hidden sm:block" />
313
+ </>
314
  )}
315
  </div>
316
+ <div className="min-w-0 flex-1">
317
+ <h3 className="font-semibold text-gray-900 text-xs sm:text-sm truncate">{content.title}</h3>
318
+ <div className="flex items-center gap-1 sm:gap-2 text-[10px] sm:text-xs text-gray-500 flex-wrap">
319
  <span className="capitalize">{content.type}</span>
320
+ <span className="hidden xs:inline">•</span>
321
+ <span className="hidden xs:inline">{new Date(content.timestamp).toLocaleDateString()}</span>
322
  {content.style && (
323
  <>
324
+ <span className="hidden sm:inline">•</span>
325
+ <span className="truncate max-w-[80px] sm:max-w-[150px] hidden sm:inline">{content.style}</span>
326
  </>
327
  )}
328
  </div>
 
330
  </div>
331
 
332
  {content.audioUrl && (
333
+ <div className="flex items-center gap-1 sm:gap-2 flex-shrink-0">
334
  <button
335
  onClick={() => handleDownload(content)}
336
+ className="p-1 sm:p-1.5 text-gray-400 hover:text-gray-600 hover:bg-gray-100 rounded-md transition-colors"
337
  title="Download"
338
  >
339
+ <DownloadSimple size={16} className="sm:hidden" />
340
+ <DownloadSimple size={18} className="hidden sm:block" />
341
  </button>
342
  </div>
343
  )}
344
  </div>
345
 
346
  {content.isProcessing ? (
347
+ <div className="flex items-center justify-center py-3 sm:py-4 bg-gray-50/50 rounded-lg border border-dashed border-gray-200">
348
+ <SpinnerGap size={18} className="text-purple-500 animate-spin sm:hidden" />
349
+ <SpinnerGap size={20} className="text-purple-500 animate-spin hidden sm:block" />
350
+ <span className="ml-2 text-xs sm:text-sm text-gray-500">Generating audio...</span>
351
  </div>
352
  ) : (
353
+ <div className="space-y-2 sm:space-y-3">
354
  {(content.lyrics || content.storyContent) && (
355
+ <div className="bg-gray-50/80 rounded-lg p-2 sm:p-3 max-h-20 sm:max-h-24 overflow-y-auto text-[10px] sm:text-xs text-gray-600 leading-relaxed border border-gray-100">
356
  <p className="whitespace-pre-line">{content.lyrics || content.storyContent}</p>
357
  </div>
358
  )}
359
 
360
  {content.audioUrl && (
361
+ <div className="mt-2 sm:mt-3">
362
  {currentlyPlaying === content.id ? (
363
+ <div className="bg-white rounded-lg border border-gray-200 p-2 sm:p-3 space-y-2">
364
+ <div className="flex items-center gap-2 sm:gap-3">
365
  <button
366
  onClick={() => handlePlay(content)}
367
+ className="w-7 h-7 sm:w-8 sm:h-8 flex items-center justify-center rounded-full bg-gray-900 text-white hover:bg-gray-800 transition-colors flex-shrink-0"
368
  >
369
  {isPlaying ? (
370
+ <Pause size={12} weight="fill" className="sm:hidden" />
371
+ ) : (
372
+ <Play size={12} weight="fill" className="sm:hidden" />
373
+ )}
374
+ {isPlaying ? (
375
+ <Pause size={14} weight="fill" className="hidden sm:block" />
376
  ) : (
377
+ <Play size={14} weight="fill" className="hidden sm:block" />
378
  )}
379
  </button>
380
+ <div className="flex-1 min-w-0">
381
  <input
382
  type="range"
383
  min="0"
 
386
  onChange={handleSeek}
387
  className="w-full h-1 bg-gray-200 rounded-lg appearance-none cursor-pointer [&::-webkit-slider-thumb]:appearance-none [&::-webkit-slider-thumb]:w-3 [&::-webkit-slider-thumb]:h-3 [&::-webkit-slider-thumb]:bg-gray-900 [&::-webkit-slider-thumb]:rounded-full"
388
  />
389
+ <div className="flex justify-between text-[9px] sm:text-[10px] text-gray-500 mt-0.5 sm:mt-1 font-medium">
390
  <span>{formatTime(currentTime)}</span>
391
  <span>{formatTime(duration)}</span>
392
  </div>
 
396
  ) : (
397
  <button
398
  onClick={() => handlePlay(content)}
399
+ className="w-full flex items-center justify-center gap-1.5 sm:gap-2 py-2 sm:py-2.5 rounded-lg font-medium text-xs sm:text-sm bg-[#F5F5F7] text-gray-700 border border-gray-200 hover:bg-gray-200 hover:border-gray-300 transition-all active:scale-[0.98]"
400
  >
401
+ <Play size={14} weight="fill" className="sm:hidden" />
402
+ <Play size={16} weight="fill" className="hidden sm:block" />
403
  Play Audio
404
  </button>
405
  )}
app/globals.css CHANGED
@@ -91,6 +91,9 @@
91
  }
92
 
93
  @theme inline {
 
 
 
94
  --radius-sm: calc(var(--radius) - 4px);
95
  --radius-md: calc(var(--radius) - 2px);
96
  --radius-lg: var(--radius);
 
91
  }
92
 
93
  @theme inline {
94
+ /* Custom breakpoints */
95
+ --breakpoint-xs: 480px;
96
+
97
  --radius-sm: calc(var(--radius) - 4px);
98
  --radius-md: calc(var(--radius) - 2px);
99
  --radius-lg: var(--radius);