Initial commit

This commit is contained in:
2026-04-24 19:18:15 +08:00
commit fbcbe08696
555 changed files with 96692 additions and 0 deletions

13
web/index.html Normal file
View File

@@ -0,0 +1,13 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>voicebox</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.tsx"></script>
</body>
</html>

32
web/package.json Normal file
View File

@@ -0,0 +1,32 @@
{
"name": "@voicebox/web",
"private": true,
"version": "0.4.5",
"type": "module",
"scripts": {
"dev": "vite",
"build": "tsc && vite build",
"preview": "vite preview",
"lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0"
},
"dependencies": {
"react": "^18.3.0",
"react-dom": "^18.3.0",
"@tanstack/react-query": "^5.0.0",
"zustand": "^4.5.0",
"wavesurfer.js": "^7.0.0"
},
"devDependencies": {
"@types/react": "^18.3.0",
"@types/react-dom": "^18.3.0",
"@typescript-eslint/eslint-plugin": "^7.0.0",
"@typescript-eslint/parser": "^7.0.0",
"@tailwindcss/vite": "^4.0.0",
"@vitejs/plugin-react": "^4.3.0",
"eslint": "^8.57.0",
"eslint-plugin-react-hooks": "^4.6.0",
"eslint-plugin-react-refresh": "^0.4.0",
"typescript": "^5.6.0",
"vite": "^5.4.0"
}
}

28
web/src/main.tsx Normal file
View File

@@ -0,0 +1,28 @@
import React from 'react';
import ReactDOM from 'react-dom/client';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import App from '../../app/src/App';
import '../../app/src/index.css';
import { PlatformProvider } from '../../app/src/platform/PlatformContext';
import { webPlatform } from './platform';
const queryClient = new QueryClient({
defaultOptions: {
queries: {
staleTime: 1000 * 60 * 5, // 5 minutes
gcTime: 1000 * 60 * 10, // 10 minutes
retry: 1,
refetchOnWindowFocus: false,
},
},
});
ReactDOM.createRoot(document.getElementById('root')!).render(
<React.StrictMode>
<QueryClientProvider client={queryClient}>
<PlatformProvider platform={webPlatform}>
<App />
</PlatformProvider>
</QueryClientProvider>
</React.StrictMode>,
);

27
web/src/platform/audio.ts Normal file
View File

@@ -0,0 +1,27 @@
import type { PlatformAudio, AudioDevice } from '@/platform/types';
export const webAudio: PlatformAudio = {
async isSystemAudioSupported(): Promise<boolean> {
return false; // System audio capture not supported in web
},
async startSystemAudioCapture(_maxDurationSecs: number): Promise<void> {
throw new Error('System audio capture is only available in the desktop app.');
},
async stopSystemAudioCapture(): Promise<Blob> {
throw new Error('System audio capture is only available in the desktop app.');
},
async listOutputDevices(): Promise<AudioDevice[]> {
return []; // No native device routing in web
},
async playToDevices(_audioData: Uint8Array, _deviceIds: string[]): Promise<void> {
throw new Error('Native audio device routing is only available in the desktop app.');
},
stopPlayback(): void {
// No-op for web
},
};

View File

@@ -0,0 +1,23 @@
import type { FileFilter, PlatformFilesystem } from '@/platform/types';
export const webFilesystem: PlatformFilesystem = {
async saveFile(filename: string, blob: Blob, _filters?: FileFilter[]) {
// Browser: trigger download
const url = window.URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = filename;
document.body.appendChild(a);
a.click();
window.URL.revokeObjectURL(url);
document.body.removeChild(a);
},
async openPath(_path: string) {
// No filesystem access in browser
},
async pickDirectory(_title: string) {
return null;
},
};

14
web/src/platform/index.ts Normal file
View File

@@ -0,0 +1,14 @@
import type { Platform } from '@/platform/types';
import { webFilesystem } from './filesystem';
import { webUpdater } from './updater';
import { webAudio } from './audio';
import { webLifecycle } from './lifecycle';
import { webMetadata } from './metadata';
export const webPlatform: Platform = {
filesystem: webFilesystem,
updater: webUpdater,
audio: webAudio,
lifecycle: webLifecycle,
metadata: webMetadata,
};

View File

@@ -0,0 +1,37 @@
import type { PlatformLifecycle, ServerLogEntry } from '@/platform/types';
class WebLifecycle implements PlatformLifecycle {
onServerReady?: () => void;
async startServer(_remote = false, _modelsDir?: string | null): Promise<string> {
// Web assumes server is running externally
// Return a default URL - this should be configured via env vars
const serverUrl = import.meta.env.VITE_SERVER_URL || 'http://localhost:17493';
this.onServerReady?.();
return serverUrl;
}
async stopServer(): Promise<void> {
// No-op for web - server is managed externally
}
async restartServer(_modelsDir?: string | null): Promise<string> {
// No-op for web - server is managed externally
return import.meta.env.VITE_SERVER_URL || 'http://localhost:17493';
}
async setKeepServerRunning(_keep: boolean): Promise<void> {
// No-op for web
}
async setupWindowCloseHandler(): Promise<void> {
// No-op for web - no window close handling needed
}
subscribeToServerLogs(_callback: (_entry: ServerLogEntry) => void): () => void {
// No-op for web - server logs are not available
return () => {};
}
}
export const webLifecycle = new WebLifecycle();

View File

@@ -0,0 +1,9 @@
import type { PlatformMetadata } from '@/platform/types';
export const webMetadata: PlatformMetadata = {
async getVersion(): Promise<string> {
// Return version from env var or package.json
return import.meta.env.VITE_APP_VERSION || '0.1.0';
},
isTauri: false,
};

View File

@@ -0,0 +1,40 @@
import type { PlatformUpdater, UpdateStatus } from '@/platform/types';
class WebUpdater implements PlatformUpdater {
private status: UpdateStatus = {
checking: false,
available: false,
downloading: false,
installing: false,
readyToInstall: false,
};
private subscribers: Set<(status: UpdateStatus) => void> = new Set();
subscribe(callback: (status: UpdateStatus) => void): () => void {
this.subscribers.add(callback);
callback(this.status);
return () => {
this.subscribers.delete(callback);
};
}
getStatus(): UpdateStatus {
return { ...this.status };
}
async checkForUpdates(): Promise<void> {
// Web apps don't need client-side updates
// Updates are handled by redeploying the web app
}
async downloadAndInstall(): Promise<void> {
// No-op for web
}
async restartAndInstall(): Promise<void> {
// No-op for web
}
}
export const webUpdater = new WebUpdater();

26
web/tsconfig.json Normal file
View File

@@ -0,0 +1,26 @@
{
"compilerOptions": {
"target": "ES2020",
"useDefineForClassFields": true,
"lib": ["ES2020", "DOM", "DOM.Iterable"],
"module": "ESNext",
"skipLibCheck": true,
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"resolveJsonModule": true,
"isolatedModules": true,
"noEmit": true,
"jsx": "react-jsx",
"strict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"noFallthroughCasesInSwitch": true,
"baseUrl": ".",
"paths": {
"@/*": ["../app/src/*"]
},
"types": ["vite/client"]
},
"include": ["src", "../app/src/global.d.ts"],
"references": [{ "path": "./tsconfig.node.json" }]
}

10
web/tsconfig.node.json Normal file
View File

@@ -0,0 +1,10 @@
{
"compilerOptions": {
"composite": true,
"skipLibCheck": true,
"module": "ESNext",
"moduleResolution": "bundler",
"allowSyntheticDefaultImports": true
},
"include": ["vite.config.ts"]
}

17
web/vite.config.ts Normal file
View File

@@ -0,0 +1,17 @@
import path from 'node:path';
import tailwindcss from '@tailwindcss/vite';
import react from '@vitejs/plugin-react';
import { defineConfig } from 'vite';
import { changelogPlugin } from '../app/plugins/changelog';
export default defineConfig({
plugins: [react(), tailwindcss(), changelogPlugin(path.resolve(__dirname, '..'))],
resolve: {
alias: {
'@': path.resolve(__dirname, '../app/src'),
},
},
build: {
outDir: 'dist',
},
});