From 98cfe2dc5ac6e87dbf6cec6529bd039ab85954ff Mon Sep 17 00:00:00 2001 From: Jamie Pine Date: Sun, 25 Jan 2026 14:44:42 -0800 Subject: [PATCH] Refactor App layout and enhance UI components for better usability - Changed the default active tab in the App component from 'profiles' to 'main'. - Improved the layout of the main content area to better accommodate different views, including profiles, generation forms, and history. - Updated Sidebar component to reflect the new tab structure with a 'main' tab. - Enhanced the GenerationForm to utilize the selected profile from the UI store, improving user feedback when no profile is selected. - Added a new CircleButton component for better icon button interactions. - Adjusted styles in various components for improved responsiveness and visual consistency. --- app/src/App.tsx | 53 ++++---- .../components/Generation/GenerationForm.tsx | 64 +++++---- app/src/components/History/HistoryTable.tsx | 128 +++++++++--------- app/src/components/Sidebar.tsx | 6 +- .../components/VoiceProfiles/ProfileCard.tsx | 73 ++++++---- .../components/VoiceProfiles/ProfileList.tsx | 48 ++++--- app/src/components/ui/circle-button.tsx | 30 ++++ app/src/index.css | 4 + app/src/stores/uiStore.ts | 7 + landing/src/app/page.tsx | 68 +++++++--- landing/src/components/DownloadSection.tsx | 2 +- landing/src/components/PlatformIcons.tsx | 23 ++++ landing/src/components/ui/button.tsx | 8 +- landing/src/components/ui/feature-card.tsx | 2 +- landing/src/components/ui/hero.tsx | 53 ++++---- landing/src/lib/constants.ts | 7 +- 16 files changed, 352 insertions(+), 224 deletions(-) create mode 100644 app/src/components/ui/circle-button.tsx create mode 100644 landing/src/components/PlatformIcons.tsx diff --git a/app/src/App.tsx b/app/src/App.tsx index 72c8e97c..5f70cf08 100644 --- a/app/src/App.tsx +++ b/app/src/App.tsx @@ -14,7 +14,7 @@ import { isTauri, startServer, setupWindowCloseHandler } from '@/lib/tauri'; let serverStarting = false; function App() { - const [activeTab, setActiveTab] = useState('profiles'); + const [activeTab, setActiveTab] = useState('main'); const [serverReady, setServerReady] = useState(false); // Setup window close handler and auto-start server when running in Tauri (production only) @@ -84,38 +84,41 @@ function App() { } return ( -
-
+
+
-
-
- {activeTab === 'profiles' && ( -
- -
- )} - - {activeTab === 'generate' && ( -
- -
- )} - - {activeTab === 'history' && ( -
- -
- )} - - {activeTab === 'settings' && ( -
+
+
+ {activeTab === 'settings' ? ( +
+ ) : ( + // Main view: Profiles top left, Generator bottom left, History right +
+ {/* Left Column */} +
+ {/* Profiles - Top Left */} +
+ +
+ + {/* Generator - Bottom Left */} +
+ +
+
+ + {/* Right Column - History */} +
+ +
+
)}
diff --git a/app/src/components/Generation/GenerationForm.tsx b/app/src/components/Generation/GenerationForm.tsx index e4f6cbd9..06502c89 100644 --- a/app/src/components/Generation/GenerationForm.tsx +++ b/app/src/components/Generation/GenerationForm.tsx @@ -1,7 +1,8 @@ import { zodResolver } from '@hookform/resolvers/zod'; -import { Loader2 } from 'lucide-react'; +import { Loader2, Mic } from 'lucide-react'; import { useForm } from 'react-hook-form'; import * as z from 'zod'; +import { Badge } from '@/components/ui/badge'; import { Button } from '@/components/ui/button'; import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; import { @@ -24,10 +25,10 @@ import { import { Textarea } from '@/components/ui/textarea'; import { useToast } from '@/components/ui/use-toast'; import { useGeneration } from '@/lib/hooks/useGeneration'; -import { useProfiles } from '@/lib/hooks/useProfiles'; +import { useProfile } from '@/lib/hooks/useProfiles'; +import { useUIStore } from '@/stores/uiStore'; const generationSchema = z.object({ - profileId: z.string().min(1, 'Please select a voice profile'), text: z.string().min(1, 'Text is required').max(5000), language: z.enum(['en', 'zh']), seed: z.number().int().optional(), @@ -37,14 +38,14 @@ const generationSchema = z.object({ type GenerationFormValues = z.infer; export function GenerationForm() { - const { data: profiles } = useProfiles(); + const selectedProfileId = useUIStore((state) => state.selectedProfileId); + const { data: selectedProfile } = useProfile(selectedProfileId || ''); const generation = useGeneration(); const { toast } = useToast(); const form = useForm({ resolver: zodResolver(generationSchema), defaultValues: { - profileId: '', text: '', language: 'en', seed: undefined, @@ -53,9 +54,18 @@ export function GenerationForm() { }); async function onSubmit(data: GenerationFormValues) { + if (!selectedProfileId) { + toast({ + title: 'No profile selected', + description: 'Please select a voice profile from the cards above.', + variant: 'destructive', + }); + return; + } + try { const result = await generation.mutateAsync({ - profile_id: data.profileId, + profile_id: selectedProfileId, text: data.text, language: data.language, seed: data.seed, @@ -85,30 +95,20 @@ export function GenerationForm() {
- ( - - Voice Profile - - - +
+ Voice Profile + {selectedProfile ? ( +
+ + {selectedProfile.name} + {selectedProfile.language} +
+ ) : ( +
+ Click on a profile card above to select a voice profile +
)} - /> +
- -
+
Page {page + 1} • {total} total
- + />
- - {profile.description && ( -

{profile.description}

- )} -
- {profile.language} + +

+ {profile.description || 'No description'} +

+
+ + {profile.language} + +

{formatDate(profile.created_at)}

-

Created {formatDate(profile.created_at)}

diff --git a/app/src/components/VoiceProfiles/ProfileList.tsx b/app/src/components/VoiceProfiles/ProfileList.tsx index 9c1f6109..0e03f593 100644 --- a/app/src/components/VoiceProfiles/ProfileList.tsx +++ b/app/src/components/VoiceProfiles/ProfileList.tsx @@ -26,9 +26,11 @@ export function ProfileList() { ); } + const allProfiles = profiles || []; + return ( -
-
+
+

Voice Profiles

- {profiles && profiles.length === 0 ? ( - - - -

- No voice profiles yet. Create your first profile to get started. -

- -
-
- ) : ( -
- {profiles?.map((profile) => ( - - ))} -
- )} +
+ {allProfiles.length === 0 ? ( + + + +

+ No voice profiles yet. Create your first profile to get started. +

+ +
+
+ ) : ( +
+ {allProfiles.map((profile) => ( + + ))} +
+ )} +
diff --git a/app/src/components/ui/circle-button.tsx b/app/src/components/ui/circle-button.tsx new file mode 100644 index 00000000..9b659d10 --- /dev/null +++ b/app/src/components/ui/circle-button.tsx @@ -0,0 +1,30 @@ +import * as React from 'react'; +import { cn } from '@/lib/utils/cn'; + +export interface CircleButtonProps + extends React.ButtonHTMLAttributes { + icon: React.ComponentType<{ className?: string }>; +} + +const CircleButton = React.forwardRef( + ({ className, icon: Icon, ...props }, ref) => { + return ( + + ); + } +); +CircleButton.displayName = 'CircleButton'; + +export { CircleButton }; diff --git a/app/src/index.css b/app/src/index.css index a587db92..6ef392e1 100644 --- a/app/src/index.css +++ b/app/src/index.css @@ -102,6 +102,10 @@ * { @apply border-border; } + html, + body { + @apply overflow-hidden; + } body { @apply bg-background text-foreground; } diff --git a/app/src/stores/uiStore.ts b/app/src/stores/uiStore.ts index f54cb099..6f3a1605 100644 --- a/app/src/stores/uiStore.ts +++ b/app/src/stores/uiStore.ts @@ -14,6 +14,10 @@ interface UIStore { generationDialogOpen: boolean; setGenerationDialogOpen: (open: boolean) => void; + // Selected profile for generation + selectedProfileId: string | null; + setSelectedProfileId: (id: string | null) => void; + // Theme theme: 'light' | 'dark'; setTheme: (theme: 'light' | 'dark') => void; @@ -31,6 +35,9 @@ export const useUIStore = create((set) => ({ generationDialogOpen: false, setGenerationDialogOpen: (open) => set({ generationDialogOpen: open }), + selectedProfileId: null, + setSelectedProfileId: (id) => set({ selectedProfileId: id }), + theme: 'light', setTheme: (theme) => { set({ theme }); diff --git a/landing/src/app/page.tsx b/landing/src/app/page.tsx index a92a714e..7d3d0e0a 100644 --- a/landing/src/app/page.tsx +++ b/landing/src/app/page.tsx @@ -7,8 +7,8 @@ import { Hero } from '@/components/ui/hero'; import { Section, SectionTitle } from '@/components/ui/section'; import { Card, CardContent } from '@/components/ui/card'; import { FeatureCard } from '@/components/ui/feature-card'; -import { DownloadSection } from '@/components/DownloadSection'; -import { GITHUB_REPO } from '@/lib/constants'; +import { GITHUB_REPO, DOWNLOAD_LINKS } from '@/lib/constants'; +import { AppleIcon, WindowsIcon, LinuxIcon } from '@/components/PlatformIcons'; export default function Home() { const features = [ @@ -33,8 +33,8 @@ export default function Home() { icon: , }, { - title: 'Production Ready', - description: 'Type-safe, modular architecture. Desktop-first experience with native performance.', + title: 'Audio Transcription', + description: 'Powered by Whisper for accurate speech-to-text. Extract reference text from voice samples automatically.', icon: , }, { @@ -51,23 +51,61 @@ export default function Home() { title="voicebox" description="Professional voice cloning powered by Qwen3-TTS. Create natural-sounding speech from text with near-perfect voice replication." actions={ - <> - + } /> @@ -89,14 +127,6 @@ export default function Home() {
- {/* Download Section */} -
- Download -

- Available for macOS, Windows, and Linux. Choose your platform below. -

- -
); } diff --git a/landing/src/components/DownloadSection.tsx b/landing/src/components/DownloadSection.tsx index e3b4d4fe..1d464fa6 100644 --- a/landing/src/components/DownloadSection.tsx +++ b/landing/src/components/DownloadSection.tsx @@ -35,7 +35,7 @@ export function DownloadSection() {
{downloads.map(({ platform, icon: Icon, link, description }) => ( - +
diff --git a/landing/src/components/PlatformIcons.tsx b/landing/src/components/PlatformIcons.tsx new file mode 100644 index 00000000..6c45f1ad --- /dev/null +++ b/landing/src/components/PlatformIcons.tsx @@ -0,0 +1,23 @@ +export function AppleIcon({ className }: { className?: string }) { + return ( + + + + ); +} + +export function WindowsIcon({ className }: { className?: string }) { + return ( + + + + ); +} + +export function LinuxIcon({ className }: { className?: string }) { + return ( + + + + ); +} diff --git a/landing/src/components/ui/button.tsx b/landing/src/components/ui/button.tsx index dcb9443b..d52fd10d 100644 --- a/landing/src/components/ui/button.tsx +++ b/landing/src/components/ui/button.tsx @@ -8,14 +8,14 @@ const buttonVariants = cva( { variants: { variant: { - default: 'bg-primary text-primary-foreground hover:bg-primary/90 shadow-lg shadow-black/20 active:scale-[0.98]', + default: 'bg-primary/10 backdrop-blur-sm border border-border text-primary hover:bg-primary/11 hover:border-primary/15 shadow-lg shadow-black/20 active:scale-[0.99]', destructive: 'bg-destructive text-destructive-foreground hover:bg-destructive/90 shadow-lg shadow-destructive/20 active:scale-[0.98]', outline: - 'border border-border bg-background/50 backdrop-blur-sm hover:bg-accent/50 hover:border-border transition-all active:scale-[0.98]', + 'border border-border bg-background/50 backdrop-blur-sm hover:bg-foreground/5 transition-all active:scale-[0.99]', secondary: - 'bg-secondary text-secondary-foreground hover:bg-secondary/80 backdrop-blur-sm active:scale-[0.98]', - ghost: 'hover:bg-accent/50 hover:text-accent-foreground backdrop-blur-sm active:scale-[0.98]', + 'bg-secondary text-secondary-foreground hover:bg-secondary/70 backdrop-blur-sm active:scale-[0.99]', + ghost: 'hover:bg-accent/30 hover:text-accent-foreground backdrop-blur-sm active:scale-[0.99]', link: 'text-primary underline-offset-4 hover:underline', }, size: { diff --git a/landing/src/components/ui/feature-card.tsx b/landing/src/components/ui/feature-card.tsx index 4d7b6bfe..b3c3156b 100644 --- a/landing/src/components/ui/feature-card.tsx +++ b/landing/src/components/ui/feature-card.tsx @@ -11,7 +11,7 @@ interface FeatureCardProps { export function FeatureCard({ title, description, icon, className }: FeatureCardProps) { return ( - + {icon && (
diff --git a/landing/src/components/ui/hero.tsx b/landing/src/components/ui/hero.tsx index df52984c..3ec5f52f 100644 --- a/landing/src/components/ui/hero.tsx +++ b/landing/src/components/ui/hero.tsx @@ -14,31 +14,34 @@ interface HeroProps { export function Hero({ title, description, actions, className, showLogo = true }: HeroProps) { return ( -
-
- {showLogo && ( -
- Voicebox Logo -
- )} -

- {title} -

-

- {description} -

- {actions && ( -
- {actions} -
- )} +
+
+ {/* Left side - Content */} +
+ {showLogo && ( +
+ Voicebox Logo +
+ )} +

+ {title} +

+

+ {description} +

+
+ + {/* Right side - Actions */} +
+ {actions} +
); diff --git a/landing/src/lib/constants.ts b/landing/src/lib/constants.ts index 28cc44fc..4795f782 100644 --- a/landing/src/lib/constants.ts +++ b/landing/src/lib/constants.ts @@ -3,9 +3,10 @@ export const LATEST_VERSION = 'v0.1.0'; export const DOWNLOAD_LINKS = { - mac: 'https://github.com/USERNAME/voicebox/releases/download/v0.1.0/voicebox-macos-universal.dmg', - windows: 'https://github.com/USERNAME/voicebox/releases/download/v0.1.0/voicebox-windows-x64.msi', - linux: 'https://github.com/USERNAME/voicebox/releases/download/v0.1.0/voicebox-linux-x86_64.AppImage', + macArm: 'https://github.com/USERNAME/voicebox/releases/download/v0.1.0/voicebox_aarch64-apple-darwin.dmg', + macIntel: 'https://github.com/USERNAME/voicebox/releases/download/v0.1.0/voicebox_x86_64-apple-darwin.dmg', + windows: 'https://github.com/USERNAME/voicebox/releases/download/v0.1.0/voicebox_x86_64-pc-windows-msvc.msi', + linux: 'https://github.com/USERNAME/voicebox/releases/download/v0.1.0/voicebox_x86_64-unknown-linux-gnu.AppImage', } as const; export const GITHUB_REPO = 'https://github.com/USERNAME/voicebox';