diff --git a/app/package.json b/app/package.json
index fbf7f775..fc19a740 100644
--- a/app/package.json
+++ b/app/package.json
@@ -30,6 +30,7 @@
"@radix-ui/react-toast": "^1.2.1",
"@tanstack/react-query": "^5.0.0",
"@tanstack/react-query-devtools": "^5.0.0",
+ "@tanstack/react-router": "^1.157.16",
"@tauri-apps/api": "^2.0.0",
"@tauri-apps/plugin-dialog": "^2.0.0",
"@tauri-apps/plugin-fs": "^2.0.0",
diff --git a/app/src/App.tsx b/app/src/App.tsx
index a832aa48..84d8fc8d 100644
--- a/app/src/App.tsx
+++ b/app/src/App.tsx
@@ -1,27 +1,17 @@
import { useEffect, useState } from 'react';
+import { RouterProvider } from '@tanstack/react-router';
import voiceboxLogo from '@/assets/voicebox-logo.png';
-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 ShinyText from '@/components/ShinyText';
-import { Sidebar } from '@/components/Sidebar';
import { TitleBarDragRegion } from '@/components/TitleBarDragRegion';
-import { Toaster } from '@/components/ui/toaster';
-import { VoicesTab } from '@/components/VoicesTab/VoicesTab';
import { TOP_SAFE_AREA_PADDING } from '@/lib/constants/ui';
-import { useModelDownloadToast } from '@/lib/hooks/useModelDownloadToast';
-import { MODEL_DISPLAY_NAMES, useRestoreActiveTasks } from '@/lib/hooks/useRestoreActiveTasks';
import {
- isMacOS,
isTauri,
setKeepServerRunning,
setupWindowCloseHandler,
startServer,
} from '@/lib/tauri';
import { cn } from '@/lib/utils/cn';
+import { router } from '@/router';
import { useServerStore } from '@/stores/serverStore';
// Track if server is starting to prevent duplicate starts
@@ -51,13 +41,9 @@ const LOADING_MESSAGES = [
];
function App() {
- const [activeTab, setActiveTab] = useState('main');
const [serverReady, setServerReady] = useState(false);
const [loadingMessageIndex, setLoadingMessageIndex] = useState(0);
- // Monitor active downloads/generations and show toasts for them
- const activeDownloads = useRestoreActiveTasks();
-
// Sync stored setting to Rust on startup
useEffect(() => {
if (isTauri()) {
@@ -172,57 +158,7 @@ function App() {
);
}
- return (
-
-
-
-
-
-
- {activeTab === 'main' &&
}
- {activeTab === 'voices' &&
}
- {activeTab === 'audio' &&
}
- {activeTab === 'server' &&
}
- {activeTab === 'models' &&
}
-
-
-
-
- {/* Show download toasts for any active downloads (from anywhere) */}
- {activeDownloads.map((download) => {
- const displayName = MODEL_DISPLAY_NAMES[download.model_name] || download.model_name;
- return (
-
- );
- })}
-
-
-
- );
-}
-
-/**
- * Component that restores a download toast for a specific model.
- */
-function DownloadToastRestorer({
- modelName,
- displayName,
-}: {
- modelName: string;
- displayName: string;
-}) {
- // Use the download toast hook to restore the toast
- useModelDownloadToast({
- modelName,
- displayName,
- enabled: true,
- });
-
- return null;
+ return ;
}
export default App;
diff --git a/app/src/components/Sidebar.tsx b/app/src/components/Sidebar.tsx
index 6ec5c4a4..5fdc16a6 100644
--- a/app/src/components/Sidebar.tsx
+++ b/app/src/components/Sidebar.tsx
@@ -1,3 +1,4 @@
+import { Link, useMatchRoute } from '@tanstack/react-router';
import { Box, Loader2, Mic, Server, Speaker, Volume2 } from 'lucide-react';
import voiceboxLogo from '@/assets/voicebox-logo.png';
import { cn } from '@/lib/utils/cn';
@@ -5,23 +6,22 @@ import { useGenerationStore } from '@/stores/generationStore';
import { usePlayerStore } from '@/stores/playerStore';
interface SidebarProps {
- activeTab: string;
- onTabChange: (tab: string) => void;
isMacOS?: boolean;
}
const tabs = [
- { id: 'main', icon: Volume2, label: 'Generate' },
- { id: 'voices', icon: Mic, label: 'Voices' },
- { id: 'audio', icon: Speaker, label: 'Audio' },
- { id: 'models', icon: Box, label: 'Models' },
- { id: 'server', icon: Server, label: 'Server' },
+ { id: 'main', path: '/', icon: Volume2, label: 'Generate' },
+ { id: 'voices', path: '/voices', icon: Mic, label: 'Voices' },
+ { id: 'audio', path: '/audio', icon: Speaker, label: 'Audio' },
+ { id: 'models', path: '/models', icon: Box, label: 'Models' },
+ { id: 'server', path: '/server', icon: Server, label: 'Server' },
];
-export function Sidebar({ activeTab, onTabChange, isMacOS }: SidebarProps) {
+export function Sidebar({ isMacOS }: SidebarProps) {
const isGenerating = useGenerationStore((state) => state.isGenerating);
const audioUrl = usePlayerStore((state) => state.audioUrl);
const isPlayerVisible = !!audioUrl;
+ const matchRoute = useMatchRoute();
return (
{tabs.map((tab) => {
const Icon = tab.icon;
- const isActive = activeTab === tab.id;
+ // For index route, use exact match; for others, use default matching
+ const isActive =
+ tab.path === '/'
+ ? matchRoute({ to: '/', exact: true })
+ : matchRoute({ to: tab.path });
return (
-
+
);
})}
diff --git a/app/src/router.tsx b/app/src/router.tsx
new file mode 100644
index 00000000..b9ce6a6e
--- /dev/null
+++ b/app/src/router.tsx
@@ -0,0 +1,125 @@
+import { createRootRoute, createRoute, createRouter, Outlet } from '@tanstack/react-router';
+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 { Sidebar } from '@/components/Sidebar';
+import { Toaster } from '@/components/ui/toaster';
+import { VoicesTab } from '@/components/VoicesTab/VoicesTab';
+import { useModelDownloadToast } from '@/lib/hooks/useModelDownloadToast';
+import { MODEL_DISPLAY_NAMES, useRestoreActiveTasks } from '@/lib/hooks/useRestoreActiveTasks';
+import { isMacOS } from '@/lib/tauri';
+
+// Root layout component
+function RootLayout() {
+ // Monitor active downloads/generations and show toasts for them
+ const activeDownloads = useRestoreActiveTasks();
+
+ return (
+
+
+
+ {/* Show download toasts for any active downloads (from anywhere) */}
+ {activeDownloads.map((download) => {
+ const displayName = MODEL_DISPLAY_NAMES[download.model_name] || download.model_name;
+ return (
+
+ );
+ })}
+
+
+
+ );
+}
+
+/**
+ * Component that restores a download toast for a specific model.
+ */
+function DownloadToastRestorer({
+ modelName,
+ displayName,
+}: {
+ modelName: string;
+ displayName: string;
+}) {
+ // Use the download toast hook to restore the toast
+ useModelDownloadToast({
+ modelName,
+ displayName,
+ enabled: true,
+ });
+
+ return null;
+}
+
+// Root route with layout
+const rootRoute = createRootRoute({
+ component: RootLayout,
+});
+
+// Index route (main/generate)
+const indexRoute = createRoute({
+ getParentRoute: () => rootRoute,
+ path: '/',
+ component: MainEditor,
+});
+
+// Voices route
+const voicesRoute = createRoute({
+ getParentRoute: () => rootRoute,
+ path: '/voices',
+ component: VoicesTab,
+});
+
+// Audio route
+const audioRoute = createRoute({
+ getParentRoute: () => rootRoute,
+ path: '/audio',
+ component: AudioTab,
+});
+
+// Models route
+const modelsRoute = createRoute({
+ getParentRoute: () => rootRoute,
+ path: '/models',
+ component: ModelsTab,
+});
+
+// Server route
+const serverRoute = createRoute({
+ getParentRoute: () => rootRoute,
+ path: '/server',
+ component: ServerTab,
+});
+
+// Route tree
+const routeTree = rootRoute.addChildren([
+ indexRoute,
+ voicesRoute,
+ audioRoute,
+ modelsRoute,
+ serverRoute,
+]);
+
+// Create router
+export const router = createRouter({ routeTree });
+
+// Register router for type safety
+declare module '@tanstack/react-router' {
+ interface Register {
+ router: typeof router;
+ }
+}
diff --git a/bun.lock b/bun.lock
index ab6ce78b..ee866528 100644
--- a/bun.lock
+++ b/bun.lock
@@ -13,7 +13,7 @@
},
"app": {
"name": "@voicebox/app",
- "version": "0.1.0",
+ "version": "0.1.4",
"dependencies": {
"@hookform/resolvers": "^3.9.0",
"@radix-ui/react-alert-dialog": "^1.1.1",
@@ -32,6 +32,7 @@
"@radix-ui/react-toast": "^1.2.1",
"@tanstack/react-query": "^5.0.0",
"@tanstack/react-query-devtools": "^5.0.0",
+ "@tanstack/react-router": "^1.157.16",
"@tauri-apps/api": "^2.0.0",
"@tauri-apps/plugin-dialog": "^2.0.0",
"@tauri-apps/plugin-fs": "^2.0.0",
@@ -63,7 +64,7 @@
},
"landing": {
"name": "@voicebox/landing",
- "version": "0.1.0",
+ "version": "0.1.4",
"dependencies": {
"@radix-ui/react-separator": "^1.1.8",
"@radix-ui/react-slot": "^1.2.4",
@@ -88,7 +89,7 @@
},
"tauri": {
"name": "@voicebox/tauri",
- "version": "0.1.0",
+ "version": "0.1.4",
"dependencies": {
"@tauri-apps/api": "^2.0.0",
"@tauri-apps/plugin-shell": "^2.0.0",
@@ -107,7 +108,7 @@
},
"web": {
"name": "@voicebox/web",
- "version": "0.1.0",
+ "version": "0.1.4",
"dependencies": {
"@tanstack/react-query": "^5.0.0",
"react": "^18.3.0",
@@ -512,6 +513,8 @@
"@tailwindcss/vite": ["@tailwindcss/vite@4.1.18", "", { "dependencies": { "@tailwindcss/node": "4.1.18", "@tailwindcss/oxide": "4.1.18", "tailwindcss": "4.1.18" }, "peerDependencies": { "vite": "^5.2.0 || ^6 || ^7" } }, "sha512-jVA+/UpKL1vRLg6Hkao5jldawNmRo7mQYrZtNHMIVpLfLhDml5nMRUo/8MwoX2vNXvnaXNNMedrMfMugAVX1nA=="],
+ "@tanstack/history": ["@tanstack/history@1.154.14", "", {}, "sha512-xyIfof8eHBuub1CkBnbKNKQXeRZC4dClhmzePHVOEel4G7lk/dW+TQ16da7CFdeNLv6u6Owf5VoBQxoo6DFTSA=="],
+
"@tanstack/query-core": ["@tanstack/query-core@5.90.20", "", {}, "sha512-OMD2HLpNouXEfZJWcKeVKUgQ5n+n3A2JFmBaScpNDUqSrQSjiveC7dKMe53uJUg1nDG16ttFPz2xfilz6i2uVg=="],
"@tanstack/query-devtools": ["@tanstack/query-devtools@5.92.0", "", {}, "sha512-N8D27KH1vEpVacvZgJL27xC6yPFUy0Zkezn5gnB3L3gRCxlDeSuiya7fKge8Y91uMTnC8aSxBQhcK6ocY7alpQ=="],
@@ -520,6 +523,14 @@
"@tanstack/react-query-devtools": ["@tanstack/react-query-devtools@5.91.2", "", { "dependencies": { "@tanstack/query-devtools": "5.92.0" }, "peerDependencies": { "@tanstack/react-query": "^5.90.14", "react": "^18 || ^19" } }, "sha512-ZJ1503ay5fFeEYFUdo7LMNFzZryi6B0Cacrgr2h1JRkvikK1khgIq6Nq2EcblqEdIlgB/r7XDW8f8DQ89RuUgg=="],
+ "@tanstack/react-router": ["@tanstack/react-router@1.157.16", "", { "dependencies": { "@tanstack/history": "1.154.14", "@tanstack/react-store": "^0.8.0", "@tanstack/router-core": "1.157.16", "isbot": "^5.1.22", "tiny-invariant": "^1.3.3", "tiny-warning": "^1.0.3" }, "peerDependencies": { "react": ">=18.0.0 || >=19.0.0", "react-dom": ">=18.0.0 || >=19.0.0" } }, "sha512-xwFQa7S7dhBhm3aJYwU79cITEYgAKSrcL6wokaROIvl2JyIeazn8jueWqUPJzFjv+QF6Q8euKRlKUEyb5q2ymg=="],
+
+ "@tanstack/react-store": ["@tanstack/react-store@0.8.0", "", { "dependencies": { "@tanstack/store": "0.8.0", "use-sync-external-store": "^1.6.0" }, "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-1vG9beLIuB7q69skxK9r5xiLN3ztzIPfSQSs0GfeqWGO2tGIyInZx0x1COhpx97RKaONSoAb8C3dxacWksm1ow=="],
+
+ "@tanstack/router-core": ["@tanstack/router-core@1.157.16", "", { "dependencies": { "@tanstack/history": "1.154.14", "@tanstack/store": "^0.8.0", "cookie-es": "^2.0.0", "seroval": "^1.4.2", "seroval-plugins": "^1.4.2", "tiny-invariant": "^1.3.3", "tiny-warning": "^1.0.3" } }, "sha512-eJuVgM7KZYTTr4uPorbUzUflmljMVcaX2g6VvhITLnHmg9SBx9RAgtQ1HmT+72mzyIbRSlQ1q0fY/m+of/fosA=="],
+
+ "@tanstack/store": ["@tanstack/store@0.8.0", "", {}, "sha512-Om+BO0YfMZe//X2z0uLF2j+75nQga6TpTJgLJQBiq85aOyZNIhkCgleNcud2KQg4k4v9Y9l+Uhru3qWMPGTOzQ=="],
+
"@tauri-apps/api": ["@tauri-apps/api@2.9.1", "", {}, "sha512-IGlhP6EivjXHepbBic618GOmiWe4URJiIeZFlB7x3czM0yDHHYviH1Xvoiv4FefdkQtn6v7TuwWCRfOGdnVUGw=="],
"@tauri-apps/cli": ["@tauri-apps/cli@2.9.6", "", { "optionalDependencies": { "@tauri-apps/cli-darwin-arm64": "2.9.6", "@tauri-apps/cli-darwin-x64": "2.9.6", "@tauri-apps/cli-linux-arm-gnueabihf": "2.9.6", "@tauri-apps/cli-linux-arm64-gnu": "2.9.6", "@tauri-apps/cli-linux-arm64-musl": "2.9.6", "@tauri-apps/cli-linux-riscv64-gnu": "2.9.6", "@tauri-apps/cli-linux-x64-gnu": "2.9.6", "@tauri-apps/cli-linux-x64-musl": "2.9.6", "@tauri-apps/cli-win32-arm64-msvc": "2.9.6", "@tauri-apps/cli-win32-ia32-msvc": "2.9.6", "@tauri-apps/cli-win32-x64-msvc": "2.9.6" }, "bin": { "tauri": "tauri.js" } }, "sha512-3xDdXL5omQ3sPfBfdC8fCtDKcnyV7OqyzQgfyT5P3+zY6lcPqIYKQBvUasNvppi21RSdfhy44ttvJmftb0PCDw=="],
@@ -664,6 +675,8 @@
"convert-source-map": ["convert-source-map@2.0.0", "", {}, "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg=="],
+ "cookie-es": ["cookie-es@2.0.0", "", {}, "sha512-RAj4E421UYRgqokKUmotqAwuplYw15qtdXfY+hGzgCJ/MBjCVZcSoHK/kH9kocfjRjcDME7IiDWR/1WX1TM2Pg=="],
+
"cross-spawn": ["cross-spawn@7.0.6", "", { "dependencies": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", "which": "^2.0.1" } }, "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA=="],
"cssesc": ["cssesc@3.0.0", "", { "bin": { "cssesc": "bin/cssesc" } }, "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg=="],
@@ -792,6 +805,8 @@
"is-path-inside": ["is-path-inside@3.0.3", "", {}, "sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ=="],
+ "isbot": ["isbot@5.1.34", "", {}, "sha512-aCMIBSKd/XPRYdiCQTLC8QHH4YT8B3JUADu+7COgYIZPvkeoMcUHMRjZLM9/7V8fCj+l7FSREc1lOPNjzogo/A=="],
+
"isexe": ["isexe@2.0.0", "", {}, "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw=="],
"jiti": ["jiti@1.21.7", "", { "bin": { "jiti": "bin/jiti.js" } }, "sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A=="],
@@ -966,6 +981,10 @@
"semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="],
+ "seroval": ["seroval@1.5.0", "", {}, "sha512-OE4cvmJ1uSPrKorFIH9/w/Qwuvi/IMcGbv5RKgcJ/zjA/IohDLU6SVaxFN9FwajbP7nsX0dQqMDes1whk3y+yw=="],
+
+ "seroval-plugins": ["seroval-plugins@1.5.0", "", { "peerDependencies": { "seroval": "^1.0" } }, "sha512-EAHqADIQondwRZIdeW2I636zgsODzoBDwb3PT/+7TLDWyw1Dy/Xv7iGUIEXXav7usHDE9HVhOU61irI3EnyyHA=="],
+
"sharp": ["sharp@0.34.5", "", { "dependencies": { "@img/colour": "^1.0.0", "detect-libc": "^2.1.2", "semver": "^7.7.3" }, "optionalDependencies": { "@img/sharp-darwin-arm64": "0.34.5", "@img/sharp-darwin-x64": "0.34.5", "@img/sharp-libvips-darwin-arm64": "1.2.4", "@img/sharp-libvips-darwin-x64": "1.2.4", "@img/sharp-libvips-linux-arm": "1.2.4", "@img/sharp-libvips-linux-arm64": "1.2.4", "@img/sharp-libvips-linux-ppc64": "1.2.4", "@img/sharp-libvips-linux-riscv64": "1.2.4", "@img/sharp-libvips-linux-s390x": "1.2.4", "@img/sharp-libvips-linux-x64": "1.2.4", "@img/sharp-libvips-linuxmusl-arm64": "1.2.4", "@img/sharp-libvips-linuxmusl-x64": "1.2.4", "@img/sharp-linux-arm": "0.34.5", "@img/sharp-linux-arm64": "0.34.5", "@img/sharp-linux-ppc64": "0.34.5", "@img/sharp-linux-riscv64": "0.34.5", "@img/sharp-linux-s390x": "0.34.5", "@img/sharp-linux-x64": "0.34.5", "@img/sharp-linuxmusl-arm64": "0.34.5", "@img/sharp-linuxmusl-x64": "0.34.5", "@img/sharp-wasm32": "0.34.5", "@img/sharp-win32-arm64": "0.34.5", "@img/sharp-win32-ia32": "0.34.5", "@img/sharp-win32-x64": "0.34.5" } }, "sha512-Ou9I5Ft9WNcCbXrU9cMgPBcCK8LiwLqcbywW3t4oDV37n1pzpuNLsYiAV8eODnjbtQlSDwZ2cUEeQz4E54Hltg=="],
"shebang-command": ["shebang-command@2.0.0", "", { "dependencies": { "shebang-regex": "^3.0.0" } }, "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA=="],
@@ -1002,6 +1021,10 @@
"thenify-all": ["thenify-all@1.6.0", "", { "dependencies": { "thenify": ">= 3.1.0 < 4" } }, "sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA=="],
+ "tiny-invariant": ["tiny-invariant@1.3.3", "", {}, "sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg=="],
+
+ "tiny-warning": ["tiny-warning@1.0.3", "", {}, "sha512-lBN9zLN/oAf68o3zNXYrdCt1kP8WsiGW8Oo2ka41b2IM5JL/S1CTyX1rW0mb/zSuJun0ZUrDxx4sqvYS2FWzPA=="],
+
"tinyglobby": ["tinyglobby@0.2.15", "", { "dependencies": { "fdir": "^6.5.0", "picomatch": "^4.0.3" } }, "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ=="],
"to-regex-range": ["to-regex-range@5.0.1", "", { "dependencies": { "is-number": "^7.0.0" } }, "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ=="],
diff --git a/tauri/src-tauri/Cargo.lock b/tauri/src-tauri/Cargo.lock
index 5bba547c..2f73203a 100644
--- a/tauri/src-tauri/Cargo.lock
+++ b/tauri/src-tauri/Cargo.lock
@@ -4840,7 +4840,7 @@ checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a"
[[package]]
name = "voicebox"
-version = "0.1.3"
+version = "0.1.4"
dependencies = [
"base64 0.22.1",
"core-foundation-sys",
diff --git a/tauri/src-tauri/gen/Assets.car b/tauri/src-tauri/gen/Assets.car
index 3cdfc5c1..ebcc4552 100644
Binary files a/tauri/src-tauri/gen/Assets.car and b/tauri/src-tauri/gen/Assets.car differ