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