mirror of
https://github.com/jamiepine/voicebox.git
synced 2026-09-20 07:10:40 -07:00
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:
+9
-39
@@ -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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -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>
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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.
Reference in New Issue
Block a user