feat(captures): scrubbable WaveSurfer player for capture detail view

Replace the placeholder fake-waveform + play button in CapturesTab's
audio card with a real CaptureInlinePlayer (wavesurfer.js). The player
renders the actual waveform, lets users scrub through the clip, and
shows a proper current/total timestamp pair in place of the
duration-only label.

Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
This commit is contained in:
James Pine
2026-04-23 19:20:42 -07:00
co-authored by Claude Opus 4.7
parent c9103a24da
commit 67a9e308a9
2 changed files with 158 additions and 53 deletions
+5 -53
View File
@@ -10,7 +10,6 @@ import {
FileAudio,
Loader2,
Mic,
Play,
Send,
Settings2,
Sparkles,
@@ -22,6 +21,7 @@ import {
import { useEffect, useMemo, useRef, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { CapturePill } from '@/components/CapturePill/CapturePill';
import { CaptureInlinePlayer } from '@/components/CapturesTab/CaptureInlinePlayer';
import { DictationReadinessChecklist } from '@/components/CapturesTab/DictationReadinessChecklist';
import { Badge } from '@/components/ui/badge';
import { Button } from '@/components/ui/button';
@@ -107,31 +107,6 @@ function SourceBadge({ source }: { source: CaptureSource }) {
);
}
function FakeWaveform({ seed = 1, className }: { seed?: number; className?: string }) {
const bars = useMemo(() => {
return Array.from({ length: 72 }).map((_, i) => {
const h =
28 +
Math.sin(i * 0.35 + seed) * 22 +
Math.cos(i * 0.81 + seed * 2) * 14 +
Math.sin(i * 1.7 + seed * 3) * 8;
return Math.max(6, Math.min(96, h));
});
}, [seed]);
return (
<div className={cn('flex items-center gap-[2px] h-10', className)}>
{bars.map((h, i) => (
<div
key={i}
className="w-[3px] rounded-full bg-foreground/25"
style={{ height: `${h}%` }}
/>
))}
</div>
);
}
type PlaybackState = 'idle' | 'generating' | 'playing';
export function CapturesTab() {
@@ -305,16 +280,6 @@ export function CapturesTab() {
session.uploadFile(file, source);
};
const handlePlayOriginal = () => {
if (!selected) return;
setAudioWithAutoPlay(
apiClient.getCaptureAudioUrl(selected.id),
`capture-${selected.id}`,
null,
t('captures.captureCardLabel', { when: formatAbsoluteDate(selected.created_at) }),
);
};
const handleCopy = async () => {
if (!selected) return;
const text = showRefined
@@ -550,23 +515,10 @@ export function CapturesTab() {
{/* Audio player card */}
<div className="rounded-xl border border-border bg-muted/20 p-4 mb-6">
<div className="flex items-center gap-4">
<Button
size="icon"
variant="outline"
className="h-10 w-10 rounded-full shrink-0"
onClick={handlePlayOriginal}
>
<Play className="h-4 w-4 ml-0.5" />
</Button>
<FakeWaveform
seed={selected.id.charCodeAt(0)}
className="flex-1"
/>
<span className="text-xs tabular-nums text-muted-foreground font-medium">
{formatDuration(selected.duration_ms)}
</span>
</div>
<CaptureInlinePlayer
audioUrl={apiClient.getCaptureAudioUrl(selected.id)}
fallbackDurationMs={selected.duration_ms}
/>
</div>
{/* Transcript header */}