Refactor components and implement generation state management

- Updated import order in App component for consistency.
- Enhanced Sidebar component with loading indicator for audio generation state.
- Integrated generation state management using Zustand in GenerationForm and Sidebar.
- Improved ProfileList component formatting for better readability.
- Added new generationStore for managing audio generation state across components.
This commit is contained in:
Jamie Pine
2026-01-25 19:13:23 -08:00
parent 1f075c1c15
commit d9de8f04f2
6 changed files with 44 additions and 10 deletions
+1 -1
View File
@@ -1,4 +1,4 @@
import { useState, useEffect } from 'react';
import { useEffect, useState } from 'react';
import voiceboxLogo from '@/assets/voicebox-logo.png';
import { AudioPlayer } from '@/components/AudioPlayer/AudioPlayer';
import { GenerationForm } from '@/components/Generation/GenerationForm';
@@ -26,6 +26,7 @@ import { useToast } from '@/components/ui/use-toast';
import { apiClient } from '@/lib/api/client';
import { useGeneration } from '@/lib/hooks/useGeneration';
import { useProfile } from '@/lib/hooks/useProfiles';
import { useGenerationStore } from '@/stores/generationStore';
import { usePlayerStore } from '@/stores/playerStore';
import { useUIStore } from '@/stores/uiStore';
@@ -45,6 +46,7 @@ export function GenerationForm() {
const generation = useGeneration();
const { toast } = useToast();
const setAudio = usePlayerStore((state) => state.setAudio);
const setIsGenerating = useGenerationStore((state) => state.setIsGenerating);
const form = useForm<GenerationFormValues>({
resolver: zodResolver(generationSchema),
@@ -68,6 +70,7 @@ export function GenerationForm() {
}
try {
setIsGenerating(true);
const result = await generation.mutateAsync({
profile_id: selectedProfileId,
text: data.text,
@@ -93,6 +96,8 @@ export function GenerationForm() {
description: error instanceof Error ? error.message : 'Failed to generate audio',
variant: 'destructive',
});
} finally {
setIsGenerating(false);
}
}
+24 -7
View File
@@ -1,6 +1,8 @@
import { Home, Settings } from 'lucide-react';
import { cn } from '@/lib/utils/cn';
import { Home, Loader2, Settings } from 'lucide-react';
import voiceboxLogo from '@/assets/voicebox-logo.png';
import { cn } from '@/lib/utils/cn';
import { useGenerationStore } from '@/stores/generationStore';
import { usePlayerStore } from '@/stores/playerStore';
interface SidebarProps {
activeTab: string;
@@ -13,15 +15,15 @@ const tabs = [
];
export function Sidebar({ activeTab, onTabChange }: SidebarProps) {
const isGenerating = useGenerationStore((state) => state.isGenerating);
const audioUrl = usePlayerStore((state) => state.audioUrl);
const isPlayerVisible = !!audioUrl;
return (
<div className="fixed left-0 top-0 h-full w-20 bg-sidebar border-r border-border flex flex-col items-center py-6 gap-6">
{/* Logo */}
<div className="mb-2">
<img
src={voiceboxLogo}
alt="Voicebox"
className="w-12 h-12 object-contain"
/>
<img src={voiceboxLogo} alt="Voicebox" className="w-12 h-12 object-contain" />
</div>
{/* Navigation Buttons */}
@@ -48,6 +50,21 @@ export function Sidebar({ activeTab, onTabChange }: SidebarProps) {
);
})}
</div>
{/* Spacer to push loader to bottom */}
<div className="flex-1" />
{/* Generation Loader */}
{isGenerating && (
<div
className={cn(
'w-full flex items-center justify-center transition-all duration-200',
isPlayerVisible ? 'mb-[120px]' : 'mb-0',
)}
>
<Loader2 className="h-6 w-6 text-accent animate-spin" />
</div>
)}
</div>
);
}
@@ -10,7 +10,7 @@ import {
DialogHeader,
DialogTitle,
} from '@/components/ui/dialog';
import { useProfiles, useImportProfile } from '@/lib/hooks/useProfiles';
import { useImportProfile, useProfiles } from '@/lib/hooks/useProfiles';
import { useUIStore } from '@/stores/uiStore';
import { ProfileCard } from './ProfileCard';
import { ProfileForm } from './ProfileForm';
@@ -128,7 +128,8 @@ export function ProfileList() {
<DialogHeader>
<DialogTitle>Import Profile</DialogTitle>
<DialogDescription>
Import the profile from "{selectedFile?.name}". This will create a new profile with all samples.
Import the profile from "{selectedFile?.name}". This will create a new profile with
all samples.
</DialogDescription>
</DialogHeader>
<DialogFooter>
+11
View File
@@ -0,0 +1,11 @@
import { create } from 'zustand';
interface GenerationState {
isGenerating: boolean;
setIsGenerating: (generating: boolean) => void;
}
export const useGenerationStore = create<GenerationState>((set) => ({
isGenerating: false,
setIsGenerating: (generating) => set({ isGenerating: generating }),
}));
Binary file not shown.