Refactor App layout and introduce new components for improved organization

- Replaced the main layout in App component with AppFrame for better structure.
- Introduced MainEditor component to encapsulate the main editing interface, including ProfileList and HistoryTable.
- Added ModelsTab component to manage model-related functionalities.
- Updated Sidebar to include a new Models tab for navigation.
- Removed unused components and streamlined the layout for enhanced user experience.
This commit is contained in:
Jamie Pine
2026-01-27 16:38:37 -08:00
parent f7cb219f6d
commit d8d9eeaa6a
9 changed files with 150 additions and 85 deletions
+9 -39
View File
@@ -1,17 +1,16 @@
import { useEffect, useState } from 'react'; import { useEffect, useState } from 'react';
import voiceboxLogo from '@/assets/voicebox-logo.png'; import voiceboxLogo from '@/assets/voicebox-logo.png';
import { AudioPlayer } from '@/components/AudioPlayer/AudioPlayer'; import { AppFrame } from '@/components/AppFrame/AppFrame';
import { AudioTab } from '@/components/AudioTab/AudioTab';
import { MainEditor } from '@/components/MainEditor/MainEditor';
import { ModelsTab } from '@/components/ModelsTab/ModelsTab';
import { ServerTab } from '@/components/ServerTab/ServerTab';
// import { GenerationForm } from '@/components/Generation/GenerationForm'; // import { GenerationForm } from '@/components/Generation/GenerationForm';
import { FloatingGenerateBox } from '@/components/Generation/FloatingGenerateBox';
import { HistoryTable } from '@/components/History/HistoryTable';
import ShinyText from '@/components/ShinyText'; import ShinyText from '@/components/ShinyText';
import { Sidebar } from '@/components/Sidebar'; import { Sidebar } from '@/components/Sidebar';
import { TitleBarDragRegion } from '@/components/TitleBarDragRegion'; import { TitleBarDragRegion } from '@/components/TitleBarDragRegion';
import { Toaster } from '@/components/ui/toaster'; import { Toaster } from '@/components/ui/toaster';
import { ProfileList } from '@/components/VoiceProfiles/ProfileList';
import { VoicesTab } from '@/components/VoicesTab/VoicesTab'; import { VoicesTab } from '@/components/VoicesTab/VoicesTab';
import { AudioTab } from '@/components/AudioTab/AudioTab';
import { ServerTab } from '@/components/ServerTab/ServerTab';
import { useModelDownloadToast } from '@/lib/hooks/useModelDownloadToast'; import { useModelDownloadToast } from '@/lib/hooks/useModelDownloadToast';
import { MODEL_DISPLAY_NAMES, useRestoreActiveTasks } from '@/lib/hooks/useRestoreActiveTasks'; import { MODEL_DISPLAY_NAMES, useRestoreActiveTasks } from '@/lib/hooks/useRestoreActiveTasks';
import { import {
@@ -21,7 +20,6 @@ import {
setupWindowCloseHandler, setupWindowCloseHandler,
startServer, startServer,
} from '@/lib/tauri'; } from '@/lib/tauri';
import { usePlayerStore } from '@/stores/playerStore';
import { useServerStore } from '@/stores/serverStore'; import { useServerStore } from '@/stores/serverStore';
// Track if server is starting to prevent duplicate starts // Track if server is starting to prevent duplicate starts
@@ -54,7 +52,6 @@ function App() {
const [activeTab, setActiveTab] = useState('main'); const [activeTab, setActiveTab] = useState('main');
const [serverReady, setServerReady] = useState(false); const [serverReady, setServerReady] = useState(false);
const [loadingMessageIndex, setLoadingMessageIndex] = useState(0); const [loadingMessageIndex, setLoadingMessageIndex] = useState(0);
const audioUrl = usePlayerStore((state) => state.audioUrl);
// Monitor active downloads/generations and show toasts for them // Monitor active downloads/generations and show toasts for them
const activeDownloads = useRestoreActiveTasks(); const activeDownloads = useRestoreActiveTasks();
@@ -169,48 +166,21 @@ function App() {
} }
return ( return (
<div className="h-screen bg-background flex flex-col overflow-hidden pt-12"> <AppFrame>
<TitleBarDragRegion />
<div className="flex flex-1 min-h-0 overflow-hidden"> <div className="flex flex-1 min-h-0 overflow-hidden">
<Sidebar activeTab={activeTab} onTabChange={setActiveTab} isMacOS={isMacOS()} /> <Sidebar activeTab={activeTab} onTabChange={setActiveTab} isMacOS={isMacOS()} />
<main className="flex-1 ml-20 overflow-hidden flex flex-col"> <main className="flex-1 ml-20 overflow-hidden flex flex-col">
<div className="container mx-auto px-8 max-w-[1800px] h-full overflow-hidden flex flex-col"> <div className="container mx-auto px-8 max-w-[1800px] h-full overflow-hidden flex flex-col">
{activeTab === 'main' && ( {activeTab === 'main' && <MainEditor />}
// Main view: Profiles top left, Generator bottom left, History right
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6 h-full min-h-0 overflow-hidden relative">
{/* Left Column */}
<div className="flex flex-col gap-6 min-h-0 overflow-y-auto pb-32">
{/* Profiles - Top Left */}
<div className="shrink-0 flex flex-col">
<ProfileList />
</div>
{/* Generator - Bottom Left */}
{/* <div className="shrink-0">
<GenerationForm />
</div> */}
</div>
{/* Right Column - History */}
<div className="flex flex-col min-h-0 overflow-hidden">
<HistoryTable />
</div>
{/* Floating Generate Box */}
<FloatingGenerateBox isPlayerOpen={!!audioUrl} />
</div>
)}
{activeTab === 'voices' && <VoicesTab />} {activeTab === 'voices' && <VoicesTab />}
{activeTab === 'audio' && <AudioTab />} {activeTab === 'audio' && <AudioTab />}
{activeTab === 'server' && <ServerTab />} {activeTab === 'server' && <ServerTab />}
{activeTab === 'models' && <ModelsTab />}
</div> </div>
</main> </main>
</div> </div>
{/* Audio Player - always visible except on server tab */}
{activeTab !== 'server' && <AudioPlayer />}
{/* Show download toasts for any active downloads (from anywhere) */} {/* Show download toasts for any active downloads (from anywhere) */}
{activeDownloads.map((download) => { {activeDownloads.map((download) => {
const displayName = MODEL_DISPLAY_NAMES[download.model_name] || download.model_name; const displayName = MODEL_DISPLAY_NAMES[download.model_name] || download.model_name;
@@ -224,7 +194,7 @@ function App() {
})} })}
<Toaster /> <Toaster />
</div> </AppFrame>
); );
} }
+16
View File
@@ -0,0 +1,16 @@
import { TitleBarDragRegion } from '@/components/TitleBarDragRegion';
import { AudioPlayer } from '@/components/AudioPlayer/AudioPlayer';
interface AppFrameProps {
children: React.ReactNode;
}
export function AppFrame({ children }: AppFrameProps) {
return (
<div className="h-screen bg-background flex flex-col overflow-hidden pt-12">
<TitleBarDragRegion />
{children}
<AudioPlayer />
</div>
);
}
+60 -7
View File
@@ -1,6 +1,6 @@
import { useQuery } from '@tanstack/react-query'; import { useQuery } from '@tanstack/react-query';
import { invoke } from '@tauri-apps/api/core'; import { invoke } from '@tauri-apps/api/core';
import { Pause, Play, Repeat, Volume2, VolumeX } from 'lucide-react'; import { Pause, Play, Repeat, Volume2, VolumeX, X } from 'lucide-react';
import { useEffect, useMemo, useRef, useState } from 'react'; import { useEffect, useMemo, useRef, useState } from 'react';
import WaveSurfer from 'wavesurfer.js'; import WaveSurfer from 'wavesurfer.js';
import { Button } from '@/components/ui/button'; import { Button } from '@/components/ui/button';
@@ -28,6 +28,7 @@ export function AudioPlayer() {
setVolume, setVolume,
toggleLoop, toggleLoop,
clearRestartFlag, clearRestartFlag,
reset,
} = usePlayerStore(); } = usePlayerStore();
// Check if profile has assigned channels (for native audio routing) // Check if profile has assigned channels (for native audio routing)
@@ -267,7 +268,12 @@ export function AudioPlayer() {
const currentVolume = usePlayerStore.getState().volume; const currentVolume = usePlayerStore.getState().volume;
mediaElement.volume = currentVolume; mediaElement.volume = currentVolume;
mediaElement.muted = false; mediaElement.muted = false;
console.log('WaveSurfer unmuted for normal playback - volume:', mediaElement.volume, 'muted:', mediaElement.muted); console.log(
'WaveSurfer unmuted for normal playback - volume:',
mediaElement.volume,
'muted:',
mediaElement.muted,
);
} }
} else { } else {
const deviceIds = assignedChannels.flatMap((ch: any) => ch.device_ids); const deviceIds = assignedChannels.flatMap((ch: any) => ch.device_ids);
@@ -298,7 +304,12 @@ export function AudioPlayer() {
if (mediaElement) { if (mediaElement) {
mediaElement.volume = 0; mediaElement.volume = 0;
mediaElement.muted = true; mediaElement.muted = true;
console.log('WaveSurfer muted for native playback - volume:', mediaElement.volume, 'muted:', mediaElement.muted); console.log(
'WaveSurfer muted for native playback - volume:',
mediaElement.volume,
'muted:',
mediaElement.muted,
);
} }
// Start WaveSurfer playback for visualization (muted) // Start WaveSurfer playback for visualization (muted)
@@ -329,7 +340,12 @@ export function AudioPlayer() {
const currentVolume = usePlayerStore.getState().volume; const currentVolume = usePlayerStore.getState().volume;
mediaElement.volume = currentVolume; mediaElement.volume = currentVolume;
mediaElement.muted = false; mediaElement.muted = false;
console.log('WaveSurfer unmuted after native playback failure - volume:', mediaElement.volume, 'muted:', mediaElement.muted); console.log(
'WaveSurfer unmuted after native playback failure - volume:',
mediaElement.volume,
'muted:',
mediaElement.muted,
);
} }
// Fall through to WaveSurfer playback // Fall through to WaveSurfer playback
} }
@@ -342,7 +358,12 @@ export function AudioPlayer() {
const currentVolume = usePlayerStore.getState().volume; const currentVolume = usePlayerStore.getState().volume;
mediaElement.volume = currentVolume; mediaElement.volume = currentVolume;
mediaElement.muted = false; mediaElement.muted = false;
console.log('WaveSurfer unmuted for normal playback - volume:', mediaElement.volume, 'muted:', mediaElement.muted); console.log(
'WaveSurfer unmuted for normal playback - volume:',
mediaElement.volume,
'muted:',
mediaElement.muted,
);
} }
} }
@@ -373,7 +394,12 @@ export function AudioPlayer() {
const currentVolume = usePlayerStore.getState().volume; const currentVolume = usePlayerStore.getState().volume;
mediaElement.volume = currentVolume; mediaElement.volume = currentVolume;
mediaElement.muted = false; mediaElement.muted = false;
console.log('Playing (normal mode) - volume:', mediaElement.volume, 'muted:', mediaElement.muted); console.log(
'Playing (normal mode) - volume:',
mediaElement.volume,
'muted:',
mediaElement.muted,
);
} }
} }
}); });
@@ -673,7 +699,7 @@ export function AudioPlayer() {
// Stop any existing native playback first // Stop any existing native playback first
try { try {
await invoke('stop_audio_playback'); await invoke('stop_audio_playback');
} catch (error) { } catch (_error) {
// Ignore errors when stopping (might not be playing) // Ignore errors when stopping (might not be playing)
console.log('No existing playback to stop'); console.log('No existing playback to stop');
} }
@@ -752,6 +778,22 @@ export function AudioPlayer() {
setVolume(value[0] / 100); setVolume(value[0] / 100);
}; };
const handleClose = () => {
// Stop any native playback
if (isUsingNativePlaybackRef.current && isTauri()) {
invoke('stop_audio_playback').catch((error) => {
console.error('Failed to stop native playback:', error);
});
}
// Stop WaveSurfer
if (wavesurferRef.current) {
wavesurferRef.current.pause();
wavesurferRef.current.seekTo(0);
}
// Reset player state
reset();
};
// Don't render if no audio // Don't render if no audio
if (!audioUrl) { if (!audioUrl) {
return null; return null;
@@ -832,6 +874,17 @@ export function AudioPlayer() {
className="flex-1" className="flex-1"
/> />
</div> </div>
{/* Close Button */}
<Button
variant="ghost"
size="icon"
onClick={handleClose}
className="shrink-0"
title="Close player"
>
<X className="h-5 w-5" />
</Button>
</div> </div>
</div> </div>
</div> </div>
+7 -23
View File
@@ -33,7 +33,7 @@ import { usePlayerStore } from '@/stores/playerStore';
// NEW ALTERNATE HISTORY VIEW - FIXED HEIGHT ROWS // NEW ALTERNATE HISTORY VIEW - FIXED HEIGHT ROWS
export function HistoryTable() { export function HistoryTable() {
const [page, setPage] = useState(0); const [page, _setPage] = useState(0);
const [isScrolled, setIsScrolled] = useState(false); const [isScrolled, setIsScrolled] = useState(false);
const scrollRef = useRef<HTMLDivElement>(null); const scrollRef = useRef<HTMLDivElement>(null);
const fileInputRef = useRef<HTMLInputElement>(null); const fileInputRef = useRef<HTMLInputElement>(null);
@@ -143,7 +143,7 @@ export function HistoryTable() {
const history = historyData?.items || []; const history = historyData?.items || [];
const total = historyData?.total || 0; const total = historyData?.total || 0;
const hasMore = history.length === limit && (page + 1) * limit < total; const _hasMore = history.length === limit && (page + 1) * limit < total;
return ( return (
<div className="flex flex-col h-full min-h-0 relative"> <div className="flex flex-col h-full min-h-0 relative">
@@ -176,8 +176,8 @@ export function HistoryTable() {
<div <div
ref={scrollRef} ref={scrollRef}
className={cn( className={cn(
'flex-1 min-h-0 overflow-y-auto space-y-2', 'flex-1 min-h-0 overflow-y-auto space-y-2 pb-4',
isPlayerVisible && 'max-h-[calc(100vh-117px)]', isPlayerVisible && 'pb-32',
)} )}
> >
{history.map((gen) => { {history.map((gen) => {
@@ -242,7 +242,9 @@ export function HistoryTable() {
</Button> </Button>
</DropdownMenuTrigger> </DropdownMenuTrigger>
<DropdownMenuContent align="end"> <DropdownMenuContent align="end">
<DropdownMenuItem onClick={() => handlePlay(gen.id, gen.text, gen.profile_id)}> <DropdownMenuItem
onClick={() => handlePlay(gen.id, gen.text, gen.profile_id)}
>
<Play className="mr-2 h-4 w-4" /> <Play className="mr-2 h-4 w-4" />
Play Play
</DropdownMenuItem> </DropdownMenuItem>
@@ -275,24 +277,6 @@ export function HistoryTable() {
); );
})} })}
</div> </div>
{(total > limit || page > 0) && (
<div className="flex justify-between items-center mt-4 shrink-0">
<Button
variant="outline"
onClick={() => setPage((p) => Math.max(0, p - 1))}
disabled={page === 0}
>
Previous
</Button>
<div className="text-sm text-muted-foreground">
Page {page + 1} {total} total
</div>
<Button variant="outline" onClick={() => setPage((p) => p + 1)} disabled={!hasMore}>
Next
</Button>
</div>
)}
</> </>
)} )}
@@ -0,0 +1,34 @@
import { ProfileList } from '@/components/VoiceProfiles/ProfileList';
import { HistoryTable } from '@/components/History/HistoryTable';
import { FloatingGenerateBox } from '@/components/Generation/FloatingGenerateBox';
import { usePlayerStore } from '@/stores/playerStore';
export function MainEditor() {
const audioUrl = usePlayerStore((state) => state.audioUrl);
return (
// Main view: Profiles top left, Generator bottom left, History right
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6 h-full min-h-0 overflow-hidden relative">
{/* Left Column */}
<div className="flex flex-col gap-6 min-h-0 overflow-y-auto pb-32">
{/* Profiles - Top Left */}
<div className="shrink-0 flex flex-col">
<ProfileList />
</div>
{/* Generator - Bottom Left */}
{/* <div className="shrink-0">
<GenerationForm />
</div> */}
</div>
{/* Right Column - History */}
<div className="flex flex-col min-h-0 overflow-hidden">
<HistoryTable />
</div>
{/* Floating Generate Box */}
<FloatingGenerateBox isPlayerOpen={!!audioUrl} />
</div>
);
}
@@ -0,0 +1,9 @@
import { ModelManagement } from '@/components/ServerSettings/ModelManagement';
export function ModelsTab() {
return (
<div className="space-y-4 overflow-y-auto flex flex-col">
<ModelManagement />
</div>
);
}
@@ -1,5 +1,4 @@
import { ConnectionForm } from '@/components/ServerSettings/ConnectionForm'; import { ConnectionForm } from '@/components/ServerSettings/ConnectionForm';
import { ModelManagement } from '@/components/ServerSettings/ModelManagement';
import { ServerStatus } from '@/components/ServerSettings/ServerStatus'; import { ServerStatus } from '@/components/ServerSettings/ServerStatus';
import { UpdateStatus } from '@/components/ServerSettings/UpdateStatus'; import { UpdateStatus } from '@/components/ServerSettings/UpdateStatus';
import { isTauri } from '@/lib/tauri'; import { isTauri } from '@/lib/tauri';
@@ -12,7 +11,6 @@ export function ServerTab() {
<ServerStatus /> <ServerStatus />
</div> </div>
{isTauri() && <UpdateStatus />} {isTauri() && <UpdateStatus />}
<ModelManagement />
<div className="py-8 text-center text-sm text-muted-foreground"> <div className="py-8 text-center text-sm text-muted-foreground">
Created by{' '} Created by{' '}
<a <a
+2 -1
View File
@@ -1,4 +1,4 @@
import { Loader2, Settings, Volume2, Mic, Speaker, Server } from 'lucide-react'; import { Loader2, Volume2, Mic, Speaker, Server, Box } from 'lucide-react';
import voiceboxLogo from '@/assets/voicebox-logo.png'; import voiceboxLogo from '@/assets/voicebox-logo.png';
import { cn } from '@/lib/utils/cn'; import { cn } from '@/lib/utils/cn';
import { useGenerationStore } from '@/stores/generationStore'; import { useGenerationStore } from '@/stores/generationStore';
@@ -14,6 +14,7 @@ const tabs = [
{ id: 'main', icon: Volume2, label: 'Generate' }, { id: 'main', icon: Volume2, label: 'Generate' },
{ id: 'voices', icon: Mic, label: 'Voices' }, { id: 'voices', icon: Mic, label: 'Voices' },
{ id: 'audio', icon: Speaker, label: 'Audio' }, { id: 'audio', icon: Speaker, label: 'Audio' },
{ id: 'models', icon: Box, label: 'Models' },
{ id: 'server', icon: Server, label: 'Server' }, { id: 'server', icon: Server, label: 'Server' },
]; ];
Binary file not shown.