Joel Woodfield commited on
Commit
de03c4e
·
1 Parent(s): 1229c63

Initial commit

Browse files
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
+ })