mirror of
https://github.com/jamiepine/voicebox.git
synced 2026-09-15 04:40:40 -07:00
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.
240 lines
6.2 KiB
TypeScript
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;
|
|
}
|
|
}
|