Spaces:
Running
Running
Joel Woodfield commited on
Commit ·
de03c4e
1
Parent(s): 1229c63
Initial commit
Browse files- backend/src/manager.py +6 -0
- frontends/react/.gitignore +24 -0
- frontends/react/README.md +73 -0
- frontends/react/eslint.config.js +23 -0
- frontends/react/index.html +13 -0
- frontends/react/package-lock.json +0 -0
- frontends/react/package.json +33 -0
- frontends/react/public/vite.svg +1 -0
- frontends/react/src/App.tsx +20 -0
- frontends/react/src/NetworkViewer.tsx +7 -0
- frontends/react/src/PyodideBackend.ts +90 -0
- frontends/react/src/Sidebar.tsx +36 -0
- frontends/react/src/assets/react.svg +1 -0
- frontends/react/src/index.css +1 -0
- frontends/react/src/main.tsx +10 -0
- frontends/react/src/pyodide.worker.ts +6 -0
- frontends/react/src/ui/Button.tsx +15 -0
- frontends/react/src/ui/Dropdown.tsx +25 -0
- frontends/react/src/ui/FileUploader.tsx +36 -0
- frontends/react/src/ui/FilesUploader.tsx +50 -0
- frontends/react/src/ui/InputField.tsx +19 -0
- frontends/react/src/ui/LoadingScreen.tsx +19 -0
- frontends/react/src/ui/Radio.tsx +27 -0
- frontends/react/src/ui/Tabs.tsx +25 -0
- frontends/react/src/useAppLogic.ts +10 -0
- frontends/react/src/usePyodideBackend.ts +53 -0
- frontends/react/tsconfig.app.json +28 -0
- frontends/react/tsconfig.json +7 -0
- frontends/react/tsconfig.node.json +26 -0
- frontends/react/vite.config.ts +11 -0
backend/src/manager.py
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import numpy as np
|
| 2 |
+
import tinygrad
|
| 3 |
+
|
| 4 |
+
class Manager:
|
| 5 |
+
def __init__(self):
|
| 6 |
+
print("Manager initialized")
|
frontends/react/.gitignore
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Logs
|
| 2 |
+
logs
|
| 3 |
+
*.log
|
| 4 |
+
npm-debug.log*
|
| 5 |
+
yarn-debug.log*
|
| 6 |
+
yarn-error.log*
|
| 7 |
+
pnpm-debug.log*
|
| 8 |
+
lerna-debug.log*
|
| 9 |
+
|
| 10 |
+
node_modules
|
| 11 |
+
dist
|
| 12 |
+
dist-ssr
|
| 13 |
+
*.local
|
| 14 |
+
|
| 15 |
+
# Editor directories and files
|
| 16 |
+
.vscode/*
|
| 17 |
+
!.vscode/extensions.json
|
| 18 |
+
.idea
|
| 19 |
+
.DS_Store
|
| 20 |
+
*.suo
|
| 21 |
+
*.ntvs*
|
| 22 |
+
*.njsproj
|
| 23 |
+
*.sln
|
| 24 |
+
*.sw?
|
frontends/react/README.md
ADDED
|
@@ -0,0 +1,73 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# React + TypeScript + Vite
|
| 2 |
+
|
| 3 |
+
This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules.
|
| 4 |
+
|
| 5 |
+
Currently, two official plugins are available:
|
| 6 |
+
|
| 7 |
+
- [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react) uses [Babel](https://babeljs.io/) (or [oxc](https://oxc.rs) when used in [rolldown-vite](https://vite.dev/guide/rolldown)) for Fast Refresh
|
| 8 |
+
- [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react-swc) uses [SWC](https://swc.rs/) for Fast Refresh
|
| 9 |
+
|
| 10 |
+
## React Compiler
|
| 11 |
+
|
| 12 |
+
The React Compiler is not enabled on this template because of its impact on dev & build performances. To add it, see [this documentation](https://react.dev/learn/react-compiler/installation).
|
| 13 |
+
|
| 14 |
+
## Expanding the ESLint configuration
|
| 15 |
+
|
| 16 |
+
If you are developing a production application, we recommend updating the configuration to enable type-aware lint rules:
|
| 17 |
+
|
| 18 |
+
```js
|
| 19 |
+
export default defineConfig([
|
| 20 |
+
globalIgnores(['dist']),
|
| 21 |
+
{
|
| 22 |
+
files: ['**/*.{ts,tsx}'],
|
| 23 |
+
extends: [
|
| 24 |
+
// Other configs...
|
| 25 |
+
|
| 26 |
+
// Remove tseslint.configs.recommended and replace with this
|
| 27 |
+
tseslint.configs.recommendedTypeChecked,
|
| 28 |
+
// Alternatively, use this for stricter rules
|
| 29 |
+
tseslint.configs.strictTypeChecked,
|
| 30 |
+
// Optionally, add this for stylistic rules
|
| 31 |
+
tseslint.configs.stylisticTypeChecked,
|
| 32 |
+
|
| 33 |
+
// Other configs...
|
| 34 |
+
],
|
| 35 |
+
languageOptions: {
|
| 36 |
+
parserOptions: {
|
| 37 |
+
project: ['./tsconfig.node.json', './tsconfig.app.json'],
|
| 38 |
+
tsconfigRootDir: import.meta.dirname,
|
| 39 |
+
},
|
| 40 |
+
// other options...
|
| 41 |
+
},
|
| 42 |
+
},
|
| 43 |
+
])
|
| 44 |
+
```
|
| 45 |
+
|
| 46 |
+
You can also install [eslint-plugin-react-x](https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-x) and [eslint-plugin-react-dom](https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-dom) for React-specific lint rules:
|
| 47 |
+
|
| 48 |
+
```js
|
| 49 |
+
// eslint.config.js
|
| 50 |
+
import reactX from 'eslint-plugin-react-x'
|
| 51 |
+
import reactDom from 'eslint-plugin-react-dom'
|
| 52 |
+
|
| 53 |
+
export default defineConfig([
|
| 54 |
+
globalIgnores(['dist']),
|
| 55 |
+
{
|
| 56 |
+
files: ['**/*.{ts,tsx}'],
|
| 57 |
+
extends: [
|
| 58 |
+
// Other configs...
|
| 59 |
+
// Enable lint rules for React
|
| 60 |
+
reactX.configs['recommended-typescript'],
|
| 61 |
+
// Enable lint rules for React DOM
|
| 62 |
+
reactDom.configs.recommended,
|
| 63 |
+
],
|
| 64 |
+
languageOptions: {
|
| 65 |
+
parserOptions: {
|
| 66 |
+
project: ['./tsconfig.node.json', './tsconfig.app.json'],
|
| 67 |
+
tsconfigRootDir: import.meta.dirname,
|
| 68 |
+
},
|
| 69 |
+
// other options...
|
| 70 |
+
},
|
| 71 |
+
},
|
| 72 |
+
])
|
| 73 |
+
```
|
frontends/react/eslint.config.js
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import js from '@eslint/js'
|
| 2 |
+
import globals from 'globals'
|
| 3 |
+
import reactHooks from 'eslint-plugin-react-hooks'
|
| 4 |
+
import reactRefresh from 'eslint-plugin-react-refresh'
|
| 5 |
+
import tseslint from 'typescript-eslint'
|
| 6 |
+
import { defineConfig, globalIgnores } from 'eslint/config'
|
| 7 |
+
|
| 8 |
+
export default defineConfig([
|
| 9 |
+
globalIgnores(['dist']),
|
| 10 |
+
{
|
| 11 |
+
files: ['**/*.{ts,tsx}'],
|
| 12 |
+
extends: [
|
| 13 |
+
js.configs.recommended,
|
| 14 |
+
tseslint.configs.recommended,
|
| 15 |
+
reactHooks.configs.flat.recommended,
|
| 16 |
+
reactRefresh.configs.vite,
|
| 17 |
+
],
|
| 18 |
+
languageOptions: {
|
| 19 |
+
ecmaVersion: 2020,
|
| 20 |
+
globals: globals.browser,
|
| 21 |
+
},
|
| 22 |
+
},
|
| 23 |
+
])
|
frontends/react/index.html
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<!doctype html>
|
| 2 |
+
<html lang="en">
|
| 3 |
+
<head>
|
| 4 |
+
<meta charset="UTF-8" />
|
| 5 |
+
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
| 6 |
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
| 7 |
+
<title>Convolutional Neural Network</title>
|
| 8 |
+
</head>
|
| 9 |
+
<body>
|
| 10 |
+
<div id="root"></div>
|
| 11 |
+
<script type="module" src="/src/main.tsx"></script>
|
| 12 |
+
</body>
|
| 13 |
+
</html>
|
frontends/react/package-lock.json
ADDED
|
The diff for this file is too large to render.
See raw diff
|
|
|
frontends/react/package.json
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"name": "react",
|
| 3 |
+
"private": true,
|
| 4 |
+
"version": "0.0.0",
|
| 5 |
+
"type": "module",
|
| 6 |
+
"scripts": {
|
| 7 |
+
"dev": "vite",
|
| 8 |
+
"build": "tsc -b && vite build",
|
| 9 |
+
"lint": "eslint .",
|
| 10 |
+
"preview": "vite preview"
|
| 11 |
+
},
|
| 12 |
+
"dependencies": {
|
| 13 |
+
"@tailwindcss/vite": "^4.1.18",
|
| 14 |
+
"comlink": "^4.4.2",
|
| 15 |
+
"react": "^19.2.0",
|
| 16 |
+
"react-dom": "^19.2.0",
|
| 17 |
+
"tailwindcss": "^4.1.18"
|
| 18 |
+
},
|
| 19 |
+
"devDependencies": {
|
| 20 |
+
"@eslint/js": "^9.39.1",
|
| 21 |
+
"@types/node": "^24.10.1",
|
| 22 |
+
"@types/react": "^19.2.5",
|
| 23 |
+
"@types/react-dom": "^19.2.3",
|
| 24 |
+
"@vitejs/plugin-react": "^5.1.1",
|
| 25 |
+
"eslint": "^9.39.1",
|
| 26 |
+
"eslint-plugin-react-hooks": "^7.0.1",
|
| 27 |
+
"eslint-plugin-react-refresh": "^0.4.24",
|
| 28 |
+
"globals": "^16.5.0",
|
| 29 |
+
"typescript": "~5.9.3",
|
| 30 |
+
"typescript-eslint": "^8.46.4",
|
| 31 |
+
"vite": "^7.2.4"
|
| 32 |
+
}
|
| 33 |
+
}
|
frontends/react/public/vite.svg
ADDED
|
|
frontends/react/src/App.tsx
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import NetworkViewer from './NetworkViewer.tsx';
|
| 2 |
+
import Sidebar from './Sidebar.tsx';
|
| 3 |
+
import LoadingScreen from './ui/LoadingScreen.tsx';
|
| 4 |
+
import useAppLogic from './useAppLogic.ts';
|
| 5 |
+
|
| 6 |
+
export default function App() {
|
| 7 |
+
|
| 8 |
+
const app = useAppLogic();
|
| 9 |
+
|
| 10 |
+
if (!app.backendReady) {
|
| 11 |
+
return <LoadingScreen message="Loading" />
|
| 12 |
+
}
|
| 13 |
+
|
| 14 |
+
return (
|
| 15 |
+
<div className="grid grid-cols-[2fr_1fr] h-dvh">
|
| 16 |
+
<NetworkViewer />
|
| 17 |
+
<Sidebar />
|
| 18 |
+
</div>
|
| 19 |
+
);
|
| 20 |
+
};
|
frontends/react/src/NetworkViewer.tsx
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
export default function NetworkViewer() {
|
| 2 |
+
return (
|
| 3 |
+
<div className="bg-red-100">
|
| 4 |
+
{/* Network viewer implementation goes here */}
|
| 5 |
+
</div>
|
| 6 |
+
);
|
| 7 |
+
}
|
frontends/react/src/PyodideBackend.ts
ADDED
|
@@ -0,0 +1,90 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
const PYODIDE_URL = "https://cdn.jsdelivr.net/pyodide/v0.26.1/full/pyodide.mjs";
|
| 2 |
+
|
| 3 |
+
import managerCode from "../../../backend/src/manager.py?raw";
|
| 4 |
+
// import logicCode from "../../../backend/src/logic.py?raw";
|
| 5 |
+
|
| 6 |
+
|
| 7 |
+
export class PyodideBackend {
|
| 8 |
+
private initialized: boolean = false;
|
| 9 |
+
private initPromise: Promise<void> | null = null;
|
| 10 |
+
|
| 11 |
+
private chain: Promise<void> = Promise.resolve();
|
| 12 |
+
|
| 13 |
+
private pyodide: any = null;
|
| 14 |
+
private manager: any = null;
|
| 15 |
+
|
| 16 |
+
async init(): Promise<void> {
|
| 17 |
+
if (this.initialized) {
|
| 18 |
+
return;
|
| 19 |
+
}
|
| 20 |
+
if (this.initPromise) {
|
| 21 |
+
return this.initPromise;
|
| 22 |
+
}
|
| 23 |
+
|
| 24 |
+
this.initPromise = (async () => {
|
| 25 |
+
const { loadPyodide } = await import(/* @vite-ignore */ PYODIDE_URL);
|
| 26 |
+
|
| 27 |
+
this.pyodide = await loadPyodide({
|
| 28 |
+
indexURL: "https://cdn.jsdelivr.net/pyodide/v0.26.1/full/"
|
| 29 |
+
});
|
| 30 |
+
|
| 31 |
+
await this.pyodide.loadPackage(["numpy", "micropip", "sqlite3"]);
|
| 32 |
+
await this.pyodide.runPythonAsync("import micropip; await micropip.install('tinygrad')");
|
| 33 |
+
|
| 34 |
+
this.pyodide.FS.writeFile("manager.py", managerCode);
|
| 35 |
+
this.pyodide.runPython(`from manager import Manager; manager = Manager();`);
|
| 36 |
+
this.manager = this.pyodide.globals.get("manager");
|
| 37 |
+
|
| 38 |
+
if (!this.manager) {
|
| 39 |
+
throw new Error("Failed to initialize pyodide manager");
|
| 40 |
+
}
|
| 41 |
+
|
| 42 |
+
console.log("Pyodide initialized");
|
| 43 |
+
this.initialized = true;
|
| 44 |
+
})();
|
| 45 |
+
|
| 46 |
+
return this.initPromise;
|
| 47 |
+
}
|
| 48 |
+
|
| 49 |
+
private enqueue<T>(fn: () => Promise<T>): Promise<T> {
|
| 50 |
+
const next = this.chain.then(() => fn(), () => fn());
|
| 51 |
+
this.chain = next.then(() => undefined, () => undefined);
|
| 52 |
+
return next;
|
| 53 |
+
}
|
| 54 |
+
|
| 55 |
+
private async handleCall<T>(methodName: string, args?: unknown): Promise<T> {
|
| 56 |
+
await this.init();
|
| 57 |
+
|
| 58 |
+
return this.enqueue(async () => {
|
| 59 |
+
let pyArgs: any = null;
|
| 60 |
+
let pyResult: any = null;
|
| 61 |
+
|
| 62 |
+
try {
|
| 63 |
+
if (args !== undefined) {
|
| 64 |
+
pyArgs = this.pyodide.toPy(args);
|
| 65 |
+
}
|
| 66 |
+
|
| 67 |
+
const fn = this.manager[methodName];
|
| 68 |
+
if (!fn) {
|
| 69 |
+
throw new Error(`Manager has no method named ${methodName}`);
|
| 70 |
+
}
|
| 71 |
+
|
| 72 |
+
pyResult = args === undefined ? fn.call(this.manager) : fn.call(this.manager, pyArgs);
|
| 73 |
+
|
| 74 |
+
const result = pyResult && typeof pyResult.toJs === "function"
|
| 75 |
+
? pyResult.toJs({ dict_converter: Object.fromEntries })
|
| 76 |
+
: pyResult;
|
| 77 |
+
|
| 78 |
+
return result as T;
|
| 79 |
+
|
| 80 |
+
} finally {
|
| 81 |
+
if (pyArgs && typeof pyArgs.destroy === "function") {
|
| 82 |
+
pyArgs.destroy();
|
| 83 |
+
}
|
| 84 |
+
if (pyResult && typeof pyResult.destroy === "function") {
|
| 85 |
+
pyResult.destroy();
|
| 86 |
+
}
|
| 87 |
+
}
|
| 88 |
+
});
|
| 89 |
+
}
|
| 90 |
+
};
|
frontends/react/src/Sidebar.tsx
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { useState } from "react";
|
| 2 |
+
import Radio from "./ui/Radio";
|
| 3 |
+
import Tabs from "./ui/Tabs";
|
| 4 |
+
import FileUploader from "./ui/FileUploader";
|
| 5 |
+
import FilesUploader from "./ui/FilesUploader";
|
| 6 |
+
|
| 7 |
+
export default function Sidebar() {
|
| 8 |
+
const tabs = ["Data", "Model", "Train"] as const;
|
| 9 |
+
const [activeTab, setActiveTab] = useState<(typeof tabs)[number]>('Data');
|
| 10 |
+
|
| 11 |
+
const dataOptions = ["Upload", "Presets"] as const;
|
| 12 |
+
const [activeDataOption, setActiveDataOption] = useState<(typeof dataOptions)[number]>('Upload');
|
| 13 |
+
|
| 14 |
+
return (
|
| 15 |
+
<div className="bg-white flex flex-col border-l border-gray-200 p-4 gap-4">
|
| 16 |
+
<Tabs tabs={tabs} activeTab={activeTab} onChange={setActiveTab} />
|
| 17 |
+
|
| 18 |
+
{ activeTab === 'Data' && (
|
| 19 |
+
<>
|
| 20 |
+
<Radio
|
| 21 |
+
label="Data Input"
|
| 22 |
+
options={dataOptions}
|
| 23 |
+
activeOption={activeDataOption}
|
| 24 |
+
onChange={setActiveDataOption}
|
| 25 |
+
/>
|
| 26 |
+
{ activeDataOption === 'Upload' && (
|
| 27 |
+
<>
|
| 28 |
+
<FilesUploader label="Upload imagefolder" onFilesUpload={() => {}}/>
|
| 29 |
+
</>
|
| 30 |
+
)}
|
| 31 |
+
</>
|
| 32 |
+
)}
|
| 33 |
+
|
| 34 |
+
</div>
|
| 35 |
+
);
|
| 36 |
+
}
|
frontends/react/src/assets/react.svg
ADDED
|
|
frontends/react/src/index.css
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
@import "tailwindcss";
|
frontends/react/src/main.tsx
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { StrictMode } from 'react'
|
| 2 |
+
import { createRoot } from 'react-dom/client'
|
| 3 |
+
import './index.css'
|
| 4 |
+
import App from './App.tsx'
|
| 5 |
+
|
| 6 |
+
createRoot(document.getElementById('root')!).render(
|
| 7 |
+
<StrictMode>
|
| 8 |
+
<App />
|
| 9 |
+
</StrictMode>,
|
| 10 |
+
)
|
frontends/react/src/pyodide.worker.ts
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import * as Comlink from "comlink";
|
| 2 |
+
import { PyodideBackend } from "./PyodideBackend";
|
| 3 |
+
|
| 4 |
+
const backend = new PyodideBackend();
|
| 5 |
+
|
| 6 |
+
Comlink.expose(backend);
|
frontends/react/src/ui/Button.tsx
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
interface ButtonProps {
|
| 2 |
+
label: string;
|
| 3 |
+
onClick?: () => void;
|
| 4 |
+
}
|
| 5 |
+
|
| 6 |
+
export default function Button({ label, onClick }: ButtonProps) {
|
| 7 |
+
return (
|
| 8 |
+
<button
|
| 9 |
+
onClick={onClick}
|
| 10 |
+
className="px-5 py-2 cursor-pointer bg-orange-200 rounded hover:bg-orange-300 border border-gray-300"
|
| 11 |
+
>
|
| 12 |
+
{label}
|
| 13 |
+
</button>
|
| 14 |
+
);
|
| 15 |
+
}
|
frontends/react/src/ui/Dropdown.tsx
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
interface DropdownProps<T extends string> {
|
| 2 |
+
label: string;
|
| 3 |
+
options: readonly T[];
|
| 4 |
+
activeOption: T;
|
| 5 |
+
onChange: (option: T) => void;
|
| 6 |
+
}
|
| 7 |
+
|
| 8 |
+
export default function Dropdown<T extends string>({ label, options, activeOption, onChange }: DropdownProps<T>) {
|
| 9 |
+
return (
|
| 10 |
+
<div className="flex flex-col gap-1">
|
| 11 |
+
<label className="text-gray-700 text-sm">{label}</label>
|
| 12 |
+
<select
|
| 13 |
+
value={activeOption}
|
| 14 |
+
onChange={(e) => onChange(e.target.value as T)}
|
| 15 |
+
className="p-2 rounded bg-white border border-gray-300"
|
| 16 |
+
>
|
| 17 |
+
{options.map((option) => (
|
| 18 |
+
<option key={option} value={option}>
|
| 19 |
+
{option}
|
| 20 |
+
</option>
|
| 21 |
+
))}
|
| 22 |
+
</select>
|
| 23 |
+
</div>
|
| 24 |
+
);
|
| 25 |
+
}
|
frontends/react/src/ui/FileUploader.tsx
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { useRef } from 'react';
|
| 2 |
+
import Button from './Button';
|
| 3 |
+
|
| 4 |
+
interface FileUploaderProps {
|
| 5 |
+
label: string;
|
| 6 |
+
onFileUpload: (file: File) => void;
|
| 7 |
+
}
|
| 8 |
+
|
| 9 |
+
export default function FileUploader({ label, onFileUpload }: FileUploaderProps) {
|
| 10 |
+
const fileInputRef = useRef<HTMLInputElement>(null);
|
| 11 |
+
|
| 12 |
+
const handleButtonClick = () => {
|
| 13 |
+
fileInputRef.current?.click();
|
| 14 |
+
};
|
| 15 |
+
|
| 16 |
+
const handleFileChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
| 17 |
+
const file = e.target.files?.[0];
|
| 18 |
+
if (file) {
|
| 19 |
+
onFileUpload(file);
|
| 20 |
+
}
|
| 21 |
+
};
|
| 22 |
+
|
| 23 |
+
return (
|
| 24 |
+
<>
|
| 25 |
+
<input
|
| 26 |
+
type="file"
|
| 27 |
+
ref={fileInputRef}
|
| 28 |
+
onChange={handleFileChange}
|
| 29 |
+
className="hidden"
|
| 30 |
+
accept=".csv"
|
| 31 |
+
/>
|
| 32 |
+
|
| 33 |
+
<Button label={label} onClick={handleButtonClick} />
|
| 34 |
+
</>
|
| 35 |
+
);
|
| 36 |
+
}
|
frontends/react/src/ui/FilesUploader.tsx
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { useRef, useEffect } from 'react';
|
| 2 |
+
import Button from './Button';
|
| 3 |
+
|
| 4 |
+
interface FilesUploaderProps {
|
| 5 |
+
label: string;
|
| 6 |
+
onFilesUpload: (files: File[]) => void;
|
| 7 |
+
}
|
| 8 |
+
|
| 9 |
+
export default function FilesUploader({ label, onFilesUpload }: FilesUploaderProps) {
|
| 10 |
+
const fileInputRef = useRef<HTMLInputElement>(null);
|
| 11 |
+
|
| 12 |
+
// enable folder picking
|
| 13 |
+
useEffect(() => {
|
| 14 |
+
const el = fileInputRef.current;
|
| 15 |
+
if (!el) return;
|
| 16 |
+
|
| 17 |
+
el.setAttribute("webkitdirectory", "");
|
| 18 |
+
el.setAttribute("directory", "");
|
| 19 |
+
}, []);
|
| 20 |
+
|
| 21 |
+
const handleButtonClick = () => {
|
| 22 |
+
fileInputRef.current?.click();
|
| 23 |
+
};
|
| 24 |
+
|
| 25 |
+
const handleFileChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
| 26 |
+
const files = e.target.files;
|
| 27 |
+
|
| 28 |
+
if (!files) {
|
| 29 |
+
return;
|
| 30 |
+
}
|
| 31 |
+
|
| 32 |
+
const fileList = Array.from(files);
|
| 33 |
+
onFilesUpload(fileList);
|
| 34 |
+
e.target.value = "";
|
| 35 |
+
};
|
| 36 |
+
|
| 37 |
+
return (
|
| 38 |
+
<>
|
| 39 |
+
<input
|
| 40 |
+
type="file"
|
| 41 |
+
ref={fileInputRef}
|
| 42 |
+
onChange={handleFileChange}
|
| 43 |
+
className="hidden"
|
| 44 |
+
multiple
|
| 45 |
+
/>
|
| 46 |
+
|
| 47 |
+
<Button label={label} onClick={handleButtonClick} />
|
| 48 |
+
</>
|
| 49 |
+
);
|
| 50 |
+
}
|
frontends/react/src/ui/InputField.tsx
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
interface InputFieldProps {
|
| 2 |
+
label: string;
|
| 3 |
+
value?: string;
|
| 4 |
+
onChange?: (newValue: string) => void;
|
| 5 |
+
}
|
| 6 |
+
|
| 7 |
+
export default function InputField({ label, value, onChange }: InputFieldProps) {
|
| 8 |
+
return (
|
| 9 |
+
<div className="flex flex-col gap-1">
|
| 10 |
+
<label className="text-gray-700 text-sm">{label}</label>
|
| 11 |
+
<input
|
| 12 |
+
type="text"
|
| 13 |
+
value={value}
|
| 14 |
+
onChange={(e) => onChange && onChange(e.target.value)}
|
| 15 |
+
className="p-2 rounded bg-white border border-gray-300"
|
| 16 |
+
/>
|
| 17 |
+
</div>
|
| 18 |
+
);
|
| 19 |
+
}
|
frontends/react/src/ui/LoadingScreen.tsx
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
export default function LoadingScreen({ message }: { message: string }) {
|
| 2 |
+
return (
|
| 3 |
+
<div className="fixed inset-0 flex flex-col items-center justify-center bg-slate-50 z-50">
|
| 4 |
+
<div className="flex flex-col items-center space-y-6">
|
| 5 |
+
{/* Animated loading spinner */}
|
| 6 |
+
<div className="relative flex items-center justify-center">
|
| 7 |
+
<div className="w-16 h-16 border-4 border-slate-200 border-t-blue-600 rounded-full animate-spin"></div>
|
| 8 |
+
</div>
|
| 9 |
+
|
| 10 |
+
{/* Loading message */}
|
| 11 |
+
<div className="text-center">
|
| 12 |
+
<h2 className="text-xl font-semibold text-slate-800 tracking-tight">
|
| 13 |
+
{message}...
|
| 14 |
+
</h2>
|
| 15 |
+
</div>
|
| 16 |
+
</div>
|
| 17 |
+
</div>
|
| 18 |
+
);
|
| 19 |
+
}
|
frontends/react/src/ui/Radio.tsx
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
interface RadioProps<T extends string> {
|
| 2 |
+
label?: string;
|
| 3 |
+
options: readonly T[];
|
| 4 |
+
activeOption: T;
|
| 5 |
+
onChange: (option: T) => void;
|
| 6 |
+
}
|
| 7 |
+
|
| 8 |
+
export default function Radio<T extends string>({ label, options, activeOption, onChange }: RadioProps<T>) {
|
| 9 |
+
return (
|
| 10 |
+
<div className="flex flex-col gap-1">
|
| 11 |
+
{label && <label className="text-gray-700 text-sm">{label}</label>}
|
| 12 |
+
<div className="flex gap-4">
|
| 13 |
+
{options.map((option) => (
|
| 14 |
+
<label key={option} className="flex items-center gap-1">
|
| 15 |
+
<input
|
| 16 |
+
type="radio"
|
| 17 |
+
value={option}
|
| 18 |
+
checked={activeOption === option}
|
| 19 |
+
onChange={() => onChange(option)}
|
| 20 |
+
/>
|
| 21 |
+
{option}
|
| 22 |
+
</label>
|
| 23 |
+
))}
|
| 24 |
+
</div>
|
| 25 |
+
</div>
|
| 26 |
+
);
|
| 27 |
+
}
|
frontends/react/src/ui/Tabs.tsx
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
interface TabsProps<T extends string> {
|
| 2 |
+
tabs: readonly T[];
|
| 3 |
+
activeTab: T;
|
| 4 |
+
onChange: (tab: T) => void;
|
| 5 |
+
}
|
| 6 |
+
|
| 7 |
+
export default function Tabs<T extends string>({ tabs, activeTab, onChange }: TabsProps<T>) {
|
| 8 |
+
return (
|
| 9 |
+
<div className="flex mb-4">
|
| 10 |
+
{tabs.map((tab) => (
|
| 11 |
+
<button
|
| 12 |
+
key={tab}
|
| 13 |
+
onClick={() => onChange(tab)}
|
| 14 |
+
className={`px-5 py-1 cursor-pointer ${
|
| 15 |
+
activeTab === tab
|
| 16 |
+
? "text-orange-400 border-b-2 border-orange-400"
|
| 17 |
+
: "text-gray-950 hover:bg-gray-200"
|
| 18 |
+
}`}
|
| 19 |
+
>
|
| 20 |
+
{tab}
|
| 21 |
+
</button>
|
| 22 |
+
))}
|
| 23 |
+
</div>
|
| 24 |
+
);
|
| 25 |
+
}
|
frontends/react/src/useAppLogic.ts
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import usePyodideBackend from "./usePyodideBackend.ts";
|
| 2 |
+
|
| 3 |
+
export default function useAppLogic() {
|
| 4 |
+
const { getBackend, backendReady } = usePyodideBackend();
|
| 5 |
+
|
| 6 |
+
return {
|
| 7 |
+
getBackend,
|
| 8 |
+
backendReady,
|
| 9 |
+
};
|
| 10 |
+
}
|
frontends/react/src/usePyodideBackend.ts
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { useCallback, useEffect, useRef, useState } from "react";
|
| 2 |
+
import * as Comlink from "comlink";
|
| 3 |
+
|
| 4 |
+
import type { PyodideBackend } from "./PyodideBackend.ts";
|
| 5 |
+
|
| 6 |
+
export default function usePyodideBackend() {
|
| 7 |
+
const backendRef = useRef<Comlink.Remote<PyodideBackend> | null>(null);
|
| 8 |
+
const workerRef = useRef<Worker | null>(null);
|
| 9 |
+
const [isReady, setIsReady] = useState<boolean>(false);
|
| 10 |
+
|
| 11 |
+
useEffect(() => {
|
| 12 |
+
const worker = new Worker(
|
| 13 |
+
new URL("./pyodide.worker.ts", import.meta.url),
|
| 14 |
+
{ type: "module" }
|
| 15 |
+
);
|
| 16 |
+
|
| 17 |
+
const backend = Comlink.wrap<PyodideBackend>(worker);
|
| 18 |
+
|
| 19 |
+
backendRef.current = backend;
|
| 20 |
+
workerRef.current = worker;
|
| 21 |
+
|
| 22 |
+
(async () => {
|
| 23 |
+
try {
|
| 24 |
+
await backend.init();
|
| 25 |
+
console.log("Pyodide intialized")
|
| 26 |
+
setIsReady(true);
|
| 27 |
+
} catch (error) {
|
| 28 |
+
console.error("Failed to initialize Pyodide backend:", error);
|
| 29 |
+
}
|
| 30 |
+
})();
|
| 31 |
+
|
| 32 |
+
return () => {
|
| 33 |
+
if (backendRef.current) {
|
| 34 |
+
backendRef.current[Comlink.releaseProxy]();
|
| 35 |
+
backendRef.current = null;
|
| 36 |
+
}
|
| 37 |
+
|
| 38 |
+
workerRef.current?.terminate();
|
| 39 |
+
workerRef.current = null;
|
| 40 |
+
|
| 41 |
+
setIsReady(false);
|
| 42 |
+
};
|
| 43 |
+
}, []);
|
| 44 |
+
|
| 45 |
+
const getBackend = useCallback((): Comlink.Remote<PyodideBackend> => {
|
| 46 |
+
if (!backendRef.current) {
|
| 47 |
+
throw new Error("Pyodide backend is not ready");
|
| 48 |
+
}
|
| 49 |
+
return backendRef.current;
|
| 50 |
+
}, []);
|
| 51 |
+
|
| 52 |
+
return { getBackend, backendReady: isReady };
|
| 53 |
+
}
|
frontends/react/tsconfig.app.json
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"compilerOptions": {
|
| 3 |
+
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
|
| 4 |
+
"target": "ES2022",
|
| 5 |
+
"useDefineForClassFields": true,
|
| 6 |
+
"lib": ["ES2022", "DOM", "DOM.Iterable"],
|
| 7 |
+
"module": "ESNext",
|
| 8 |
+
"types": ["vite/client"],
|
| 9 |
+
"skipLibCheck": true,
|
| 10 |
+
|
| 11 |
+
/* Bundler mode */
|
| 12 |
+
"moduleResolution": "bundler",
|
| 13 |
+
"allowImportingTsExtensions": true,
|
| 14 |
+
"verbatimModuleSyntax": true,
|
| 15 |
+
"moduleDetection": "force",
|
| 16 |
+
"noEmit": true,
|
| 17 |
+
"jsx": "react-jsx",
|
| 18 |
+
|
| 19 |
+
/* Linting */
|
| 20 |
+
"strict": true,
|
| 21 |
+
"noUnusedLocals": true,
|
| 22 |
+
"noUnusedParameters": true,
|
| 23 |
+
"erasableSyntaxOnly": true,
|
| 24 |
+
"noFallthroughCasesInSwitch": true,
|
| 25 |
+
"noUncheckedSideEffectImports": true
|
| 26 |
+
},
|
| 27 |
+
"include": ["src"]
|
| 28 |
+
}
|
frontends/react/tsconfig.json
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"files": [],
|
| 3 |
+
"references": [
|
| 4 |
+
{ "path": "./tsconfig.app.json" },
|
| 5 |
+
{ "path": "./tsconfig.node.json" }
|
| 6 |
+
]
|
| 7 |
+
}
|
frontends/react/tsconfig.node.json
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"compilerOptions": {
|
| 3 |
+
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo",
|
| 4 |
+
"target": "ES2023",
|
| 5 |
+
"lib": ["ES2023"],
|
| 6 |
+
"module": "ESNext",
|
| 7 |
+
"types": ["node"],
|
| 8 |
+
"skipLibCheck": true,
|
| 9 |
+
|
| 10 |
+
/* Bundler mode */
|
| 11 |
+
"moduleResolution": "bundler",
|
| 12 |
+
"allowImportingTsExtensions": true,
|
| 13 |
+
"verbatimModuleSyntax": true,
|
| 14 |
+
"moduleDetection": "force",
|
| 15 |
+
"noEmit": true,
|
| 16 |
+
|
| 17 |
+
/* Linting */
|
| 18 |
+
"strict": true,
|
| 19 |
+
"noUnusedLocals": true,
|
| 20 |
+
"noUnusedParameters": true,
|
| 21 |
+
"erasableSyntaxOnly": true,
|
| 22 |
+
"noFallthroughCasesInSwitch": true,
|
| 23 |
+
"noUncheckedSideEffectImports": true
|
| 24 |
+
},
|
| 25 |
+
"include": ["vite.config.ts"]
|
| 26 |
+
}
|
frontends/react/vite.config.ts
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { defineConfig } from 'vite'
|
| 2 |
+
import react from '@vitejs/plugin-react'
|
| 3 |
+
import tailwindcss from '@tailwindcss/vite'
|
| 4 |
+
|
| 5 |
+
// https://vite.dev/config/
|
| 6 |
+
export default defineConfig({
|
| 7 |
+
plugins: [
|
| 8 |
+
react(),
|
| 9 |
+
tailwindcss(),
|
| 10 |
+
],
|
| 11 |
+
})
|