diff --git a/.gitignore b/.gitignore
index 05f7ef0d..118735ec 100644
--- a/.gitignore
+++ b/.gitignore
@@ -35,10 +35,7 @@ target/
Thumbs.db
# Data (user-generated)
-data/profiles/*
-data/generations/*
-data/projects/*
-data/voicebox.db
+data/
!data/.gitkeep
# Logs
diff --git a/app/src/components/History/HistoryTable.tsx b/app/src/components/History/HistoryTable.tsx
index 7900cd44..404a1914 100644
--- a/app/src/components/History/HistoryTable.tsx
+++ b/app/src/components/History/HistoryTable.tsx
@@ -3,11 +3,12 @@ import { AnimatePresence, motion } from 'framer-motion';
import {
Download,
FileArchive,
- Layers,
+ GalleryVerticalEnd,
Loader2,
MoreHorizontal,
Play,
RotateCcw,
+ Star,
Trash2,
Wand2,
} from 'lucide-react';
@@ -238,6 +239,19 @@ export function HistoryTable() {
}
};
+ const handleToggleFavorite = async (generationId: string) => {
+ try {
+ await apiClient.toggleFavorite(generationId);
+ queryClient.invalidateQueries({ queryKey: ['history'] });
+ } catch (error) {
+ toast({
+ title: 'Failed to update favorite',
+ description: error instanceof Error ? error.message : 'Unknown error',
+ variant: 'destructive',
+ });
+ }
+ };
+
const handleApplyEffects = (generationId: string) => {
setEffectsTargetId(generationId);
setEffectsChain([]);
@@ -380,6 +394,7 @@ export function HistoryTable() {
className={cn(
'flex items-stretch gap-4 h-26 p-3',
isPlayable && 'hover:bg-muted/70 cursor-pointer rounded-md',
+ isVersionsExpanded && 'rounded-b-none',
)}
aria-label={
isGenerating
@@ -457,7 +472,7 @@ export function HistoryTable() {
{/* Far right - Actions */}
e.stopPropagation()}
onClick={(e) => e.stopPropagation()}
>
@@ -465,11 +480,11 @@ export function HistoryTable() {
) : (
<>
@@ -478,11 +493,11 @@ export function HistoryTable() {
@@ -517,24 +532,42 @@ export function HistoryTable() {
handleDeleteClick(gen.id, gen.profile_name)}
disabled={deleteGeneration.isPending}
- className="text-destructive focus:text-destructive"
+ // className="text-destructive focus:text-destructive"
>
Delete
+
{hasVersions && (
)}
>
diff --git a/app/src/components/VoiceProfiles/ProfileCard.tsx b/app/src/components/VoiceProfiles/ProfileCard.tsx
index 5cf4c931..77bb7c61 100644
--- a/app/src/components/VoiceProfiles/ProfileCard.tsx
+++ b/app/src/components/VoiceProfiles/ProfileCard.tsx
@@ -1,4 +1,4 @@
-import { Download, Edit, Mic, Trash2 } from 'lucide-react';
+import { Download, Edit, Mic, Sparkles, Trash2 } from 'lucide-react';
import { useState } from 'react';
import { Badge } from '@/components/ui/badge';
import { Button } from '@/components/ui/button';
@@ -79,7 +79,7 @@ export function ProfileCard({ profile }: ProfileCardProps) {
-
-
+
+
{avatarUrl && !avatarError ? (

{profile.description || 'No description'}
-
+
{profile.language}
+ {profile.effects_chain && profile.effects_chain.length > 0 && (
+
+ )}
{
+ return this.request<{ is_favorited: boolean }>(`/history/${generationId}/favorite`, {
+ method: 'POST',
+ });
+ }
+
// History
async listHistory(query?: HistoryQuery): Promise {
const params = new URLSearchParams();
diff --git a/app/src/lib/api/types.ts b/app/src/lib/api/types.ts
index 95dc0f68..5a00c2f7 100644
--- a/app/src/lib/api/types.ts
+++ b/app/src/lib/api/types.ts
@@ -72,6 +72,7 @@ export interface GenerationResponse {
model_size?: string;
status: 'generating' | 'completed' | 'failed';
error?: string;
+ is_favorited?: boolean;
created_at: string;
versions?: GenerationVersionResponse[];
active_version_id?: string;
diff --git a/backend/database.py b/backend/database.py
index 78afef9b..92332d71 100644
--- a/backend/database.py
+++ b/backend/database.py
@@ -54,6 +54,7 @@ class Generation(Base):
model_size = Column(String, nullable=True)
status = Column(String, default="completed") # generating, completed, failed
error = Column(Text, nullable=True)
+ is_favorited = Column(Boolean, default=False)
created_at = Column(DateTime, default=datetime.utcnow)
@@ -375,6 +376,15 @@ def _run_migrations(engine):
conn.commit()
print("Added sort_order column to effect_presets")
+ if 'generations' in inspector.get_table_names():
+ columns = {col['name'] for col in inspector.get_columns('generations')}
+ if 'is_favorited' not in columns:
+ print("Migrating generations: adding is_favorited column")
+ with engine.connect() as conn:
+ conn.execute(text("ALTER TABLE generations ADD COLUMN is_favorited BOOLEAN DEFAULT 0"))
+ conn.commit()
+ print("Added is_favorited column to generations")
+
# Migration: Create generation_versions for existing generations
# (populate after tables are created, handled in init_db)
diff --git a/backend/history.py b/backend/history.py
index 8747206e..78da51ca 100644
--- a/backend/history.py
+++ b/backend/history.py
@@ -222,6 +222,7 @@ async def list_generations(
model_size=generation.model_size,
status=generation.status or "completed",
error=generation.error,
+ is_favorited=bool(generation.is_favorited),
created_at=generation.created_at,
versions=versions,
active_version_id=active_version_id,
diff --git a/backend/main.py b/backend/main.py
index 31ca5ad9..98970e51 100644
--- a/backend/main.py
+++ b/backend/main.py
@@ -1314,6 +1314,20 @@ async def get_generation(
)
+@app.post("/history/{generation_id}/favorite")
+async def toggle_favorite(
+ generation_id: str,
+ db: Session = Depends(get_db),
+):
+ """Toggle the favorite status of a generation."""
+ gen = db.query(DBGeneration).filter_by(id=generation_id).first()
+ if not gen:
+ raise HTTPException(status_code=404, detail="Generation not found")
+ gen.is_favorited = not gen.is_favorited
+ db.commit()
+ return {"is_favorited": gen.is_favorited}
+
+
@app.delete("/history/{generation_id}")
async def delete_generation(
generation_id: str,
diff --git a/backend/models.py b/backend/models.py
index 428f3803..b411000d 100644
--- a/backend/models.py
+++ b/backend/models.py
@@ -79,6 +79,7 @@ class GenerationResponse(BaseModel):
model_size: Optional[str] = None
status: str = "completed"
error: Optional[str] = None
+ is_favorited: bool = False
created_at: datetime
versions: Optional[List["GenerationVersionResponse"]] = None
active_version_id: Optional[str] = None
@@ -110,6 +111,7 @@ class HistoryResponse(BaseModel):
model_size: Optional[str] = None
status: str = "completed"
error: Optional[str] = None
+ is_favorited: bool = False
created_at: datetime
versions: Optional[List["GenerationVersionResponse"]] = None
active_version_id: Optional[str] = None