Regenerate as new version, UI polish, and bugfixes

- Add /generate/{id}/regenerate endpoint that creates a new take version
- Wire regenerate into history dropdown with SSE progress + autoplay
- Add normalize_audio to regenerate path
- Remove duplicate Regenerate menu item
- Show disabled ellipsis menu during generation instead of hiding it
- Fix sf.write format kwarg broken by asyncio.to_thread migration
- Rename clean version label to 'original', effects to 'version-N'
- Show effects chain names in version list instead of 'N fx'
- Include all versions in export package
- Clean up version panel padding
This commit is contained in:
Jamie Pine
2026-03-14 08:34:58 -07:00
parent 25134b4ba9
commit 00c5b75ffb
5 changed files with 197 additions and 21 deletions
+14 -1
View File
@@ -157,8 +157,21 @@ export function AudioPlayer() {
const wavesurfer = wavesurferRef.current;
if (!wavesurfer) return;
// Update store when time changes
// Update store when time changes, stop if past duration
wavesurfer.on('timeupdate', (time) => {
const dur = usePlayerStore.getState().duration;
if (dur > 0 && time >= dur) {
setCurrentTime(dur);
const loop = usePlayerStore.getState().isLooping;
if (loop) {
wavesurfer.seekTo(0);
wavesurfer.play();
} else {
wavesurfer.pause();
setIsPlaying(false);
}
return;
}
setCurrentTime(time);
});
+25 -6
View File
@@ -224,6 +224,20 @@ export function HistoryTable() {
}
};
const handleRegenerate = async (generationId: string) => {
try {
await apiClient.regenerateGeneration(generationId);
addPendingGeneration(generationId);
queryClient.invalidateQueries({ queryKey: ['history'] });
} catch (error) {
toast({
title: 'Regenerate failed',
description: error instanceof Error ? error.message : 'Could not regenerate',
variant: 'destructive',
});
}
};
const handleApplyEffects = (generationId: string) => {
setEffectsTargetId(generationId);
setEffectsChain([]);
@@ -457,7 +471,7 @@ export function HistoryTable() {
>
<RotateCcw className="h-4 w-4" />
</Button>
) : isPlayable ? (
) : (
<>
<DropdownMenu>
<DropdownMenuTrigger asChild>
@@ -466,6 +480,7 @@ export function HistoryTable() {
size="icon"
className="h-8 w-8"
aria-label="Actions"
disabled={isGenerating}
>
<MoreHorizontal className="h-4 w-4" />
</Button>
@@ -495,6 +510,10 @@ export function HistoryTable() {
<Wand2 className="mr-2 h-4 w-4" />
Apply Effects
</DropdownMenuItem>
<DropdownMenuItem onClick={() => handleRegenerate(gen.id)}>
<RotateCcw className="mr-2 h-4 w-4" />
Regenerate
</DropdownMenuItem>
<DropdownMenuItem
onClick={() => handleDeleteClick(gen.id, gen.profile_name)}
disabled={deleteGeneration.isPending}
@@ -519,7 +538,7 @@ export function HistoryTable() {
</Button>
)}
</>
) : null}
)}
</div>
</div>
@@ -533,13 +552,13 @@ export function HistoryTable() {
transition={{ duration: 0.2, ease: 'easeOut' }}
className="overflow-hidden"
>
<div className="border-t border-border/50 px-3 pb-2 pt-2">
<div className="border-t border-border/50">
<div className="divide-y divide-border/40">
{gen.versions.map((v) => (
<button
key={v.id}
type="button"
className="flex items-center gap-2 w-full h-9 px-2 text-left hover:bg-muted/50 transition-colors"
className="flex items-center gap-2 w-full h-9 px-3 text-left hover:bg-muted/50 transition-colors"
onClick={() => {
handlePlayVersion(gen.id, v.id, gen.text, gen.profile_id);
if (!v.is_default) {
@@ -550,8 +569,8 @@ export function HistoryTable() {
<Play className="h-3 w-3 shrink-0 text-muted-foreground" />
<span className="truncate text-xs font-medium">{v.label}</span>
{v.effects_chain && v.effects_chain.length > 0 && (
<span className="text-[10px] text-muted-foreground">
{v.effects_chain.length} fx
<span className="text-[10px] text-muted-foreground truncate">
{v.effects_chain.map((e) => e.type).join(' → ')}
</span>
)}
<span className="flex-1" />