Initial commit
This commit is contained in:
13
web/index.html
Normal file
13
web/index.html
Normal 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
32
web/package.json
Normal 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
28
web/src/main.tsx
Normal 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
27
web/src/platform/audio.ts
Normal 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
|
||||
},
|
||||
};
|
||||
23
web/src/platform/filesystem.ts
Normal file
23
web/src/platform/filesystem.ts
Normal 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
14
web/src/platform/index.ts
Normal 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,
|
||||
};
|
||||
37
web/src/platform/lifecycle.ts
Normal file
37
web/src/platform/lifecycle.ts
Normal 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();
|
||||
9
web/src/platform/metadata.ts
Normal file
9
web/src/platform/metadata.ts
Normal 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,
|
||||
};
|
||||
40
web/src/platform/updater.ts
Normal file
40
web/src/platform/updater.ts
Normal 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
26
web/tsconfig.json
Normal 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
10
web/tsconfig.node.json
Normal 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
17
web/vite.config.ts
Normal 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',
|
||||
},
|
||||
});
|
||||
Reference in New Issue
Block a user