Files
voicebox/app/src/router.tsx
T
James Pine f4d21504e3 Mobile companion app + paired-device backend
New iOS-first companion (Expo SDK 54 + NativeWind v4) with three tabs:
Captures (the hero — floating gold mic, live mic-meter waveform,
expand-row playback), Generate (profile picker + speak + autoplay +
recent), and Voices (searchable profile list).

Pairing (V0): backend mints a one-time token, mobile scans/pastes the
voicebox:// URL, server returns a long-lived bearer it stores only as a
SHA-256 hash. Bearer-or-loopback auth applied to every user-data router
so binding 0.0.0.0 doesn't leak existing endpoints. Loopback callers
(the desktop app) keep their friction-free access.

Desktop Settings → Mobile pane: live host picker (LAN / Tailscale auto-
detected via the App-bundle binary path on macOS), QR rendering,
5-minute expiry countdown, copyable URL fallback, paired-device list
with revoke. Auto-closes when a new device pairs.

just dev now binds the backend to 0.0.0.0 so paired phones can reach
it — and just setup-python pins mlx-audio==0.4.1 + mlx-lm so fresh
Apple Silicon worktrees get a working STT path on first install.
2026-04-25 17:09:32 -07:00

240 lines
6.2 KiB
TypeScript

import {
createRootRoute,
createRoute,
createRouter,
Outlet,
redirect,
} from '@tanstack/react-router';
import { AppFrame } from '@/components/AppFrame/AppFrame';
import { CapturesTab } from '@/components/CapturesTab/CapturesTab';
import { EffectsTab } from '@/components/EffectsTab/EffectsTab';
import { MainEditor } from '@/components/MainEditor/MainEditor';
import { ModelsTab } from '@/components/ModelsTab/ModelsTab';
import { AboutPage } from '@/components/ServerTab/AboutPage';
import { CapturesPage } from '@/components/ServerTab/CapturesPage';
import { ChangelogPage } from '@/components/ServerTab/ChangelogPage';
import { GeneralPage } from '@/components/ServerTab/GeneralPage';
import { GenerationPage } from '@/components/ServerTab/GenerationPage';
import { GpuPage } from '@/components/ServerTab/GpuPage';
import { LogsPage } from '@/components/ServerTab/LogsPage';
import { MCPPage } from '@/components/ServerTab/MCPPage';
import { MobilePage } from '@/components/ServerTab/MobilePage';
import { SettingsLayout } from '@/components/ServerTab/ServerTab';
import { Sidebar } from '@/components/Sidebar';
import { StoriesTab } from '@/components/StoriesTab/StoriesTab';
import { Toaster } from '@/components/ui/toaster';
import { VoicesTab } from '@/components/VoicesTab/VoicesTab';
import { useGenerationProgress } from '@/lib/hooks/useGenerationProgress';
import { useModelDownloadToast } from '@/lib/hooks/useModelDownloadToast';
import { MODEL_DISPLAY_NAMES, useRestoreActiveTasks } from '@/lib/hooks/useRestoreActiveTasks';
// Simple platform check that works in both web and Tauri
const isMacOS = () => navigator.platform.toLowerCase().includes('mac');
// Root layout component
function RootLayout() {
// Monitor active downloads/generations and show toasts for them
const activeDownloads = useRestoreActiveTasks();
// Subscribe to SSE for pending generations — handles completion, auto-play, and history refresh
useGenerationProgress();
return (
<AppFrame>
<div className="flex flex-1 min-h-0 overflow-hidden">
<Sidebar isMacOS={isMacOS()} />
<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">
<Outlet />
</div>
</main>
</div>
{/* Show download toasts for any active downloads (from anywhere) */}
{activeDownloads.map((download) => {
const displayName = MODEL_DISPLAY_NAMES[download.model_name] || download.model_name;
return (
<DownloadToastRestorer
key={download.model_name}
modelName={download.model_name}
displayName={displayName}
/>
);
})}
<Toaster />
</AppFrame>
);
}
/**
* 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,
});
// Stories route
const storiesRoute = createRoute({
getParentRoute: () => rootRoute,
path: '/stories',
component: StoriesTab,
});
// Voices route
const voicesRoute = createRoute({
getParentRoute: () => rootRoute,
path: '/voices',
component: VoicesTab,
});
// Captures route (prototype — will replace AudioTab once the new flow is ready)
const capturesRoute = createRoute({
getParentRoute: () => rootRoute,
path: '/captures',
component: CapturesTab,
});
// Effects route
const effectsRoute = createRoute({
getParentRoute: () => rootRoute,
path: '/effects',
component: EffectsTab,
});
// Models route
const modelsRoute = createRoute({
getParentRoute: () => rootRoute,
path: '/models',
component: ModelsTab,
});
// Settings layout route (parent for sub-tabs)
const settingsRoute = createRoute({
getParentRoute: () => rootRoute,
path: '/settings',
component: SettingsLayout,
});
// Settings sub-routes
const settingsGeneralRoute = createRoute({
getParentRoute: () => settingsRoute,
path: '/',
component: GeneralPage,
});
const settingsGenerationRoute = createRoute({
getParentRoute: () => settingsRoute,
path: '/generation',
component: GenerationPage,
});
const settingsCapturesRoute = createRoute({
getParentRoute: () => settingsRoute,
path: '/captures',
component: CapturesPage,
});
const settingsMCPRoute = createRoute({
getParentRoute: () => settingsRoute,
path: '/mcp',
component: MCPPage,
});
const settingsMobileRoute = createRoute({
getParentRoute: () => settingsRoute,
path: '/mobile',
component: MobilePage,
});
const settingsGpuRoute = createRoute({
getParentRoute: () => settingsRoute,
path: '/gpu',
component: GpuPage,
});
const settingsChangelogRoute = createRoute({
getParentRoute: () => settingsRoute,
path: '/changelog',
component: ChangelogPage,
});
const settingsLogsRoute = createRoute({
getParentRoute: () => settingsRoute,
path: '/logs',
component: LogsPage,
});
const settingsAboutRoute = createRoute({
getParentRoute: () => settingsRoute,
path: '/about',
component: AboutPage,
});
// Redirect old /server path to /settings
const serverRedirectRoute = createRoute({
getParentRoute: () => rootRoute,
path: '/server',
beforeLoad: () => {
throw redirect({ to: '/settings' });
},
});
// Route tree
const routeTree = rootRoute.addChildren([
indexRoute,
storiesRoute,
capturesRoute,
voicesRoute,
effectsRoute,
modelsRoute,
settingsRoute.addChildren([
settingsGeneralRoute,
settingsGenerationRoute,
settingsCapturesRoute,
settingsMCPRoute,
settingsMobileRoute,
settingsGpuRoute,
settingsLogsRoute,
settingsChangelogRoute,
settingsAboutRoute,
]),
serverRedirectRoute,
]);
// Create router
export const router = createRouter({ routeTree });
// Register router for type safety
declare module '@tanstack/react-router' {
interface Register {
router: typeof router;
}
}