mirror of
https://github.com/jamiepine/voicebox.git
synced 2026-09-29 07:05:14 -07:00
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:
co-authored by
Claude Opus 4.7
parent
c9103a24da
commit
67a9e308a9
@@ -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 */}
|
||||
|
||||
Reference in New Issue
Block a user