diff --git a/app/src/lib/hooks/useHistory.ts b/app/src/lib/hooks/useHistory.ts index e6f4aa7a6..6b2b1dcca 100644 --- a/app/src/lib/hooks/useHistory.ts +++ b/app/src/lib/hooks/useHistory.ts @@ -78,12 +78,12 @@ export function useExportGenerationAudio() { .substring(0, 30) .replace(/[^a-z0-9]/gi, '-') .toLowerCase(); - const filename = `${safeText}.wav`; + const filename = `${safeText}.mp3`; await platform.filesystem.saveFile(filename, blob, [ { name: 'Audio File', - extensions: ['wav'], + extensions: ['mp3'], }, ]); diff --git a/app/src/lib/hooks/useStories.ts b/app/src/lib/hooks/useStories.ts index 7c7eae6c9..f7b6cebb9 100644 --- a/app/src/lib/hooks/useStories.ts +++ b/app/src/lib/hooks/useStories.ts @@ -240,12 +240,12 @@ export function useExportStoryAudio() { .substring(0, 50) .replace(/[^a-z0-9]/gi, '-') .toLowerCase(); - const filename = `${safeName || 'story'}.wav`; + const filename = `${safeName || 'story'}.mp3`; await platform.filesystem.saveFile(filename, blob, [ { name: 'Audio File', - extensions: ['wav'], + extensions: ['mp3'], }, ]); diff --git a/app/src/main.tsx b/app/src/main.tsx index 52dc90445..34b458c78 100644 --- a/app/src/main.tsx +++ b/app/src/main.tsx @@ -6,12 +6,16 @@ import App from './App'; import './i18n'; import './index.css'; import { queryClient } from './lib/queryClient'; +import { PlatformProvider } from './platform/PlatformContext'; +import { webPlatform } from './platform/webPlatform'; ReactDOM.createRoot(document.getElementById('root')!).render( - - {/* */} + + + {/* */} + , ); diff --git a/app/src/platform/webPlatform.ts b/app/src/platform/webPlatform.ts new file mode 100644 index 000000000..479fe5298 --- /dev/null +++ b/app/src/platform/webPlatform.ts @@ -0,0 +1,111 @@ +/** + * Web platform implementation + * + * Voicebox's primary target is the Tauri desktop app; the web build + * (app/ served as a static SPA by the Python backend) had no platform + * implementation after the Tauri refactor, which crashed the UI with + * "usePlatform must be used within PlatformProvider". + * + * This implementation provides browser-safe no-ops for the desktop-only + * features (updater, system audio capture, server lifecycle) and a + * download-based saveFile so audio export keeps working in the browser. + */ + +import type { + Platform, + PlatformAudio, + PlatformFilesystem, + PlatformLifecycle, + PlatformMetadata, + PlatformUpdater, + UpdateStatus, +} from './types'; + +const noop = () => {}; + +const EMPTY_UPDATE_STATUS: UpdateStatus = { + checking: false, + available: false, + downloading: false, + installing: false, + readyToInstall: false, +}; + +const webFilesystem: PlatformFilesystem = { + async saveFile(filename: string, blob: Blob) { + // Browser download via object URL — file filters are a desktop concept + const url = URL.createObjectURL(blob); + const anchor = document.createElement('a'); + anchor.href = url; + anchor.download = filename; + document.body.appendChild(anchor); + anchor.click(); + anchor.remove(); + setTimeout(() => URL.revokeObjectURL(url), 10_000); + }, + + async openPath(path: string) { + window.open(path, '_blank', 'noopener'); + }, + + async pickDirectory() { + // Browsers can't return a persistent directory path + return null; + }, +}; + +const webUpdater: PlatformUpdater = { + async checkForUpdates() {}, + async downloadAndInstall() {}, + async restartAndInstall() {}, + getStatus: () => ({ ...EMPTY_UPDATE_STATUS }), + subscribe: () => noop, +}; + +const webAudio: PlatformAudio = { + async isSystemAudioSupported() { + return false; + }, + async startSystemAudioCapture() { + throw new Error('System audio capture is not supported in the browser.'); + }, + async stopSystemAudioCapture() { + throw new Error('System audio capture is not supported in the browser.'); + }, + async listOutputDevices() { + return []; + }, + async playToDevices() { + // Web playback uses the default