diff --git a/mofa-frontend/.gitignore b/mofa-frontend/.gitignore
new file mode 100644
index 0000000..fd7d79d
--- /dev/null
+++ b/mofa-frontend/.gitignore
@@ -0,0 +1,5 @@
+node_modules/
+dist/
+.env
+.DS_Store
+*.log
diff --git a/mofa-frontend/README.md b/mofa-frontend/README.md
new file mode 100644
index 0000000..d2bdace
--- /dev/null
+++ b/mofa-frontend/README.md
@@ -0,0 +1,37 @@
+# MoFA FM
+
+MoFA FM is a local AI podcast studio. It turns any article into a high-quality podcast script and synthesizes it into natural-sounding audio — running entirely on your machine. No cloud, no API keys, full privacy.
+
+## Features
+
+- **Offline-First**: Operates seamlessly against a local inference engine.
+- **Visual Intelligence**: Real-time observability into the model's inner workings via the Theater view and Engine Monitor.
+- **Design Excellence**: Highly polished interface with considered motion, sound design, and typography.
+- **Robust Pipeline**: Hardened against intermittent network failures with intelligent retries.
+- **Accessibility**: Full keyboard and screen reader support, honoring reduced-motion preferences.
+
+## Quick Start
+
+\`\`\`bash
+npm install
+npm run dev
+\`\`\`
+
+## Architecture
+
+- **React + Vite**: Fast, modern frontend build.
+- **Tailwind CSS**: Utility-first styling tied to a strict design token system.
+- **Framer Motion**: Smooth, intentional animations and state transitions.
+
+### Mock vs. Real Engine
+
+MoFA FM is designed to run against the **MoFA Core Engine** (at `127.0.0.1:8420`).
+However, for development and demonstration without the heavy backend, the app can run against a simulated local mock engine.
+
+To use the mock engine, create a `.env` file or set the environment variable:
+\`\`\`env
+VITE_USE_MOCK=true
+VITE_ENGINE_URL=http://localhost:8420
+\`\`\`
+
+If `VITE_USE_MOCK=false`, the application will make real HTTP and SSE connections to your local Engine.
diff --git a/mofa-frontend/eslint.config.js b/mofa-frontend/eslint.config.js
new file mode 100644
index 0000000..bdcc475
--- /dev/null
+++ b/mofa-frontend/eslint.config.js
@@ -0,0 +1,27 @@
+import js from '@eslint/js';
+import globals from 'globals';
+import reactHooks from 'eslint-plugin-react-hooks';
+import reactRefresh from 'eslint-plugin-react-refresh';
+import tseslint from 'typescript-eslint';
+
+export default tseslint.config({
+ extends: [js.configs.recommended, ...tseslint.configs.recommended],
+ files: ['**/*.{ts,tsx}'],
+ languageOptions: {
+ ecmaVersion: 2020,
+ globals: globals.browser,
+ },
+ plugins: {
+ 'react-hooks': reactHooks,
+ 'react-refresh': reactRefresh,
+ },
+ rules: {
+ ...reactHooks.configs.recommended.rules,
+ 'react-refresh/only-export-components': [
+ 'warn',
+ { allowConstantExport: true },
+ ],
+ '@typescript-eslint/no-explicit-any': 'off',
+ '@typescript-eslint/no-unused-vars': ['warn', { argsIgnorePattern: '^_', varsIgnorePattern: '^_' }]
+ },
+});
diff --git a/mofa-frontend/index.html b/mofa-frontend/index.html
new file mode 100644
index 0000000..916e967
--- /dev/null
+++ b/mofa-frontend/index.html
@@ -0,0 +1,15 @@
+
+
+
+
+
+
+
+ MoFA Engine
+
+
+
+
+
+
+
diff --git a/mofa-frontend/package-lock.json b/mofa-frontend/package-lock.json
new file mode 100644
index 0000000..fe26f84
--- /dev/null
+++ b/mofa-frontend/package-lock.json
@@ -0,0 +1,3217 @@
+{
+ "name": "applet",
+ "version": "1.0.0",
+ "lockfileVersion": 3,
+ "requires": true,
+ "packages": {
+ "": {
+ "name": "applet",
+ "version": "1.0.0",
+ "license": "ISC",
+ "dependencies": {
+ "@tanstack/react-query": "^5.101.2",
+ "clsx": "^2.1.1",
+ "framer-motion": "^12.42.2",
+ "lucide-react": "^1.23.0",
+ "react": "^19.2.7",
+ "react-dom": "^19.2.7",
+ "tailwind-merge": "^3.6.0",
+ "wavesurfer.js": "^7.12.10"
+ },
+ "devDependencies": {
+ "@eslint/js": "^10.0.1",
+ "@tailwindcss/postcss": "^4.3.2",
+ "@types/react": "^19.2.17",
+ "@types/react-dom": "^19.2.3",
+ "@typescript-eslint/eslint-plugin": "^8.63.0",
+ "@typescript-eslint/parser": "^8.63.0",
+ "@vitejs/plugin-react": "^6.0.3",
+ "eslint": "^10.6.0",
+ "eslint-plugin-react-hooks": "^7.1.1",
+ "eslint-plugin-react-refresh": "^0.5.3",
+ "globals": "^17.7.0",
+ "postcss": "^8.5.16",
+ "tailwindcss": "^4.3.2",
+ "typescript": "^6.0.3",
+ "typescript-eslint": "^8.63.0",
+ "vite": "^8.1.4"
+ }
+ },
+ "node_modules/@alloc/quick-lru": {
+ "version": "5.2.0",
+ "resolved": "https://registry.npmjs.org/@alloc/quick-lru/-/quick-lru-5.2.0.tgz",
+ "integrity": "sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/@babel/code-frame": {
+ "version": "7.29.7",
+ "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.7.tgz",
+ "integrity": "sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-validator-identifier": "^7.29.7",
+ "js-tokens": "^4.0.0",
+ "picocolors": "^1.1.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/compat-data": {
+ "version": "7.29.7",
+ "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.29.7.tgz",
+ "integrity": "sha512-locTkQyKvwIEgBzVrn8693ebc97F2U8ZHjbXwDXJ5Fn2TCpNwTlKcaKLkdHop5c/icOFE7qt7Q9JC5hnKNa6Gg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/core": {
+ "version": "7.29.7",
+ "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.7.tgz",
+ "integrity": "sha512-RgHBCvtjbOK2gXSNBNIkNoEc9qoVEtau3hj8gEqKQuL3HZAibKarWFEI3Lfm6EYKkLalOh8eSrj9b+ch9H/VBA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/code-frame": "^7.29.7",
+ "@babel/generator": "^7.29.7",
+ "@babel/helper-compilation-targets": "^7.29.7",
+ "@babel/helper-module-transforms": "^7.29.7",
+ "@babel/helpers": "^7.29.7",
+ "@babel/parser": "^7.29.7",
+ "@babel/template": "^7.29.7",
+ "@babel/traverse": "^7.29.7",
+ "@babel/types": "^7.29.7",
+ "@jridgewell/remapping": "^2.3.5",
+ "convert-source-map": "^2.0.0",
+ "debug": "^4.1.0",
+ "gensync": "^1.0.0-beta.2",
+ "json5": "^2.2.3",
+ "semver": "^6.3.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/babel"
+ }
+ },
+ "node_modules/@babel/core/node_modules/semver": {
+ "version": "6.3.1",
+ "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz",
+ "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==",
+ "dev": true,
+ "license": "ISC",
+ "bin": {
+ "semver": "bin/semver.js"
+ }
+ },
+ "node_modules/@babel/generator": {
+ "version": "7.29.7",
+ "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.7.tgz",
+ "integrity": "sha512-DkXD5OJQaAQIdZ1bt3UZdEnHAn9Imd3IVBdX03UFe+ony9Ojw5pzr9YVKGDY1jt+Gcn/FnGkNf8r+Vj5NOJWtQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/parser": "^7.29.7",
+ "@babel/types": "^7.29.7",
+ "@jridgewell/gen-mapping": "^0.3.12",
+ "@jridgewell/trace-mapping": "^0.3.28",
+ "jsesc": "^3.0.2"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-compilation-targets": {
+ "version": "7.29.7",
+ "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.29.7.tgz",
+ "integrity": "sha512-wem6WaBj4NaVYVdNhLPPVacES6ZJ+KBBfSkTMD3YZxbP3rm3Di85tJU5ljaUNhaOynt+Aj0xruhYuzQBt8n71g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/compat-data": "^7.29.7",
+ "@babel/helper-validator-option": "^7.29.7",
+ "browserslist": "^4.24.0",
+ "lru-cache": "^5.1.1",
+ "semver": "^6.3.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-compilation-targets/node_modules/semver": {
+ "version": "6.3.1",
+ "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz",
+ "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==",
+ "dev": true,
+ "license": "ISC",
+ "bin": {
+ "semver": "bin/semver.js"
+ }
+ },
+ "node_modules/@babel/helper-globals": {
+ "version": "7.29.7",
+ "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.29.7.tgz",
+ "integrity": "sha512-3nQVUAtvkKH9zahfWgw96Jc/uFOmjACE1kQz82E2lqWmHBgjzbNlsC22nuQTfahmWeQtTq5nQ/4Nnd2A1wj4zA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-module-imports": {
+ "version": "7.29.7",
+ "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.29.7.tgz",
+ "integrity": "sha512-ejHwrQQYcm9xnTivShn2IDOlIzInN34AXskvq9QicvCtEzq1Vzclu/tKF8Jq1Cg8JG2GL6/EmjgsCT7lXepE3g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/traverse": "^7.29.7",
+ "@babel/types": "^7.29.7"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-module-transforms": {
+ "version": "7.29.7",
+ "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.29.7.tgz",
+ "integrity": "sha512-UPUVSyXbOh627KiCIGQSgwWzGeBKLkaJ9PJEdrngIwMSzxLR4jS4+f1f1jb7VzBbg8nFLaYotvVPFCTqdrmTAg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-module-imports": "^7.29.7",
+ "@babel/helper-validator-identifier": "^7.29.7",
+ "@babel/traverse": "^7.29.7"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0"
+ }
+ },
+ "node_modules/@babel/helper-string-parser": {
+ "version": "7.29.7",
+ "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.29.7.tgz",
+ "integrity": "sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-validator-identifier": {
+ "version": "7.29.7",
+ "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.29.7.tgz",
+ "integrity": "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-validator-option": {
+ "version": "7.29.7",
+ "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.29.7.tgz",
+ "integrity": "sha512-N9ZErrD+yW5geCDtBqnOoxmR8+tNKiGuxKlDpuJxfsqpa2dFcexaziGAE/qoHLiDDreVNMupxGmSoNlyvsA3gw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helpers": {
+ "version": "7.29.7",
+ "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.29.7.tgz",
+ "integrity": "sha512-1k2lAGRMfHTcwuNYcCNUmaUffmQv8KWMfh2iJUUeRlwlwH4FdNG7mfPI10NPfLHJFThE4Tyr4mv7kTNZOiPuBg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/template": "^7.29.7",
+ "@babel/types": "^7.29.7"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/parser": {
+ "version": "7.29.7",
+ "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.7.tgz",
+ "integrity": "sha512-hnORnjP/1P/zFEndoeX+n+t1RwWRJiJpM/jO7FW32Kn9r5+sJB2JWOdYo4L6k78j15eCwY3Gm/7364B1EMwtNg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/types": "^7.29.7"
+ },
+ "bin": {
+ "parser": "bin/babel-parser.js"
+ },
+ "engines": {
+ "node": ">=6.0.0"
+ }
+ },
+ "node_modules/@babel/template": {
+ "version": "7.29.7",
+ "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.29.7.tgz",
+ "integrity": "sha512-puq+Gf35oI24FeN11LkoUQFqv9uwNeWpxXZi/Ji3rRIoKAzKnxRaZ+Gkj0vKS9ZCiTESfng1N9LyOyXvo+m+Gg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/code-frame": "^7.29.7",
+ "@babel/parser": "^7.29.7",
+ "@babel/types": "^7.29.7"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/traverse": {
+ "version": "7.29.7",
+ "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.7.tgz",
+ "integrity": "sha512-EhlfNQtZ+NK22w5BM61ciuiq1m58ed33Wr1Xan//ZRTy6hgjnwyCffRYwzsGXdASJSUJ1guZILsErh1eQcl+zw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/code-frame": "^7.29.7",
+ "@babel/generator": "^7.29.7",
+ "@babel/helper-globals": "^7.29.7",
+ "@babel/parser": "^7.29.7",
+ "@babel/template": "^7.29.7",
+ "@babel/types": "^7.29.7",
+ "debug": "^4.3.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/types": {
+ "version": "7.29.7",
+ "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.7.tgz",
+ "integrity": "sha512-4zBIxpPzowiZpusoFkyGVwakdRJUyuH5PxQ/PrqghfdFWWasvnCdPfQXHrenDai+gyLARulZjZowCOj6fjT4pA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-string-parser": "^7.29.7",
+ "@babel/helper-validator-identifier": "^7.29.7"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@emnapi/core": {
+ "version": "1.11.1",
+ "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.11.1.tgz",
+ "integrity": "sha512-RSvbQmHzdKzNsLYa/wHrbc3KN4sYLKAdPZxqiM2HATqv/SBk2/ENSHpvXGaLOMcsAyz0poEGqkmmKYG3OWiJEQ==",
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "dependencies": {
+ "@emnapi/wasi-threads": "1.2.2",
+ "tslib": "^2.4.0"
+ }
+ },
+ "node_modules/@emnapi/runtime": {
+ "version": "1.11.1",
+ "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.11.1.tgz",
+ "integrity": "sha512-vgj7R3y3Wgx24IQaGPA/R6YFXLHVMOZ0uVEyIQPaWs+rd1AzfEMXlAC22FYwO1XkKR6NPsq7mUandH8oIRdZFw==",
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "dependencies": {
+ "tslib": "^2.4.0"
+ }
+ },
+ "node_modules/@emnapi/wasi-threads": {
+ "version": "1.2.2",
+ "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.2.tgz",
+ "integrity": "sha512-c95qOXkHdydNKhscBTebqEC1CVAZpyqOfVfBzQ1qgzyl3gfeldUjIggDbIZgDKsHLgnsM+igH7TJ/eAasaVuMA==",
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "dependencies": {
+ "tslib": "^2.4.0"
+ }
+ },
+ "node_modules/@eslint-community/eslint-utils": {
+ "version": "4.9.1",
+ "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.1.tgz",
+ "integrity": "sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "eslint-visitor-keys": "^3.4.3"
+ },
+ "engines": {
+ "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/eslint"
+ },
+ "peerDependencies": {
+ "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0"
+ }
+ },
+ "node_modules/@eslint-community/regexpp": {
+ "version": "4.12.2",
+ "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.2.tgz",
+ "integrity": "sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": "^12.0.0 || ^14.0.0 || >=16.0.0"
+ }
+ },
+ "node_modules/@eslint/config-array": {
+ "version": "0.23.5",
+ "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.23.5.tgz",
+ "integrity": "sha512-Y3kKLvC1dvTOT+oGlqNQ1XLqK6D1HU2YXPc52NmAlJZbMMWDzGYXMiPRJ8TYD39muD/OTjlZmNJ4ib7dvSrMBA==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@eslint/object-schema": "^3.0.5",
+ "debug": "^4.3.1",
+ "minimatch": "^10.2.4"
+ },
+ "engines": {
+ "node": "^20.19.0 || ^22.13.0 || >=24"
+ }
+ },
+ "node_modules/@eslint/config-helpers": {
+ "version": "0.6.0",
+ "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.6.0.tgz",
+ "integrity": "sha512-ii6Bw9jJ2zi2cWA2Z+9/QZ/+3DX6kwaV5Q986D/CdP3Lap3w/pgQZ373FV7byY/i7L4IRH/G43I5dz1ClsCbpA==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@eslint/core": "^1.2.1"
+ },
+ "engines": {
+ "node": "^20.19.0 || ^22.13.0 || >=24"
+ }
+ },
+ "node_modules/@eslint/core": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/@eslint/core/-/core-1.2.1.tgz",
+ "integrity": "sha512-MwcE1P+AZ4C6DWlpin/OmOA54mmIZ/+xZuJiQd4SyB29oAJjN30UW9wkKNptW2ctp4cEsvhlLY/CsQ1uoHDloQ==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@types/json-schema": "^7.0.15"
+ },
+ "engines": {
+ "node": "^20.19.0 || ^22.13.0 || >=24"
+ }
+ },
+ "node_modules/@eslint/js": {
+ "version": "10.0.1",
+ "resolved": "https://registry.npmjs.org/@eslint/js/-/js-10.0.1.tgz",
+ "integrity": "sha512-zeR9k5pd4gxjZ0abRoIaxdc7I3nDktoXZk2qOv9gCNWx3mVwEn32VRhyLaRsDiJjTs0xq/T8mfPtyuXu7GWBcA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": "^20.19.0 || ^22.13.0 || >=24"
+ },
+ "funding": {
+ "url": "https://eslint.org/donate"
+ },
+ "peerDependencies": {
+ "eslint": "^10.0.0"
+ },
+ "peerDependenciesMeta": {
+ "eslint": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@eslint/object-schema": {
+ "version": "3.0.5",
+ "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-3.0.5.tgz",
+ "integrity": "sha512-vqTaUEgxzm+YDSdElad6PiRoX4t8VGDjCtt05zn4nU810UIx/uNEV7/lZJ6KwFThKZOzOxzXy48da+No7HZaMw==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "engines": {
+ "node": "^20.19.0 || ^22.13.0 || >=24"
+ }
+ },
+ "node_modules/@eslint/plugin-kit": {
+ "version": "0.7.2",
+ "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.7.2.tgz",
+ "integrity": "sha512-+CNAzxglkrpNf/kKywqQfk74QjtceuOE7Qm+AF8miRvPF/wmmK5+OJOgVh3AVTT3RP2mH3+FOaxlE5v72owk0A==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@eslint/core": "^1.2.1",
+ "levn": "^0.4.1"
+ },
+ "engines": {
+ "node": "^20.19.0 || ^22.13.0 || >=24"
+ }
+ },
+ "node_modules/@humanfs/core": {
+ "version": "0.19.2",
+ "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.2.tgz",
+ "integrity": "sha512-UhXNm+CFMWcbChXywFwkmhqjs3PRCmcSa/hfBgLIb7oQ5HNb1wS0icWsGtSAUNgefHeI+eBrA8I1fxmbHsGdvA==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@humanfs/types": "^0.15.0"
+ },
+ "engines": {
+ "node": ">=18.18.0"
+ }
+ },
+ "node_modules/@humanfs/node": {
+ "version": "0.16.8",
+ "resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.8.tgz",
+ "integrity": "sha512-gE1eQNZ3R++kTzFUpdGlpmy8kDZD/MLyHqDwqjkVQI0JMdI1D51sy1H958PNXYkM2rAac7e5/CnIKZrHtPh3BQ==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@humanfs/core": "^0.19.2",
+ "@humanfs/types": "^0.15.0",
+ "@humanwhocodes/retry": "^0.4.0"
+ },
+ "engines": {
+ "node": ">=18.18.0"
+ }
+ },
+ "node_modules/@humanfs/types": {
+ "version": "0.15.0",
+ "resolved": "https://registry.npmjs.org/@humanfs/types/-/types-0.15.0.tgz",
+ "integrity": "sha512-ZZ1w0aoQkwuUuC7Yf+7sdeaNfqQiiLcSRbfI08oAxqLtpXQr9AIVX7Ay7HLDuiLYAaFPu8oBYNq/QIi9URHJ3Q==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "engines": {
+ "node": ">=18.18.0"
+ }
+ },
+ "node_modules/@humanwhocodes/module-importer": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz",
+ "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "engines": {
+ "node": ">=12.22"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/nzakas"
+ }
+ },
+ "node_modules/@humanwhocodes/retry": {
+ "version": "0.4.3",
+ "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.3.tgz",
+ "integrity": "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "engines": {
+ "node": ">=18.18"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/nzakas"
+ }
+ },
+ "node_modules/@jridgewell/gen-mapping": {
+ "version": "0.3.13",
+ "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz",
+ "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@jridgewell/sourcemap-codec": "^1.5.0",
+ "@jridgewell/trace-mapping": "^0.3.24"
+ }
+ },
+ "node_modules/@jridgewell/remapping": {
+ "version": "2.3.5",
+ "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz",
+ "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@jridgewell/gen-mapping": "^0.3.5",
+ "@jridgewell/trace-mapping": "^0.3.24"
+ }
+ },
+ "node_modules/@jridgewell/resolve-uri": {
+ "version": "3.1.2",
+ "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz",
+ "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.0.0"
+ }
+ },
+ "node_modules/@jridgewell/sourcemap-codec": {
+ "version": "1.5.5",
+ "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz",
+ "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@jridgewell/trace-mapping": {
+ "version": "0.3.31",
+ "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz",
+ "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@jridgewell/resolve-uri": "^3.1.0",
+ "@jridgewell/sourcemap-codec": "^1.4.14"
+ }
+ },
+ "node_modules/@napi-rs/wasm-runtime": {
+ "version": "1.1.6",
+ "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.6.tgz",
+ "integrity": "sha512-ZLv/JdUfkvOy9eCnnBaGfiO+XimbjebAeO+MRQqD/B+FR1tnRN0tpKSJHRbE8sFfS6aqsXZ67TQjfwfsxULVbg==",
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "dependencies": {
+ "@tybys/wasm-util": "^0.10.3"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/Brooooooklyn"
+ },
+ "peerDependencies": {
+ "@emnapi/core": "^1.7.1",
+ "@emnapi/runtime": "^1.7.1"
+ }
+ },
+ "node_modules/@oxc-project/types": {
+ "version": "0.139.0",
+ "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.139.0.tgz",
+ "integrity": "sha512-r9gHphtCs+1M7J0pw6Sn/hh/Wpa/iQrOOkrNAlVLF/gHq+/CJmHIWKKUUhdWjcD6CIa8idarspCsASiXCXvFUw==",
+ "dev": true,
+ "license": "MIT",
+ "funding": {
+ "url": "https://github.com/sponsors/Boshen"
+ }
+ },
+ "node_modules/@rolldown/binding-android-arm64": {
+ "version": "1.1.5",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.1.5.tgz",
+ "integrity": "sha512-lZg8fqIv2v7FF237bwMgzGZEJvGL79/s5knJ/i6FmsGF4XXlzccZ4jb+TrFIxtSSxFtIpdsgrPZeMk1I9AFcyQ==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@rolldown/binding-darwin-arm64": {
+ "version": "1.1.5",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.1.5.tgz",
+ "integrity": "sha512-51Bnx9pNiMRKSUNtBfySkNJ9vMU9Hh3I1ozDd6gyPPYzaXCfnptUcEZxXGYFn+ul2dtcMUiqGR1Yai2K10uoTw==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@rolldown/binding-darwin-x64": {
+ "version": "1.1.5",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.1.5.tgz",
+ "integrity": "sha512-Tm+gbfC0aHu1tBA/JvKQh32S0K6YgCHkiAF4/W6xX0K0RmNuc94VeK419dJoE65R5aRxmo+noZQSWrAMF6yb6g==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@rolldown/binding-freebsd-x64": {
+ "version": "1.1.5",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.1.5.tgz",
+ "integrity": "sha512-JMzDKCCXq93YccG5gz3hvOs1oXRKAf0XYpfOS88e+wZrC8Iugj6j68867vrYZkvpDDpKn/KoKORThmchMpF6TA==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@rolldown/binding-linux-arm-gnueabihf": {
+ "version": "1.1.5",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.1.5.tgz",
+ "integrity": "sha512-uML21j2K5TfPGutKxub+M+nLjZIrWjXQ5Grx4lCe/nimTj9B4L63zHpjXLl4y0L3mcm2htEQIb06oCG/szerNw==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@rolldown/binding-linux-arm64-gnu": {
+ "version": "1.1.5",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.1.5.tgz",
+ "integrity": "sha512-navSiuTMogvnQoZoM/v+l3ZWo50/NTwSHSzheABx/RCnmUPaKwq9qSo4Br2OYRs21+Fz8uFqITZM3H4opOB0/Q==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@rolldown/binding-linux-arm64-musl": {
+ "version": "1.1.5",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.1.5.tgz",
+ "integrity": "sha512-lAryqH7IteztmCXQXk0etKj4wBQ7Gx5S6LjKhsgp9zb8I5bsuvU/2llH1hDQcjsFeqIsovMVN339/8pUDDBXxA==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@rolldown/binding-linux-ppc64-gnu": {
+ "version": "1.1.5",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.1.5.tgz",
+ "integrity": "sha512-fsK/sNBnxzBlL4O1JNrZakVQxPspqpED5dLtNsZS9oOKmtSpdNIzxH2kkol5HYTWJN47sE20ztMJPxfZ89qGOg==",
+ "cpu": [
+ "ppc64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@rolldown/binding-linux-s390x-gnu": {
+ "version": "1.1.5",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.1.5.tgz",
+ "integrity": "sha512-gLYb4BIadlfTOYT5gO503n8zQjXflgzpD0FcyKh0Mzx3rqCZKnHoJWV9xe1KXUJ5lx2JfcSHr/mhzS0PC/McAA==",
+ "cpu": [
+ "s390x"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@rolldown/binding-linux-x64-gnu": {
+ "version": "1.1.5",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.1.5.tgz",
+ "integrity": "sha512-FjcpEKUyJygHgs1o50VYNvkt5+7Le/VEdYt0AkRpkL33MnyQfwr8l5mXwMmfmTbyMPr5vJLC+8/Gd9gXnwU1QQ==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@rolldown/binding-linux-x64-musl": {
+ "version": "1.1.5",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.1.5.tgz",
+ "integrity": "sha512-Me+PfPI2TMeOQk0gYWfLQZtTktrmzbr8cDboqX83XKc7UrgAi55gF+2dUkWdxd19n55Essp2yeca+O9N5rBxHg==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@rolldown/binding-openharmony-arm64": {
+ "version": "1.1.5",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.1.5.tgz",
+ "integrity": "sha512-yc5WrLzXks6zCQfn9Oxr8pORKyl/pF+QjHmW/Qx3qu0oyrrNC+y2JLTU1E2rcWYAmzlnqngWXHQjy51VzW70Vw==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "openharmony"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@rolldown/binding-wasm32-wasi": {
+ "version": "1.1.5",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-wasm32-wasi/-/binding-wasm32-wasi-1.1.5.tgz",
+ "integrity": "sha512-VbQGPX2b4r48TAMIM2cjgluIM1HYutm4pcTEJsle7iEP7sB1dFqtPLBVbdLAZCxy1txCcPxf4QFf4v8uvltPqA==",
+ "cpu": [
+ "wasm32"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "dependencies": {
+ "@emnapi/core": "1.11.1",
+ "@emnapi/runtime": "1.11.1",
+ "@napi-rs/wasm-runtime": "^1.1.6"
+ },
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@rolldown/binding-win32-arm64-msvc": {
+ "version": "1.1.5",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.1.5.tgz",
+ "integrity": "sha512-gHv82k63z4qpV5+Q1y/12KrK0ltWBukVDI8nZcbT7Tt/ZlOIVwppazneq0F93oDxTo3IgAMEDIoQh3E2n6mVsw==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@rolldown/binding-win32-x64-msvc": {
+ "version": "1.1.5",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.1.5.tgz",
+ "integrity": "sha512-tTZuDBPw85tEN5PQi1pnEBzDy0Z49HtScLAbD5t6hyeU92A95pRWaSMw1GZZi/RwgSgUIl0xrSlXIT/9QzvYSA==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@rolldown/pluginutils": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.1.tgz",
+ "integrity": "sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@tailwindcss/node": {
+ "version": "4.3.2",
+ "resolved": "https://registry.npmjs.org/@tailwindcss/node/-/node-4.3.2.tgz",
+ "integrity": "sha512-yWP/sqEcBLaD8JuA6zNwxoYKr75qxTioYwlRwekj5Jr/I5GXnoJfjetH/psLUIv74cYTH2lBUEzBkinthoYcBg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@jridgewell/remapping": "^2.3.5",
+ "enhanced-resolve": "5.21.6",
+ "jiti": "^2.7.0",
+ "lightningcss": "1.32.0",
+ "magic-string": "^0.30.21",
+ "source-map-js": "^1.2.1",
+ "tailwindcss": "4.3.2"
+ }
+ },
+ "node_modules/@tailwindcss/oxide": {
+ "version": "4.3.2",
+ "resolved": "https://registry.npmjs.org/@tailwindcss/oxide/-/oxide-4.3.2.tgz",
+ "integrity": "sha512-z8ZgnzX8gdNoWLBLqBPoh/sjnxkwvf9ZuWjnO0l0yIzbLa5/9S+eC5QxGZKRobVHIC3/1BoMWjHblqWjcgFgag==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 20"
+ },
+ "optionalDependencies": {
+ "@tailwindcss/oxide-android-arm64": "4.3.2",
+ "@tailwindcss/oxide-darwin-arm64": "4.3.2",
+ "@tailwindcss/oxide-darwin-x64": "4.3.2",
+ "@tailwindcss/oxide-freebsd-x64": "4.3.2",
+ "@tailwindcss/oxide-linux-arm-gnueabihf": "4.3.2",
+ "@tailwindcss/oxide-linux-arm64-gnu": "4.3.2",
+ "@tailwindcss/oxide-linux-arm64-musl": "4.3.2",
+ "@tailwindcss/oxide-linux-x64-gnu": "4.3.2",
+ "@tailwindcss/oxide-linux-x64-musl": "4.3.2",
+ "@tailwindcss/oxide-wasm32-wasi": "4.3.2",
+ "@tailwindcss/oxide-win32-arm64-msvc": "4.3.2",
+ "@tailwindcss/oxide-win32-x64-msvc": "4.3.2"
+ }
+ },
+ "node_modules/@tailwindcss/oxide-android-arm64": {
+ "version": "4.3.2",
+ "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-android-arm64/-/oxide-android-arm64-4.3.2.tgz",
+ "integrity": "sha512-WHxqIuHpvZ5VtdX6GTl1Ik/Vp2YuN42Et+0CdeaVd/frQ9jAvGmvR8vLT+jk3e8/Q3x8kECB9+R17pgpp2BulA==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": ">= 20"
+ }
+ },
+ "node_modules/@tailwindcss/oxide-darwin-arm64": {
+ "version": "4.3.2",
+ "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-arm64/-/oxide-darwin-arm64-4.3.2.tgz",
+ "integrity": "sha512-GZypeUY/IDJW3877KeM+O67vbXr3MBnbtEL4aYhNErv/JWZhye2vGSWWG9tB6iiqR2MqRNkY8IOUy4NdSZV26w==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">= 20"
+ }
+ },
+ "node_modules/@tailwindcss/oxide-darwin-x64": {
+ "version": "4.3.2",
+ "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-x64/-/oxide-darwin-x64-4.3.2.tgz",
+ "integrity": "sha512-UIIzmefR6KO1sDU7MzRqAxC8iBpft/VhkGjTjnhoS6k7Z3rQ9wEgA1ODSiyH/tcSYssulNm4Ci3hOeK1jH7ccQ==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">= 20"
+ }
+ },
+ "node_modules/@tailwindcss/oxide-freebsd-x64": {
+ "version": "4.3.2",
+ "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-freebsd-x64/-/oxide-freebsd-x64-4.3.2.tgz",
+ "integrity": "sha512-GN+uAmcI6DNspnCDwtOAZrTz6oukJnp337qZvxqCGLd3BHBzJpO0ZbTLRvJNdztOeAmTzewewGIMPb0tk2R4WA==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ],
+ "engines": {
+ "node": ">= 20"
+ }
+ },
+ "node_modules/@tailwindcss/oxide-linux-arm-gnueabihf": {
+ "version": "4.3.2",
+ "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm-gnueabihf/-/oxide-linux-arm-gnueabihf-4.3.2.tgz",
+ "integrity": "sha512-4ABn7qSbdHRwTiDiuWNegCyb5+2FJ4vKIKc3DmKrvAFw7MU1Lm11dIkTPwUaFdTzc7IsOpDbqBrlh0x6y36U/w==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 20"
+ }
+ },
+ "node_modules/@tailwindcss/oxide-linux-arm64-gnu": {
+ "version": "4.3.2",
+ "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-gnu/-/oxide-linux-arm64-gnu-4.3.2.tgz",
+ "integrity": "sha512-wDgEIGwoM8w8pufh9LVt1PahDgNdKXrLC2qfAnV3vAmococ9RWbxeAw4pxPttd/TsJfwjyLf90Dg1y9y8I6Emw==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 20"
+ }
+ },
+ "node_modules/@tailwindcss/oxide-linux-arm64-musl": {
+ "version": "4.3.2",
+ "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-musl/-/oxide-linux-arm64-musl-4.3.2.tgz",
+ "integrity": "sha512-J5Nuk0uZQIiMTJj3LEx4sAA9tMFUoXQZFv1J6An+QGYe53HKRJuFDi0rpq/tuouCZeAbOBY3kQ6g8qeD4TUjtA==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 20"
+ }
+ },
+ "node_modules/@tailwindcss/oxide-linux-x64-gnu": {
+ "version": "4.3.2",
+ "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-gnu/-/oxide-linux-x64-gnu-4.3.2.tgz",
+ "integrity": "sha512-kqCZpSKOBEJO4mz7OqWoofBZeXTAwaVGPj0ErAj7CojmhKpWVWVOnrt9dE8odoIraZq4oj3ausM37kXi+Tow8w==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 20"
+ }
+ },
+ "node_modules/@tailwindcss/oxide-linux-x64-musl": {
+ "version": "4.3.2",
+ "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-musl/-/oxide-linux-x64-musl-4.3.2.tgz",
+ "integrity": "sha512-cixpqbh2toJDmkuCRI68nXA8ZxNmdK9Y+9v5h3MC3ZQKy/0BO8AWzlkWyRM7JAFSGBlfig4YVTPsK6MVgqz1uw==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 20"
+ }
+ },
+ "node_modules/@tailwindcss/oxide-wasm32-wasi": {
+ "version": "4.3.2",
+ "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-wasm32-wasi/-/oxide-wasm32-wasi-4.3.2.tgz",
+ "integrity": "sha512-4ec2Z/LOmRsAgU23CS4xeJfcJlmRg94A/XrbGRCF1gyU/zdDfRLYDVsS+ynSZCmGNxQ1jQriQOKMQeQxBA3Isw==",
+ "bundleDependencies": [
+ "@napi-rs/wasm-runtime",
+ "@emnapi/core",
+ "@emnapi/runtime",
+ "@tybys/wasm-util",
+ "@emnapi/wasi-threads",
+ "tslib"
+ ],
+ "cpu": [
+ "wasm32"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "dependencies": {
+ "@emnapi/core": "^1.11.1",
+ "@emnapi/runtime": "^1.11.1",
+ "@emnapi/wasi-threads": "^1.2.2",
+ "@napi-rs/wasm-runtime": "^1.1.4",
+ "@tybys/wasm-util": "^0.10.2",
+ "tslib": "^2.8.1"
+ },
+ "engines": {
+ "node": ">=14.0.0"
+ }
+ },
+ "node_modules/@tailwindcss/oxide-win32-arm64-msvc": {
+ "version": "4.3.2",
+ "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-arm64-msvc/-/oxide-win32-arm64-msvc-4.3.2.tgz",
+ "integrity": "sha512-Zyr/M0+XcYZu3bZrUytc7TXvrk0ftWfl8gN2MwekNDzhqhKRUucMPSeOzM0o0wH5AWOU49BsKRrfKxI2atCPMQ==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">= 20"
+ }
+ },
+ "node_modules/@tailwindcss/oxide-win32-x64-msvc": {
+ "version": "4.3.2",
+ "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-x64-msvc/-/oxide-win32-x64-msvc-4.3.2.tgz",
+ "integrity": "sha512-QI9BO7KlNZsp2GuO0jwAAj5jCDABOKXRkCk2XuKTSaNEFSdfzqswYVTtCHBNKHLsqyjFyFkqlDiwkNbTYSssMQ==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">= 20"
+ }
+ },
+ "node_modules/@tailwindcss/postcss": {
+ "version": "4.3.2",
+ "resolved": "https://registry.npmjs.org/@tailwindcss/postcss/-/postcss-4.3.2.tgz",
+ "integrity": "sha512-rjVWYCa7Ngbi5AarT6k8TkxUG3Wl1QKzHdIZVsjZSzf36Jmo2IKZt/NHRAwly8oDkbBOH0YTu+CHuf9jPxMc+g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@alloc/quick-lru": "^5.2.0",
+ "@tailwindcss/node": "4.3.2",
+ "@tailwindcss/oxide": "4.3.2",
+ "postcss": "^8.5.15",
+ "tailwindcss": "4.3.2"
+ }
+ },
+ "node_modules/@tanstack/query-core": {
+ "version": "5.101.2",
+ "resolved": "https://registry.npmjs.org/@tanstack/query-core/-/query-core-5.101.2.tgz",
+ "integrity": "sha512-hH5MLoJhF7KaIGd7q3xTXGXvslI+GYlM1Z/35aSHHWaCJWB7XvTSHYuV3eM7tw+aE0mT/xMro4M4Q9rCGHT0lw==",
+ "license": "MIT",
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/tannerlinsley"
+ }
+ },
+ "node_modules/@tanstack/react-query": {
+ "version": "5.101.2",
+ "resolved": "https://registry.npmjs.org/@tanstack/react-query/-/react-query-5.101.2.tgz",
+ "integrity": "sha512-seDkr6kzGzX1okaaTtZPtgA688CDPlXUz1C6xSg0ESqn04Vuc8tlrYms1s3de+znBqhPVxFRfpAfUf+6XvfPWg==",
+ "license": "MIT",
+ "dependencies": {
+ "@tanstack/query-core": "5.101.2"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/tannerlinsley"
+ },
+ "peerDependencies": {
+ "react": "^18 || ^19"
+ }
+ },
+ "node_modules/@tybys/wasm-util": {
+ "version": "0.10.3",
+ "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.3.tgz",
+ "integrity": "sha512-F3fo1MYrRJYL3zER0OUOmkutjr1Vp23m7OsSgp7nq4SP6OqX6C/56XFIPAl5bt3zaBRjmW7SGz3u/6LwFpYcOg==",
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "dependencies": {
+ "tslib": "^2.4.0"
+ }
+ },
+ "node_modules/@types/esrecurse": {
+ "version": "4.3.1",
+ "resolved": "https://registry.npmjs.org/@types/esrecurse/-/esrecurse-4.3.1.tgz",
+ "integrity": "sha512-xJBAbDifo5hpffDBuHl0Y8ywswbiAp/Wi7Y/GtAgSlZyIABppyurxVueOPE8LUQOxdlgi6Zqce7uoEpqNTeiUw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@types/estree": {
+ "version": "1.0.9",
+ "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.9.tgz",
+ "integrity": "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@types/json-schema": {
+ "version": "7.0.15",
+ "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz",
+ "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@types/react": {
+ "version": "19.2.17",
+ "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.17.tgz",
+ "integrity": "sha512-MXfmqaVPEVgkBT/aY0aGCkRWWtByiYQXo3xdQ8r5RzuFrPiRn8Gar2tQdXSUQ2GKV3bkXckek89V8wQBY2Q/Aw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "csstype": "^3.2.2"
+ }
+ },
+ "node_modules/@types/react-dom": {
+ "version": "19.2.3",
+ "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.2.3.tgz",
+ "integrity": "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==",
+ "dev": true,
+ "license": "MIT",
+ "peerDependencies": {
+ "@types/react": "^19.2.0"
+ }
+ },
+ "node_modules/@typescript-eslint/eslint-plugin": {
+ "version": "8.63.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.63.0.tgz",
+ "integrity": "sha512-rvwSgqT+DHpWdzfSzPatRLm02a0GlESt++9iy3hLCDY4BgkaLcl8LBi9Yh7XGFBpwcBE/K3024QuXWTpbz4FfQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@eslint-community/regexpp": "^4.12.2",
+ "@typescript-eslint/scope-manager": "8.63.0",
+ "@typescript-eslint/type-utils": "8.63.0",
+ "@typescript-eslint/utils": "8.63.0",
+ "@typescript-eslint/visitor-keys": "8.63.0",
+ "ignore": "^7.0.5",
+ "natural-compare": "^1.4.0",
+ "ts-api-utils": "^2.5.0"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ },
+ "peerDependencies": {
+ "@typescript-eslint/parser": "^8.63.0",
+ "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0",
+ "typescript": ">=4.8.4 <6.1.0"
+ }
+ },
+ "node_modules/@typescript-eslint/parser": {
+ "version": "8.63.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.63.0.tgz",
+ "integrity": "sha512-gwh4gvvlaVDKKxyfxMG+Gnu1u9X0OQBwyGLkbwB65dIzBKnxeRiJlNFqlI3zwVhNXJIs6qV7mlFCn/BIajlVig==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@typescript-eslint/scope-manager": "8.63.0",
+ "@typescript-eslint/types": "8.63.0",
+ "@typescript-eslint/typescript-estree": "8.63.0",
+ "@typescript-eslint/visitor-keys": "8.63.0",
+ "debug": "^4.4.3"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ },
+ "peerDependencies": {
+ "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0",
+ "typescript": ">=4.8.4 <6.1.0"
+ }
+ },
+ "node_modules/@typescript-eslint/project-service": {
+ "version": "8.63.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.63.0.tgz",
+ "integrity": "sha512-e5dh0/UI0ok53AlZ5wRkXCB32z/f2jUZqPR/ygAw5WYaSw8j9EoJWlS7wQjr/dmOaqWjnPIn2m+HhVPCMWGZVQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@typescript-eslint/tsconfig-utils": "^8.63.0",
+ "@typescript-eslint/types": "^8.63.0",
+ "debug": "^4.4.3"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ },
+ "peerDependencies": {
+ "typescript": ">=4.8.4 <6.1.0"
+ }
+ },
+ "node_modules/@typescript-eslint/scope-manager": {
+ "version": "8.63.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.63.0.tgz",
+ "integrity": "sha512-uUyfMWCnDSN8bCpcrY8nGP2BLkQ9Xn0GsipcONcpIDWhwhO4ZSyHvyS14U3X75mzxWxL3I2UZIrenTzdzcJO8A==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@typescript-eslint/types": "8.63.0",
+ "@typescript-eslint/visitor-keys": "8.63.0"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ }
+ },
+ "node_modules/@typescript-eslint/tsconfig-utils": {
+ "version": "8.63.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.63.0.tgz",
+ "integrity": "sha512-sUAbkulqBAsncKnbRP3+7CtQFRKicexnj7ZwNC6ddCR7EmrXvjvdCYMJbUIqMd6lwoEriZjwLo08aS5tSjVMHg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ },
+ "peerDependencies": {
+ "typescript": ">=4.8.4 <6.1.0"
+ }
+ },
+ "node_modules/@typescript-eslint/type-utils": {
+ "version": "8.63.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.63.0.tgz",
+ "integrity": "sha512-Nzzh/OGxVCOjObjaj1CQF2RUasyYy2Jfuh+zZ3PjLzG2fYRriAiZLib9UKtO+CpQAS3YHiAS+ckZDclwqI1TPA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@typescript-eslint/types": "8.63.0",
+ "@typescript-eslint/typescript-estree": "8.63.0",
+ "@typescript-eslint/utils": "8.63.0",
+ "debug": "^4.4.3",
+ "ts-api-utils": "^2.5.0"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ },
+ "peerDependencies": {
+ "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0",
+ "typescript": ">=4.8.4 <6.1.0"
+ }
+ },
+ "node_modules/@typescript-eslint/types": {
+ "version": "8.63.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.63.0.tgz",
+ "integrity": "sha512-xyLtl9DUBBFrcJS4x2pIqGLH68/tC2uOa4Z7pUteW09D3bXnnXUom4dyPikzWgB7llmIc1zoeI3aoUdC4rPK/Q==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ }
+ },
+ "node_modules/@typescript-eslint/typescript-estree": {
+ "version": "8.63.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.63.0.tgz",
+ "integrity": "sha512-ygBkU+B7ex5UI/gKhaqexWev79uISfIv7XQCRNYO/jmD8rGLPyWLAb3KMRT6nd8Gt9bmUBi9+iX6tBdYfOY81Q==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@typescript-eslint/project-service": "8.63.0",
+ "@typescript-eslint/tsconfig-utils": "8.63.0",
+ "@typescript-eslint/types": "8.63.0",
+ "@typescript-eslint/visitor-keys": "8.63.0",
+ "debug": "^4.4.3",
+ "minimatch": "^10.2.2",
+ "semver": "^7.7.3",
+ "tinyglobby": "^0.2.15",
+ "ts-api-utils": "^2.5.0"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ },
+ "peerDependencies": {
+ "typescript": ">=4.8.4 <6.1.0"
+ }
+ },
+ "node_modules/@typescript-eslint/utils": {
+ "version": "8.63.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.63.0.tgz",
+ "integrity": "sha512-fUKaeAvrTuQg/Tgt3nliAUSZHJM6DlCcfyEmxCvlX8kieWSStBX+5O5Fnidtc3i2JrH+9c/GL4RY2iasd/GPTA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@eslint-community/eslint-utils": "^4.9.1",
+ "@typescript-eslint/scope-manager": "8.63.0",
+ "@typescript-eslint/types": "8.63.0",
+ "@typescript-eslint/typescript-estree": "8.63.0"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ },
+ "peerDependencies": {
+ "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0",
+ "typescript": ">=4.8.4 <6.1.0"
+ }
+ },
+ "node_modules/@typescript-eslint/visitor-keys": {
+ "version": "8.63.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.63.0.tgz",
+ "integrity": "sha512-UexrHGnGTpbuQHct2ExOc2ZcFbGUS9FOesCxxqdBGcpI1BxYu/LZ6U8Aq6/72XtF/qRBk9nhuGHFJIXXMhPMdw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@typescript-eslint/types": "8.63.0",
+ "eslint-visitor-keys": "^5.0.0"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ }
+ },
+ "node_modules/@typescript-eslint/visitor-keys/node_modules/eslint-visitor-keys": {
+ "version": "5.0.1",
+ "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-5.0.1.tgz",
+ "integrity": "sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "engines": {
+ "node": "^20.19.0 || ^22.13.0 || >=24"
+ },
+ "funding": {
+ "url": "https://opencollective.com/eslint"
+ }
+ },
+ "node_modules/@vitejs/plugin-react": {
+ "version": "6.0.3",
+ "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-6.0.3.tgz",
+ "integrity": "sha512-vmFvco5/QuC2f9Oj+wTk0+9XeDFkHxSamwZKYc7MxYwKICfvUvlMhqKI0VuICPltGqh1neqBKDvO4kes1ya8vg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@rolldown/pluginutils": "^1.0.1"
+ },
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ },
+ "peerDependencies": {
+ "@rolldown/plugin-babel": "^0.1.7 || ^0.2.0",
+ "babel-plugin-react-compiler": "^1.0.0",
+ "vite": "^8.0.0"
+ },
+ "peerDependenciesMeta": {
+ "@rolldown/plugin-babel": {
+ "optional": true
+ },
+ "babel-plugin-react-compiler": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/acorn": {
+ "version": "8.17.0",
+ "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.17.0.tgz",
+ "integrity": "sha512-xRQbDb9BnwDafYNn6Vwl839DYVjqXYb1XVGtWAZ1kcDc6iwAL4hg3B1dZlRiuENFeO2H53gFG3in621AdERVAg==",
+ "dev": true,
+ "license": "MIT",
+ "bin": {
+ "acorn": "bin/acorn"
+ },
+ "engines": {
+ "node": ">=0.4.0"
+ }
+ },
+ "node_modules/acorn-jsx": {
+ "version": "5.3.2",
+ "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz",
+ "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==",
+ "dev": true,
+ "license": "MIT",
+ "peerDependencies": {
+ "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0"
+ }
+ },
+ "node_modules/ajv": {
+ "version": "6.15.0",
+ "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.15.0.tgz",
+ "integrity": "sha512-fgFx7Hfoq60ytK2c7DhnF8jIvzYgOMxfugjLOSMHjLIPgenqa7S7oaagATUq99mV6IYvN2tRmC0wnTYX6iPbMw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "fast-deep-equal": "^3.1.1",
+ "fast-json-stable-stringify": "^2.0.0",
+ "json-schema-traverse": "^0.4.1",
+ "uri-js": "^4.2.2"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/epoberezkin"
+ }
+ },
+ "node_modules/balanced-match": {
+ "version": "4.0.4",
+ "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz",
+ "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": "18 || 20 || >=22"
+ }
+ },
+ "node_modules/baseline-browser-mapping": {
+ "version": "2.10.42",
+ "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.42.tgz",
+ "integrity": "sha512-c/jurFrDLyui7o1J86yLkRu4LMsTYcBohveus7/I2Hzdn9KIP2bdJPTue/lR1KH46enoPbD77GKeSYNdyPoD3Q==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "bin": {
+ "baseline-browser-mapping": "dist/cli.cjs"
+ },
+ "engines": {
+ "node": ">=6.0.0"
+ }
+ },
+ "node_modules/brace-expansion": {
+ "version": "5.0.7",
+ "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.7.tgz",
+ "integrity": "sha512-7oFy703dxfY3/NLxC1fh2SUCQ0H9rmAY+5EpDVfXjUTTs+HEwR2nYaqLv+GWcTsumwxPfiz6CzCNkwXwBUwqCA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "balanced-match": "^4.0.2"
+ },
+ "engines": {
+ "node": "18 || 20 || >=22"
+ }
+ },
+ "node_modules/browserslist": {
+ "version": "4.28.5",
+ "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.5.tgz",
+ "integrity": "sha512-Cu2E6QejHWzuDMTkuwgpABFgDfZrXLQq5V13YOACZx4mFAG4IwGTbTfHPMr4WtxlHoXSM8FIuRwYYCz5XiabaQ==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/browserslist"
+ },
+ {
+ "type": "tidelift",
+ "url": "https://tidelift.com/funding/github/npm/browserslist"
+ },
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "baseline-browser-mapping": "^2.10.42",
+ "caniuse-lite": "^1.0.30001800",
+ "electron-to-chromium": "^1.5.387",
+ "node-releases": "^2.0.50",
+ "update-browserslist-db": "^1.2.3"
+ },
+ "bin": {
+ "browserslist": "cli.js"
+ },
+ "engines": {
+ "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7"
+ }
+ },
+ "node_modules/caniuse-lite": {
+ "version": "1.0.30001803",
+ "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001803.tgz",
+ "integrity": "sha512-g/uHREV2ZpK9qMalCsWaxmA6ol+DX8GYhuf3T40RKoP+oL7vhRJh8LNt73PCjpnR6l14FzfPrB5Yux4PKm2meg==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/browserslist"
+ },
+ {
+ "type": "tidelift",
+ "url": "https://tidelift.com/funding/github/npm/caniuse-lite"
+ },
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "license": "CC-BY-4.0"
+ },
+ "node_modules/clsx": {
+ "version": "2.1.1",
+ "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz",
+ "integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/convert-source-map": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz",
+ "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/cross-spawn": {
+ "version": "7.0.6",
+ "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz",
+ "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "path-key": "^3.1.0",
+ "shebang-command": "^2.0.0",
+ "which": "^2.0.1"
+ },
+ "engines": {
+ "node": ">= 8"
+ }
+ },
+ "node_modules/csstype": {
+ "version": "3.2.3",
+ "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz",
+ "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/debug": {
+ "version": "4.4.3",
+ "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
+ "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "ms": "^2.1.3"
+ },
+ "engines": {
+ "node": ">=6.0"
+ },
+ "peerDependenciesMeta": {
+ "supports-color": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/deep-is": {
+ "version": "0.1.4",
+ "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz",
+ "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/detect-libc": {
+ "version": "2.1.2",
+ "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz",
+ "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/electron-to-chromium": {
+ "version": "1.5.389",
+ "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.389.tgz",
+ "integrity": "sha512-cEto7aeOqBfU1D+c5py5pE+ooscKE75JifxLBdFUZsqAxRS6y7kebtxAZvICszSl05gPjYHDTjY+lXpyGvpJbg==",
+ "dev": true,
+ "license": "ISC"
+ },
+ "node_modules/enhanced-resolve": {
+ "version": "5.21.6",
+ "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.21.6.tgz",
+ "integrity": "sha512-aNnGCvbJ/RIyWo1IuhNdVjnNF+EjH9wpzpNHt+ci/m9He9LJvUN8wrCcXjp9cWsGNAuvSpVFTx/vraAFQ8qGjQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "graceful-fs": "^4.2.4",
+ "tapable": "^2.3.3"
+ },
+ "engines": {
+ "node": ">=10.13.0"
+ }
+ },
+ "node_modules/escalade": {
+ "version": "3.2.0",
+ "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz",
+ "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/escape-string-regexp": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz",
+ "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/eslint": {
+ "version": "10.6.0",
+ "resolved": "https://registry.npmjs.org/eslint/-/eslint-10.6.0.tgz",
+ "integrity": "sha512-6lVbcqSodALYo+4ELD0heG6lFiFxnLMuLkiMi2qV8LMp54N8tE8FT1GMH+ev4Ti00nFjNze2+Su6DsV5OQW3Dg==",
+ "dev": true,
+ "license": "MIT",
+ "workspaces": [
+ "packages/*"
+ ],
+ "dependencies": {
+ "@eslint-community/eslint-utils": "^4.8.0",
+ "@eslint-community/regexpp": "^4.12.2",
+ "@eslint/config-array": "^0.23.5",
+ "@eslint/config-helpers": "^0.6.0",
+ "@eslint/core": "^1.2.1",
+ "@eslint/plugin-kit": "^0.7.2",
+ "@humanfs/node": "^0.16.6",
+ "@humanwhocodes/module-importer": "^1.0.1",
+ "@humanwhocodes/retry": "^0.4.2",
+ "@types/estree": "^1.0.6",
+ "ajv": "^6.14.0",
+ "cross-spawn": "^7.0.6",
+ "debug": "^4.3.2",
+ "escape-string-regexp": "^4.0.0",
+ "eslint-scope": "^9.1.2",
+ "eslint-visitor-keys": "^5.0.1",
+ "espree": "^11.2.0",
+ "esquery": "^1.7.0",
+ "esutils": "^2.0.2",
+ "fast-deep-equal": "^3.1.3",
+ "file-entry-cache": "^8.0.0",
+ "find-up": "^5.0.0",
+ "glob-parent": "^6.0.2",
+ "ignore": "^5.2.0",
+ "imurmurhash": "^0.1.4",
+ "is-glob": "^4.0.0",
+ "json-stable-stringify-without-jsonify": "^1.0.1",
+ "minimatch": "^10.2.4",
+ "natural-compare": "^1.4.0",
+ "optionator": "^0.9.3"
+ },
+ "bin": {
+ "eslint": "bin/eslint.js"
+ },
+ "engines": {
+ "node": "^20.19.0 || ^22.13.0 || >=24"
+ },
+ "funding": {
+ "url": "https://eslint.org/donate"
+ },
+ "peerDependencies": {
+ "jiti": "*"
+ },
+ "peerDependenciesMeta": {
+ "jiti": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/eslint-plugin-react-hooks": {
+ "version": "7.1.1",
+ "resolved": "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-7.1.1.tgz",
+ "integrity": "sha512-f2I7Gw6JbvCexzIInuSbZpfdQ44D7iqdWX01FKLvrPgqxoE7oMj8clOfto8U6vYiz4yd5oKu39rRSVOe1zRu0g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/core": "^7.24.4",
+ "@babel/parser": "^7.24.4",
+ "hermes-parser": "^0.25.1",
+ "zod": "^3.25.0 || ^4.0.0",
+ "zod-validation-error": "^3.5.0 || ^4.0.0"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "peerDependencies": {
+ "eslint": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 || ^9.0.0 || ^10.0.0"
+ }
+ },
+ "node_modules/eslint-plugin-react-refresh": {
+ "version": "0.5.3",
+ "resolved": "https://registry.npmjs.org/eslint-plugin-react-refresh/-/eslint-plugin-react-refresh-0.5.3.tgz",
+ "integrity": "sha512-5EMmLCV98Pi4o/f/3DP/v/tNqLHMIc9I8LKClNDWhZ9JTho89/kQcitCXQBMG7sAfVRK0Ie3T2EDOzp1YXYiVA==",
+ "dev": true,
+ "license": "MIT",
+ "peerDependencies": {
+ "eslint": "^9 || ^10"
+ }
+ },
+ "node_modules/eslint-scope": {
+ "version": "9.1.2",
+ "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-9.1.2.tgz",
+ "integrity": "sha512-xS90H51cKw0jltxmvmHy2Iai1LIqrfbw57b79w/J7MfvDfkIkFZ+kj6zC3BjtUwh150HsSSdxXZcsuv72miDFQ==",
+ "dev": true,
+ "license": "BSD-2-Clause",
+ "dependencies": {
+ "@types/esrecurse": "^4.3.1",
+ "@types/estree": "^1.0.8",
+ "esrecurse": "^4.3.0",
+ "estraverse": "^5.2.0"
+ },
+ "engines": {
+ "node": "^20.19.0 || ^22.13.0 || >=24"
+ },
+ "funding": {
+ "url": "https://opencollective.com/eslint"
+ }
+ },
+ "node_modules/eslint-visitor-keys": {
+ "version": "3.4.3",
+ "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz",
+ "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "engines": {
+ "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/eslint"
+ }
+ },
+ "node_modules/eslint/node_modules/eslint-visitor-keys": {
+ "version": "5.0.1",
+ "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-5.0.1.tgz",
+ "integrity": "sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "engines": {
+ "node": "^20.19.0 || ^22.13.0 || >=24"
+ },
+ "funding": {
+ "url": "https://opencollective.com/eslint"
+ }
+ },
+ "node_modules/eslint/node_modules/ignore": {
+ "version": "5.3.2",
+ "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz",
+ "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 4"
+ }
+ },
+ "node_modules/espree": {
+ "version": "11.2.0",
+ "resolved": "https://registry.npmjs.org/espree/-/espree-11.2.0.tgz",
+ "integrity": "sha512-7p3DrVEIopW1B1avAGLuCSh1jubc01H2JHc8B4qqGblmg5gI9yumBgACjWo4JlIc04ufug4xJ3SQI8HkS/Rgzw==",
+ "dev": true,
+ "license": "BSD-2-Clause",
+ "dependencies": {
+ "acorn": "^8.16.0",
+ "acorn-jsx": "^5.3.2",
+ "eslint-visitor-keys": "^5.0.1"
+ },
+ "engines": {
+ "node": "^20.19.0 || ^22.13.0 || >=24"
+ },
+ "funding": {
+ "url": "https://opencollective.com/eslint"
+ }
+ },
+ "node_modules/espree/node_modules/eslint-visitor-keys": {
+ "version": "5.0.1",
+ "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-5.0.1.tgz",
+ "integrity": "sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "engines": {
+ "node": "^20.19.0 || ^22.13.0 || >=24"
+ },
+ "funding": {
+ "url": "https://opencollective.com/eslint"
+ }
+ },
+ "node_modules/esquery": {
+ "version": "1.7.0",
+ "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.7.0.tgz",
+ "integrity": "sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g==",
+ "dev": true,
+ "license": "BSD-3-Clause",
+ "dependencies": {
+ "estraverse": "^5.1.0"
+ },
+ "engines": {
+ "node": ">=0.10"
+ }
+ },
+ "node_modules/esrecurse": {
+ "version": "4.3.0",
+ "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz",
+ "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==",
+ "dev": true,
+ "license": "BSD-2-Clause",
+ "dependencies": {
+ "estraverse": "^5.2.0"
+ },
+ "engines": {
+ "node": ">=4.0"
+ }
+ },
+ "node_modules/estraverse": {
+ "version": "5.3.0",
+ "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz",
+ "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==",
+ "dev": true,
+ "license": "BSD-2-Clause",
+ "engines": {
+ "node": ">=4.0"
+ }
+ },
+ "node_modules/esutils": {
+ "version": "2.0.3",
+ "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz",
+ "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==",
+ "dev": true,
+ "license": "BSD-2-Clause",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/fast-deep-equal": {
+ "version": "3.1.3",
+ "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
+ "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/fast-json-stable-stringify": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz",
+ "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/fast-levenshtein": {
+ "version": "2.0.6",
+ "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz",
+ "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/fdir": {
+ "version": "6.5.0",
+ "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz",
+ "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=12.0.0"
+ },
+ "peerDependencies": {
+ "picomatch": "^3 || ^4"
+ },
+ "peerDependenciesMeta": {
+ "picomatch": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/file-entry-cache": {
+ "version": "8.0.0",
+ "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz",
+ "integrity": "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "flat-cache": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=16.0.0"
+ }
+ },
+ "node_modules/find-up": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz",
+ "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "locate-path": "^6.0.0",
+ "path-exists": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/flat-cache": {
+ "version": "4.0.1",
+ "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-4.0.1.tgz",
+ "integrity": "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "flatted": "^3.2.9",
+ "keyv": "^4.5.4"
+ },
+ "engines": {
+ "node": ">=16"
+ }
+ },
+ "node_modules/flatted": {
+ "version": "3.4.2",
+ "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.4.2.tgz",
+ "integrity": "sha512-PjDse7RzhcPkIJwy5t7KPWQSZ9cAbzQXcafsetQoD7sOJRQlGikNbx7yZp2OotDnJyrDcbyRq3Ttb18iYOqkxA==",
+ "dev": true,
+ "license": "ISC"
+ },
+ "node_modules/framer-motion": {
+ "version": "12.42.2",
+ "resolved": "https://registry.npmjs.org/framer-motion/-/framer-motion-12.42.2.tgz",
+ "integrity": "sha512-5XY9luDiu0oHfHBjpDthFMh0ES+122w6p/papSJBweMkO8Sn+PW2QaEgRblQBpWFnuvZS5qvarpt/hO2pjGmnw==",
+ "license": "MIT",
+ "dependencies": {
+ "motion-dom": "^12.42.2",
+ "motion-utils": "^12.39.0",
+ "tslib": "^2.4.0"
+ },
+ "peerDependencies": {
+ "@emotion/is-prop-valid": "*",
+ "react": "^18.0.0 || ^19.0.0",
+ "react-dom": "^18.0.0 || ^19.0.0"
+ },
+ "peerDependenciesMeta": {
+ "@emotion/is-prop-valid": {
+ "optional": true
+ },
+ "react": {
+ "optional": true
+ },
+ "react-dom": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/fsevents": {
+ "version": "2.3.3",
+ "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
+ "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
+ "dev": true,
+ "hasInstallScript": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": "^8.16.0 || ^10.6.0 || >=11.0.0"
+ }
+ },
+ "node_modules/gensync": {
+ "version": "1.0.0-beta.2",
+ "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz",
+ "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/glob-parent": {
+ "version": "6.0.2",
+ "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz",
+ "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "is-glob": "^4.0.3"
+ },
+ "engines": {
+ "node": ">=10.13.0"
+ }
+ },
+ "node_modules/globals": {
+ "version": "17.7.0",
+ "resolved": "https://registry.npmjs.org/globals/-/globals-17.7.0.tgz",
+ "integrity": "sha512-Czmyns5dUsq4seFBR/Kdydhmo8y9kC79hiSkPn0YcGtNnYWnrgt0vjrSjx9tspoDGWm2CMarffRuLjM4xUz8xg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/graceful-fs": {
+ "version": "4.2.11",
+ "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz",
+ "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==",
+ "dev": true,
+ "license": "ISC"
+ },
+ "node_modules/hermes-estree": {
+ "version": "0.25.1",
+ "resolved": "https://registry.npmjs.org/hermes-estree/-/hermes-estree-0.25.1.tgz",
+ "integrity": "sha512-0wUoCcLp+5Ev5pDW2OriHC2MJCbwLwuRx+gAqMTOkGKJJiBCLjtrvy4PWUGn6MIVefecRpzoOZ/UV6iGdOr+Cw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/hermes-parser": {
+ "version": "0.25.1",
+ "resolved": "https://registry.npmjs.org/hermes-parser/-/hermes-parser-0.25.1.tgz",
+ "integrity": "sha512-6pEjquH3rqaI6cYAXYPcz9MS4rY6R4ngRgrgfDshRptUZIc3lw0MCIJIGDj9++mfySOuPTHB4nrSW99BCvOPIA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "hermes-estree": "0.25.1"
+ }
+ },
+ "node_modules/ignore": {
+ "version": "7.0.5",
+ "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.5.tgz",
+ "integrity": "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 4"
+ }
+ },
+ "node_modules/imurmurhash": {
+ "version": "0.1.4",
+ "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz",
+ "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.8.19"
+ }
+ },
+ "node_modules/is-extglob": {
+ "version": "2.1.1",
+ "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz",
+ "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/is-glob": {
+ "version": "4.0.3",
+ "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz",
+ "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "is-extglob": "^2.1.1"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/isexe": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz",
+ "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==",
+ "dev": true,
+ "license": "ISC"
+ },
+ "node_modules/jiti": {
+ "version": "2.7.0",
+ "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.7.0.tgz",
+ "integrity": "sha512-AC/7JofJvZGrrneWNaEnJeOLUx+JlGt7tNa0wZiRPT4MY1wmfKjt2+6O2p2uz2+skll8OZZmJMNqeke7kKbNgQ==",
+ "dev": true,
+ "license": "MIT",
+ "bin": {
+ "jiti": "lib/jiti-cli.mjs"
+ }
+ },
+ "node_modules/js-tokens": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
+ "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/jsesc": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz",
+ "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==",
+ "dev": true,
+ "license": "MIT",
+ "bin": {
+ "jsesc": "bin/jsesc"
+ },
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/json-buffer": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz",
+ "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/json-schema-traverse": {
+ "version": "0.4.1",
+ "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz",
+ "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/json-stable-stringify-without-jsonify": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz",
+ "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/json5": {
+ "version": "2.2.3",
+ "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz",
+ "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==",
+ "dev": true,
+ "license": "MIT",
+ "bin": {
+ "json5": "lib/cli.js"
+ },
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/keyv": {
+ "version": "4.5.4",
+ "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz",
+ "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "json-buffer": "3.0.1"
+ }
+ },
+ "node_modules/levn": {
+ "version": "0.4.1",
+ "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz",
+ "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "prelude-ls": "^1.2.1",
+ "type-check": "~0.4.0"
+ },
+ "engines": {
+ "node": ">= 0.8.0"
+ }
+ },
+ "node_modules/lightningcss": {
+ "version": "1.32.0",
+ "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.32.0.tgz",
+ "integrity": "sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ==",
+ "dev": true,
+ "license": "MPL-2.0",
+ "dependencies": {
+ "detect-libc": "^2.0.3"
+ },
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ },
+ "optionalDependencies": {
+ "lightningcss-android-arm64": "1.32.0",
+ "lightningcss-darwin-arm64": "1.32.0",
+ "lightningcss-darwin-x64": "1.32.0",
+ "lightningcss-freebsd-x64": "1.32.0",
+ "lightningcss-linux-arm-gnueabihf": "1.32.0",
+ "lightningcss-linux-arm64-gnu": "1.32.0",
+ "lightningcss-linux-arm64-musl": "1.32.0",
+ "lightningcss-linux-x64-gnu": "1.32.0",
+ "lightningcss-linux-x64-musl": "1.32.0",
+ "lightningcss-win32-arm64-msvc": "1.32.0",
+ "lightningcss-win32-x64-msvc": "1.32.0"
+ }
+ },
+ "node_modules/lightningcss-android-arm64": {
+ "version": "1.32.0",
+ "resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.32.0.tgz",
+ "integrity": "sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/lightningcss-darwin-arm64": {
+ "version": "1.32.0",
+ "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.32.0.tgz",
+ "integrity": "sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/lightningcss-darwin-x64": {
+ "version": "1.32.0",
+ "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.32.0.tgz",
+ "integrity": "sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/lightningcss-freebsd-x64": {
+ "version": "1.32.0",
+ "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.32.0.tgz",
+ "integrity": "sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/lightningcss-linux-arm-gnueabihf": {
+ "version": "1.32.0",
+ "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.32.0.tgz",
+ "integrity": "sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/lightningcss-linux-arm64-gnu": {
+ "version": "1.32.0",
+ "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.32.0.tgz",
+ "integrity": "sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/lightningcss-linux-arm64-musl": {
+ "version": "1.32.0",
+ "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.32.0.tgz",
+ "integrity": "sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/lightningcss-linux-x64-gnu": {
+ "version": "1.32.0",
+ "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.32.0.tgz",
+ "integrity": "sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/lightningcss-linux-x64-musl": {
+ "version": "1.32.0",
+ "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.32.0.tgz",
+ "integrity": "sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/lightningcss-win32-arm64-msvc": {
+ "version": "1.32.0",
+ "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.32.0.tgz",
+ "integrity": "sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/lightningcss-win32-x64-msvc": {
+ "version": "1.32.0",
+ "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.32.0.tgz",
+ "integrity": "sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/locate-path": {
+ "version": "6.0.0",
+ "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz",
+ "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "p-locate": "^5.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/lru-cache": {
+ "version": "5.1.1",
+ "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz",
+ "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "yallist": "^3.0.2"
+ }
+ },
+ "node_modules/lucide-react": {
+ "version": "1.23.0",
+ "resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-1.23.0.tgz",
+ "integrity": "sha512-38BpJcD0JhFosxHApP/BYsBetLpQFRoTRzEzstM/XCc3jsAG7wqaY1lgVwxiUe3xqYE+lNxo2PkCmYwXWrwwIw==",
+ "license": "ISC",
+ "peerDependencies": {
+ "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0"
+ }
+ },
+ "node_modules/magic-string": {
+ "version": "0.30.21",
+ "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz",
+ "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@jridgewell/sourcemap-codec": "^1.5.5"
+ }
+ },
+ "node_modules/minimatch": {
+ "version": "10.2.5",
+ "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.5.tgz",
+ "integrity": "sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==",
+ "dev": true,
+ "license": "BlueOak-1.0.0",
+ "dependencies": {
+ "brace-expansion": "^5.0.5"
+ },
+ "engines": {
+ "node": "18 || 20 || >=22"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/isaacs"
+ }
+ },
+ "node_modules/motion-dom": {
+ "version": "12.42.2",
+ "resolved": "https://registry.npmjs.org/motion-dom/-/motion-dom-12.42.2.tgz",
+ "integrity": "sha512-5gIMWLp/PycBtJRJWRgjxke5n8dlvkSn2DrYW+tr3XcqAZY1xZh6BJyooJXCM8wdfM7wfMjkBJNLge1CKPUIRA==",
+ "license": "MIT",
+ "dependencies": {
+ "motion-utils": "^12.39.0"
+ }
+ },
+ "node_modules/motion-utils": {
+ "version": "12.39.0",
+ "resolved": "https://registry.npmjs.org/motion-utils/-/motion-utils-12.39.0.tgz",
+ "integrity": "sha512-8nadJAJjTtqRkmRF36FoJTrywK9nnFmnPwnSMyxaOCU7GDjN9RTMJIxx9De8ErM+vpPhMccr/6fo5WciyQLnMQ==",
+ "license": "MIT"
+ },
+ "node_modules/ms": {
+ "version": "2.1.3",
+ "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
+ "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/nanoid": {
+ "version": "3.3.15",
+ "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.15.tgz",
+ "integrity": "sha512-y7Wygv/7mEOvxTuEQDB8StXdMRBWf1kR/tlhAzBRUFkB2jfcLOAxO/SHmOO2zgz1pVgK29/kyupn059/bCHdjA==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "license": "MIT",
+ "bin": {
+ "nanoid": "bin/nanoid.cjs"
+ },
+ "engines": {
+ "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
+ }
+ },
+ "node_modules/natural-compare": {
+ "version": "1.4.0",
+ "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz",
+ "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/node-releases": {
+ "version": "2.0.51",
+ "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.51.tgz",
+ "integrity": "sha512-wRNIrw4DmVLKQlbgOMdkMx27Wrpzes2hh5Jtbi2bjPd+4wJstWIqP5A+lscnqbm0xxmT5Bpg8Lec5ItEBwx6BQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/optionator": {
+ "version": "0.9.4",
+ "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz",
+ "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "deep-is": "^0.1.3",
+ "fast-levenshtein": "^2.0.6",
+ "levn": "^0.4.1",
+ "prelude-ls": "^1.2.1",
+ "type-check": "^0.4.0",
+ "word-wrap": "^1.2.5"
+ },
+ "engines": {
+ "node": ">= 0.8.0"
+ }
+ },
+ "node_modules/p-limit": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz",
+ "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "yocto-queue": "^0.1.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/p-locate": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz",
+ "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "p-limit": "^3.0.2"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/path-exists": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz",
+ "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/path-key": {
+ "version": "3.1.1",
+ "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz",
+ "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/picocolors": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
+ "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==",
+ "dev": true,
+ "license": "ISC"
+ },
+ "node_modules/picomatch": {
+ "version": "4.0.5",
+ "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.5.tgz",
+ "integrity": "sha512-RvwwcruNjI1ncT5xRakeyS9Lf8lcItv34KD+aif+VH9kduAyfYBipGh12274xtenIPZ119/R9BdTBa8gAwSh0A==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/jonschlinkert"
+ }
+ },
+ "node_modules/postcss": {
+ "version": "8.5.16",
+ "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.16.tgz",
+ "integrity": "sha512-vuwillviilfKZsg0VGj5R/YwwcHx4SLsIOI/7K6mQkWx+l5cUHTjj5g0AasTBcyXsbfTgrwsUNmVUb5xVwyPwg==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/postcss/"
+ },
+ {
+ "type": "tidelift",
+ "url": "https://tidelift.com/funding/github/npm/postcss"
+ },
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "nanoid": "^3.3.12",
+ "picocolors": "^1.1.1",
+ "source-map-js": "^1.2.1"
+ },
+ "engines": {
+ "node": "^10 || ^12 || >=14"
+ }
+ },
+ "node_modules/prelude-ls": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz",
+ "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.8.0"
+ }
+ },
+ "node_modules/punycode": {
+ "version": "2.3.1",
+ "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz",
+ "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/react": {
+ "version": "19.2.7",
+ "resolved": "https://registry.npmjs.org/react/-/react-19.2.7.tgz",
+ "integrity": "sha512-HNe9WslTbXmFK8o8cmwgAeJFSBvt1bPdHCVKtaaV+WlAN36mpT4hcRpwbf3fY56ar2oIXzsBpOAiIRHAdY0OlQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/react-dom": {
+ "version": "19.2.7",
+ "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.7.tgz",
+ "integrity": "sha512-t0BRVXvbiE/o20Hfw669rLbMCDWtYZLvmJigy2f0MxsXF+71pxhR3xOkspmsO8h3ZlNzyibAmtCa3l4lYKk6gQ==",
+ "license": "MIT",
+ "dependencies": {
+ "scheduler": "^0.27.0"
+ },
+ "peerDependencies": {
+ "react": "^19.2.7"
+ }
+ },
+ "node_modules/rolldown": {
+ "version": "1.1.5",
+ "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.1.5.tgz",
+ "integrity": "sha512-t9z29cJjXf/vxQ8dyhCSpt6H6aSwHTk8cT5I3iy6SMXuFpk5mB6PL6XfC8PCwrPTx93udwKUm9HRteAlTGBLiA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@oxc-project/types": "=0.139.0",
+ "@rolldown/pluginutils": "^1.0.0"
+ },
+ "bin": {
+ "rolldown": "bin/cli.mjs"
+ },
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ },
+ "optionalDependencies": {
+ "@rolldown/binding-android-arm64": "1.1.5",
+ "@rolldown/binding-darwin-arm64": "1.1.5",
+ "@rolldown/binding-darwin-x64": "1.1.5",
+ "@rolldown/binding-freebsd-x64": "1.1.5",
+ "@rolldown/binding-linux-arm-gnueabihf": "1.1.5",
+ "@rolldown/binding-linux-arm64-gnu": "1.1.5",
+ "@rolldown/binding-linux-arm64-musl": "1.1.5",
+ "@rolldown/binding-linux-ppc64-gnu": "1.1.5",
+ "@rolldown/binding-linux-s390x-gnu": "1.1.5",
+ "@rolldown/binding-linux-x64-gnu": "1.1.5",
+ "@rolldown/binding-linux-x64-musl": "1.1.5",
+ "@rolldown/binding-openharmony-arm64": "1.1.5",
+ "@rolldown/binding-wasm32-wasi": "1.1.5",
+ "@rolldown/binding-win32-arm64-msvc": "1.1.5",
+ "@rolldown/binding-win32-x64-msvc": "1.1.5"
+ }
+ },
+ "node_modules/scheduler": {
+ "version": "0.27.0",
+ "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.27.0.tgz",
+ "integrity": "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==",
+ "license": "MIT"
+ },
+ "node_modules/semver": {
+ "version": "7.8.5",
+ "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz",
+ "integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==",
+ "dev": true,
+ "license": "ISC",
+ "bin": {
+ "semver": "bin/semver.js"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/shebang-command": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz",
+ "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "shebang-regex": "^3.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/shebang-regex": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz",
+ "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/source-map-js": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
+ "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==",
+ "dev": true,
+ "license": "BSD-3-Clause",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/tailwind-merge": {
+ "version": "3.6.0",
+ "resolved": "https://registry.npmjs.org/tailwind-merge/-/tailwind-merge-3.6.0.tgz",
+ "integrity": "sha512-uxL7qAVQriqRQPAyK3pj66VqskWqoZ37PW94jwOTwNfq/z9oyu1V+eqrZqtR2+fCiXdYOZe/Modt8GtvqNzu+w==",
+ "license": "MIT",
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/dcastil"
+ }
+ },
+ "node_modules/tailwindcss": {
+ "version": "4.3.2",
+ "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.3.2.tgz",
+ "integrity": "sha512-WtctNNSH8A9jlMIqxzuYumOHU5uGZyRv0Q5svQl+oEPy5w84YpBxdb7MdqyiSPQge5jTJ6zFQLq0PFygdccSBA==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/tapable": {
+ "version": "2.3.3",
+ "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.3.3.tgz",
+ "integrity": "sha512-uxc/zpqFg6x7C8vOE7lh6Lbda8eEL9zmVm/PLeTPBRhh1xCgdWaQ+J1CUieGpIfm2HdtsUpRv+HshiasBMcc6A==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/webpack"
+ }
+ },
+ "node_modules/tinyglobby": {
+ "version": "0.2.17",
+ "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.17.tgz",
+ "integrity": "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "fdir": "^6.5.0",
+ "picomatch": "^4.0.4"
+ },
+ "engines": {
+ "node": ">=12.0.0"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/SuperchupuDev"
+ }
+ },
+ "node_modules/ts-api-utils": {
+ "version": "2.5.0",
+ "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.5.0.tgz",
+ "integrity": "sha512-OJ/ibxhPlqrMM0UiNHJ/0CKQkoKF243/AEmplt3qpRgkW8VG7IfOS41h7V8TjITqdByHzrjcS/2si+y4lIh8NA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=18.12"
+ },
+ "peerDependencies": {
+ "typescript": ">=4.8.4"
+ }
+ },
+ "node_modules/tslib": {
+ "version": "2.8.1",
+ "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
+ "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==",
+ "license": "0BSD"
+ },
+ "node_modules/type-check": {
+ "version": "0.4.0",
+ "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz",
+ "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "prelude-ls": "^1.2.1"
+ },
+ "engines": {
+ "node": ">= 0.8.0"
+ }
+ },
+ "node_modules/typescript": {
+ "version": "6.0.3",
+ "resolved": "https://registry.npmjs.org/typescript/-/typescript-6.0.3.tgz",
+ "integrity": "sha512-y2TvuxSZPDyQakkFRPZHKFm+KKVqIisdg9/CZwm9ftvKXLP8NRWj38/ODjNbr43SsoXqNuAisEf1GdCxqWcdBw==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "bin": {
+ "tsc": "bin/tsc",
+ "tsserver": "bin/tsserver"
+ },
+ "engines": {
+ "node": ">=14.17"
+ }
+ },
+ "node_modules/typescript-eslint": {
+ "version": "8.63.0",
+ "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.63.0.tgz",
+ "integrity": "sha512-xgwXyzG4sK9ALkBxbyGkTMMOS+imnW65iPhxCQMK83KhxyoDNW7l+IDqEf9vMdoUidHpOoS967RCq4eMiTexwQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@typescript-eslint/eslint-plugin": "8.63.0",
+ "@typescript-eslint/parser": "8.63.0",
+ "@typescript-eslint/typescript-estree": "8.63.0",
+ "@typescript-eslint/utils": "8.63.0"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ },
+ "peerDependencies": {
+ "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0",
+ "typescript": ">=4.8.4 <6.1.0"
+ }
+ },
+ "node_modules/update-browserslist-db": {
+ "version": "1.2.3",
+ "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz",
+ "integrity": "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/browserslist"
+ },
+ {
+ "type": "tidelift",
+ "url": "https://tidelift.com/funding/github/npm/browserslist"
+ },
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "escalade": "^3.2.0",
+ "picocolors": "^1.1.1"
+ },
+ "bin": {
+ "update-browserslist-db": "cli.js"
+ },
+ "peerDependencies": {
+ "browserslist": ">= 4.21.0"
+ }
+ },
+ "node_modules/uri-js": {
+ "version": "4.4.1",
+ "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz",
+ "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==",
+ "dev": true,
+ "license": "BSD-2-Clause",
+ "dependencies": {
+ "punycode": "^2.1.0"
+ }
+ },
+ "node_modules/vite": {
+ "version": "8.1.4",
+ "resolved": "https://registry.npmjs.org/vite/-/vite-8.1.4.tgz",
+ "integrity": "sha512-bTT9PsdWO+MQMNG9ZXIP/qM9wGh37DFxTV/sPq9cFpHr3w4jkgef032PkAL9jAqhk3Nz8NQw3O8n6/xFkqO4QQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "lightningcss": "^1.32.0",
+ "picomatch": "^4.0.5",
+ "postcss": "^8.5.16",
+ "rolldown": "~1.1.4",
+ "tinyglobby": "^0.2.17"
+ },
+ "bin": {
+ "vite": "bin/vite.js"
+ },
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ },
+ "funding": {
+ "url": "https://github.com/vitejs/vite?sponsor=1"
+ },
+ "optionalDependencies": {
+ "fsevents": "~2.3.3"
+ },
+ "peerDependencies": {
+ "@types/node": "^20.19.0 || >=22.12.0",
+ "@vitejs/devtools": "^0.3.0",
+ "esbuild": "^0.27.0 || ^0.28.0",
+ "jiti": ">=1.21.0",
+ "less": "^4.0.0",
+ "sass": "^1.70.0",
+ "sass-embedded": "^1.70.0",
+ "stylus": ">=0.54.8",
+ "sugarss": "^5.0.0",
+ "terser": "^5.16.0",
+ "tsx": "^4.8.1",
+ "yaml": "^2.4.2"
+ },
+ "peerDependenciesMeta": {
+ "@types/node": {
+ "optional": true
+ },
+ "@vitejs/devtools": {
+ "optional": true
+ },
+ "esbuild": {
+ "optional": true
+ },
+ "jiti": {
+ "optional": true
+ },
+ "less": {
+ "optional": true
+ },
+ "sass": {
+ "optional": true
+ },
+ "sass-embedded": {
+ "optional": true
+ },
+ "stylus": {
+ "optional": true
+ },
+ "sugarss": {
+ "optional": true
+ },
+ "terser": {
+ "optional": true
+ },
+ "tsx": {
+ "optional": true
+ },
+ "yaml": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/wavesurfer.js": {
+ "version": "7.12.10",
+ "resolved": "https://registry.npmjs.org/wavesurfer.js/-/wavesurfer.js-7.12.10.tgz",
+ "integrity": "sha512-M3AC5biFmvfy7Oxe8FN6I88H0z1c8Vuz81N6Oq2CY4kOumNDb6U1/0qg4pomjP619vJqdWADlgUcpOKvfGifng==",
+ "license": "BSD-3-Clause"
+ },
+ "node_modules/which": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
+ "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "isexe": "^2.0.0"
+ },
+ "bin": {
+ "node-which": "bin/node-which"
+ },
+ "engines": {
+ "node": ">= 8"
+ }
+ },
+ "node_modules/word-wrap": {
+ "version": "1.2.5",
+ "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz",
+ "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/yallist": {
+ "version": "3.1.1",
+ "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz",
+ "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==",
+ "dev": true,
+ "license": "ISC"
+ },
+ "node_modules/yocto-queue": {
+ "version": "0.1.0",
+ "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz",
+ "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/zod": {
+ "version": "4.4.3",
+ "resolved": "https://registry.npmjs.org/zod/-/zod-4.4.3.tgz",
+ "integrity": "sha512-ytENFjIJFl2UwYglde2jchW2Hwm4GJFLDiSXWdTrJQBIN9Fcyp7n4DhxJEiWNAJMV1/BqWfW/kkg71UDcHJyTQ==",
+ "dev": true,
+ "license": "MIT",
+ "funding": {
+ "url": "https://github.com/sponsors/colinhacks"
+ }
+ },
+ "node_modules/zod-validation-error": {
+ "version": "4.0.2",
+ "resolved": "https://registry.npmjs.org/zod-validation-error/-/zod-validation-error-4.0.2.tgz",
+ "integrity": "sha512-Q6/nZLe6jxuU80qb/4uJ4t5v2VEZ44lzQjPDhYJNztRQ4wyWc6VF3D3Kb/fAuPetZQnhS3hnajCf9CsWesghLQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=18.0.0"
+ },
+ "peerDependencies": {
+ "zod": "^3.25.0 || ^4.0.0"
+ }
+ }
+ }
+}
diff --git a/mofa-frontend/package.json b/mofa-frontend/package.json
new file mode 100644
index 0000000..560804b
--- /dev/null
+++ b/mofa-frontend/package.json
@@ -0,0 +1,43 @@
+{
+ "name": "applet",
+ "version": "1.0.0",
+ "type": "module",
+ "description": "",
+ "main": "index.js",
+ "scripts": {
+ "dev": "vite --port 3000 --host 0.0.0.0",
+ "build": "tsc && vite build",
+ "lint": "eslint ."
+ },
+ "keywords": [],
+ "author": "",
+ "license": "ISC",
+ "dependencies": {
+ "@tanstack/react-query": "^5.101.2",
+ "clsx": "^2.1.1",
+ "framer-motion": "^12.42.2",
+ "lucide-react": "^1.23.0",
+ "react": "^19.2.7",
+ "react-dom": "^19.2.7",
+ "tailwind-merge": "^3.6.0",
+ "wavesurfer.js": "^7.12.10"
+ },
+ "devDependencies": {
+ "@eslint/js": "^10.0.1",
+ "@tailwindcss/postcss": "^4.3.2",
+ "@types/react": "^19.2.17",
+ "@types/react-dom": "^19.2.3",
+ "@typescript-eslint/eslint-plugin": "^8.63.0",
+ "@typescript-eslint/parser": "^8.63.0",
+ "@vitejs/plugin-react": "^6.0.3",
+ "eslint": "^10.6.0",
+ "eslint-plugin-react-hooks": "^7.1.1",
+ "eslint-plugin-react-refresh": "^0.5.3",
+ "globals": "^17.7.0",
+ "postcss": "^8.5.16",
+ "tailwindcss": "^4.3.2",
+ "typescript": "^6.0.3",
+ "typescript-eslint": "^8.63.0",
+ "vite": "^8.1.4"
+ }
+}
diff --git a/mofa-frontend/postcss.config.mjs b/mofa-frontend/postcss.config.mjs
new file mode 100644
index 0000000..ef6af3e
--- /dev/null
+++ b/mofa-frontend/postcss.config.mjs
@@ -0,0 +1,5 @@
+export default {
+ plugins: {
+ '@tailwindcss/postcss': {},
+ }
+}
diff --git a/mofa-frontend/public/favicon.svg b/mofa-frontend/public/favicon.svg
new file mode 100644
index 0000000..95eaf7b
--- /dev/null
+++ b/mofa-frontend/public/favicon.svg
@@ -0,0 +1,15 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/mofa-frontend/public/mofa-logo.png b/mofa-frontend/public/mofa-logo.png
new file mode 100644
index 0000000..8eeb2cb
Binary files /dev/null and b/mofa-frontend/public/mofa-logo.png differ
diff --git a/mofa-frontend/src/App.tsx b/mofa-frontend/src/App.tsx
new file mode 100644
index 0000000..b383b88
--- /dev/null
+++ b/mofa-frontend/src/App.tsx
@@ -0,0 +1,81 @@
+import React, { useState, useEffect } from 'react';
+import { Studio } from './studio/Studio';
+import { TopBar } from './shell/TopBar';
+import { useEngineConnection } from './engine/useEngineConnection';
+import { EngineOffline } from './errors/EngineOffline';
+import { MonitorSidebar } from './monitor/MonitorSidebar';
+import { HistoryDrawer } from './storage/HistoryDrawer';
+import { CommandPalette } from './shell/CommandPalette';
+import { AnimatePresence, motion, MotionConfig } from 'framer-motion';
+import { useSettings } from './storage/useSettings';
+import { ObservabilityView } from './observability/ObservabilityView';
+
+class ErrorBoundary extends React.Component<{children: React.ReactNode}, {hasError: boolean}> {
+ constructor(props: {children: React.ReactNode}) {
+ super(props);
+ this.state = { hasError: false };
+ }
+ static getDerivedStateFromError(_error: any) {
+ return { hasError: true };
+ }
+ render() {
+ if (this.state.hasError) {
+ return (
+
+
+
Something went wrong
+
An unexpected error occurred in the application.
+
window.location.reload()}
+ className="px-4 py-2 bg-accent-cyan text-white rounded font-medium text-sm hover:bg-accent-cyan/90 transition-colors"
+ >
+ Reload Application
+
+
+
+ );
+ }
+ return this.props.children;
+ }
+}
+
+export default function App() {
+ const { state } = useEngineConnection();
+ const { settings } = useSettings();
+ const [currentView, setCurrentView] = useState<'studio' | 'observability'>('studio');
+
+ useEffect(() => {
+ const handleNavigate = (e: any) => setCurrentView(e.detail);
+ document.addEventListener('navigate', handleNavigate);
+ return () => document.removeEventListener('navigate', handleNavigate);
+ }, []);
+
+ return (
+
+
+
+
+ {currentView === 'studio' ? (
+
+
+
+
+ ) : (
+
+ )}
+
+
+
+
+ {state === 'disconnected' && }
+
+
+
+
+ );
+}
diff --git a/mofa-frontend/src/engine/client.ts b/mofa-frontend/src/engine/client.ts
new file mode 100644
index 0000000..eec03da
--- /dev/null
+++ b/mofa-frontend/src/engine/client.ts
@@ -0,0 +1,115 @@
+import {
+ HealthResponse,
+ EngineStatus,
+ ModelCard,
+ InferenceRequest,
+ InferenceResponse,
+ EngineResult,
+ EngineEvent
+} from './types';
+import { IEngineClient } from './index';
+
+export class RealEngineClient implements IEngineClient {
+ private baseUrl: string;
+
+ constructor(baseUrl: string) {
+ this.baseUrl = baseUrl.replace(/\/$/, '');
+ }
+
+ setBaseUrl(url: string) {
+ this.baseUrl = url.replace(/\/$/, '');
+ }
+
+ getBaseUrl() {
+ return this.baseUrl;
+ }
+
+ private async request(path: string, options?: RequestInit): Promise> {
+ try {
+ const response = await fetch(`${this.baseUrl}${path}`, {
+ ...options,
+ headers: {
+ 'Content-Type': 'application/json',
+ ...(options?.headers || {})
+ }
+ });
+
+ if (!response.ok) {
+ let detail = response.statusText;
+ let error = `HTTP ${response.status}`;
+ try {
+ const body = await response.json();
+ if (body.error) error = body.error;
+ if (body.detail) detail = body.detail;
+ } catch {
+ // ignore parsing error if it's not JSON
+ }
+ return { success: false, type: 'http', error, detail };
+ }
+
+ const data = await response.json();
+ return { success: true, data };
+ } catch (e) {
+ return { success: false, type: 'network', error: 'Engine unreachable', detail: (e as Error).message };
+ }
+ }
+
+ async getHealth() {
+ return this.request('/health');
+ }
+
+ async getStatus() {
+ return this.request('/v1/status');
+ }
+
+ async getCapabilities() {
+ return this.request('/v1/capabilities');
+ }
+
+ async invoke(payload: InferenceRequest) {
+ return this.request('/v1/invoke', {
+ method: 'POST',
+ body: JSON.stringify(payload)
+ });
+ }
+
+ async refreshDiscovery() {
+ return this.request('/v1/discovery/refresh', {
+ method: 'POST'
+ });
+ }
+
+ subscribeEvents(handler: (e: EngineEvent) => void): () => void {
+ const eventSource = new EventSource(`${this.baseUrl}/v1/events`);
+
+ eventSource.onmessage = (event) => {
+ try {
+ const raw = JSON.parse(event.data);
+ // Real engine uses serde tag="type" rename_all="snake_case"
+ // so type comes as "request_started", "model_status_changed", etc.
+ const snakeType: string = raw.type || '';
+ // Convert snake_case to PascalCase: "request_started" → "RequestStarted"
+ const pascalType = snakeType.replace(/(^|_)([a-z])/g, (_: string, __: string, c: string) => c.toUpperCase());
+ // Everything except 'type' goes into data
+ const { type: _, ...data } = raw;
+ handler({ type: pascalType as any, data, timestamp: Date.now() });
+ } catch (e) {
+ console.error("Failed to parse SSE", e);
+ }
+ };
+
+ return () => {
+ eventSource.close();
+ };
+ }
+
+ getAudioUrl(filename: string): string {
+ return `${this.baseUrl}/v1/files/${filename}`;
+ }
+
+ async fetchAudio(filename: string): Promise {
+ const res = await fetch(this.getAudioUrl(filename));
+ if (!res.ok) throw new Error(`Failed to fetch audio: ${res.statusText}`);
+ return res.blob();
+ }
+}
diff --git a/mofa-frontend/src/engine/index.ts b/mofa-frontend/src/engine/index.ts
new file mode 100644
index 0000000..c994705
--- /dev/null
+++ b/mofa-frontend/src/engine/index.ts
@@ -0,0 +1,43 @@
+import { HealthResponse, EngineStatus, ModelCard, InferenceRequest, InferenceResponse, EngineResult, EngineEvent } from './types';
+import { RealEngineClient } from './client';
+export interface IEngineClient {
+ getBaseUrl(): string;
+ setBaseUrl(url: string): void;
+ getHealth(): Promise>;
+ getStatus(): Promise>;
+ getCapabilities(): Promise>;
+ invoke(req: InferenceRequest): Promise>;
+ refreshDiscovery(): Promise>;
+ subscribeEvents(handler: (e: EngineEvent) => void): () => void;
+ getAudioUrl(filename: string): string;
+ fetchAudio(filename: string): Promise;
+}
+
+export const defaultEngineUrl = import.meta.env.VITE_ENGINE_URL || 'http://127.0.0.1:8420';
+
+export const getStoredEngineUrl = () => {
+ return localStorage.getItem('mofa_engine_url') || defaultEngineUrl;
+};
+
+import { useState, useEffect } from 'react';
+
+type Listener = () => void;
+const listeners = new Set();
+
+export const setStoredEngineUrl = (url: string) => {
+ localStorage.setItem('mofa_engine_url', url);
+ engine.setBaseUrl(url);
+ listeners.forEach(l => l());
+};
+
+export const useEngineUrl = () => {
+ const [url, setUrl] = useState(getStoredEngineUrl());
+ useEffect(() => {
+ const listener = () => setUrl(getStoredEngineUrl());
+ listeners.add(listener);
+ return () => { listeners.delete(listener); };
+ }, []);
+ return url;
+};
+
+export const engine: IEngineClient = new RealEngineClient(getStoredEngineUrl());
diff --git a/mofa-frontend/src/engine/types.ts b/mofa-frontend/src/engine/types.ts
new file mode 100644
index 0000000..fee392a
--- /dev/null
+++ b/mofa-frontend/src/engine/types.ts
@@ -0,0 +1,97 @@
+export type Capability = 'Chat' | 'Tts' | 'Asr' | 'ImageGen' | 'VideoGen' | 'Vlm' | 'Embedding' | 'chat' | 'tts' | 'asr' | 'imagegen' | 'image_gen' | 'videogen' | 'video_gen' | 'vlm' | 'embedding';
+export type ModelStatus = 'Cold' | 'Warming' | 'Hot' | 'Busy' | 'Failed' | 'cold' | 'warming' | 'hot' | 'busy' | 'failed';
+export type ModelResidency = 'Unknown' | 'Unloaded' | 'Loading' | 'Loaded' | 'Unloading' | 'Remote' | 'unknown' | 'unloaded' | 'loading' | 'loaded' | 'unloading' | 'remote';
+export type ModelAvailability = 'Discovered' | 'Configured' | 'Unavailable' | 'discovered' | 'configured' | 'unavailable';
+export type ExecutionState = 'Idle' | 'Active' | 'Overloaded' | 'idle' | 'active' | 'overloaded';
+export type CostTier = 'Free' | 'Low' | 'Medium' | 'High' | 'free' | 'low' | 'medium' | 'high';
+export type CircuitState = 'Closed' | 'Open' | 'HalfOpen' | 'closed' | 'open' | 'half_open' | 'half-open';
+export type BackendHealth = 'Healthy' | 'Degraded' | 'Unhealthy' | 'healthy' | 'degraded' | 'unhealthy';
+export type FallbackPolicy = 'capability_only' | 'disabled' | 'allow_named';
+
+
+export interface HealthResponse {
+ status: string;
+ version: string;
+ uptime_secs: number;
+}
+
+export interface ProviderHealth {
+ name: string;
+ healthy: boolean;
+ circuit_state: CircuitState;
+}
+
+export interface Backend {
+ name: string;
+ kind: string;
+ health: BackendHealth;
+ circuit_state: CircuitState;
+ features: string[];
+}
+
+export interface EngineStatus {
+ total_models: number;
+ loaded_models: number;
+ providers: number;
+ memory_used_bytes: number;
+ memory_budget_bytes: number;
+ uptime_secs: number;
+ provider_health: ProviderHealth[];
+ backends: Backend[];
+}
+
+export interface ModelCard {
+ id: string;
+ name: string;
+ provider: string;
+ capability: Capability;
+ capabilities: Capability[];
+ status: ModelStatus;
+ availability: ModelAvailability;
+ residency: ModelResidency;
+ execution: ExecutionState;
+ cost_tier: CostTier;
+ context_window?: number;
+ memory_estimate_bytes?: number;
+}
+
+export interface InferenceRequest {
+ capability: Capability;
+ model: string | null;
+ messages: Array<{ role: string; content: string }>;
+ hint_next?: string;
+ params: Record;
+ app_id: string;
+ session_id: string | null;
+ fallback_policy: FallbackPolicy;
+}
+
+export interface InferenceResponse {
+ text: string;
+ file: string | null;
+ model_used: string;
+ provider: string;
+ duration_ms: number;
+ request_id: string;
+ tokens_used: number;
+ fallback_used: boolean;
+ routing_reason: string;
+ candidates_considered?: number;
+}
+
+export interface EngineError {
+ error: string;
+ detail: string;
+}
+
+export type EngineResult =
+ | { success: true; data: T }
+ | { success: false; type: 'network' | 'http'; error: string; detail?: string };
+
+export type EngineEventType = 'RequestStarted' | 'RequestCompleted' | 'ModelStatusChanged' | 'ModelResidencyChanged' | 'MemoryChanged' | 'ProviderHealthChanged' | 'DiscoveryCompleted';
+
+export interface EngineEvent {
+ type: EngineEventType;
+ data: any;
+ timestamp: number;
+}
diff --git a/mofa-frontend/src/engine/useEngineConnection.ts b/mofa-frontend/src/engine/useEngineConnection.ts
new file mode 100644
index 0000000..305142a
--- /dev/null
+++ b/mofa-frontend/src/engine/useEngineConnection.ts
@@ -0,0 +1,28 @@
+import { useQuery } from '@tanstack/react-query';
+import { engine, useEngineUrl } from './index';
+
+export type ConnectionState = 'connecting' | 'connected' | 'disconnected';
+
+export function useEngineConnection() {
+ const url = useEngineUrl();
+ const { data, error, isFetching } = useQuery({
+ queryKey: ['engine_health', url],
+ queryFn: () => engine.getHealth(),
+ refetchInterval: 3000,
+ retry: false,
+ });
+
+ let state: ConnectionState = 'connecting';
+ if (data?.success) {
+ state = 'connected';
+ } else if (data?.success === false || error) {
+ state = 'disconnected';
+ }
+
+ return {
+ state,
+ version: data?.success ? data.data.version : null,
+ uptime_secs: data?.success ? data.data.uptime_secs : null,
+ isFetching
+ };
+}
diff --git a/mofa-frontend/src/engine/useEngineStatus.ts b/mofa-frontend/src/engine/useEngineStatus.ts
new file mode 100644
index 0000000..3328fc9
--- /dev/null
+++ b/mofa-frontend/src/engine/useEngineStatus.ts
@@ -0,0 +1,15 @@
+import { useQuery } from '@tanstack/react-query';
+import { engine, useEngineUrl } from './index';
+import { useEngineConnection } from './useEngineConnection';
+
+export function useEngineStatus() {
+ const { state } = useEngineConnection();
+ const url = useEngineUrl();
+
+ return useQuery({
+ queryKey: ['engine_status', url],
+ queryFn: () => engine.getStatus(),
+ refetchInterval: 2000,
+ enabled: state === 'connected',
+ });
+}
diff --git a/mofa-frontend/src/errors/EngineOffline.tsx b/mofa-frontend/src/errors/EngineOffline.tsx
new file mode 100644
index 0000000..c9fc3ce
--- /dev/null
+++ b/mofa-frontend/src/errors/EngineOffline.tsx
@@ -0,0 +1,60 @@
+import React, { useState } from 'react';
+import { motion } from 'framer-motion';
+import { Card } from '../shared/Card';
+import { Button } from '../shared/Button';
+import { RefreshCw, ZapOff, CheckCircle2, Copy } from 'lucide-react';
+import { useEngineUrl } from '../engine';
+
+export function EngineOffline() {
+ const url = useEngineUrl();
+ const [copied, setCopied] = useState(false);
+
+ const handleCopy = () => {
+ navigator.clipboard.writeText('cargo run --bin mofa-engine');
+ setCopied(true);
+ setTimeout(() => setCopied(false), 2000);
+ };
+
+ return (
+
+
+
+
+
+
+ Can't reach the MoFA engine
+
+ The engine at {url} is offline.
+
+
+
+
Start the engine
+
+
+ cargo run --bin mofa-engine
+
+
+ {copied ? : }
+
+
+
+
+
+ window.location.reload()}>
+
+ Retry now
+
+
+
+
+ );
+}
diff --git a/mofa-frontend/src/errors/InlineError.tsx b/mofa-frontend/src/errors/InlineError.tsx
new file mode 100644
index 0000000..da43321
--- /dev/null
+++ b/mofa-frontend/src/errors/InlineError.tsx
@@ -0,0 +1,98 @@
+import { useEffect, useState } from 'react';
+import { motion } from 'framer-motion';
+import { Card } from '../shared/Card';
+import { Button } from '../shared/Button';
+import { XCircle, Copy, CheckCircle2, RotateCcw } from 'lucide-react';
+import { ErrorDescriptor } from './errorCatalog';
+
+interface InlineErrorProps {
+ descriptor: ErrorDescriptor;
+ onAction: (actionCode: string) => void;
+}
+
+export function InlineError({ descriptor, onAction }: InlineErrorProps) {
+ const [copied, setCopied] = useState(false);
+ const [countdown, setCountdown] = useState(
+ descriptor.autoRecoverMs ? Math.ceil(descriptor.autoRecoverMs / 1000) : null
+ );
+
+ useEffect(() => {
+ if (descriptor.autoRecoverMs) {
+ const interval = setInterval(() => {
+ setCountdown((c) => {
+ if (c === null || c <= 1) {
+ clearInterval(interval);
+ onAction(descriptor.actionCode);
+ return null;
+ }
+ return c - 1;
+ });
+ }, 1000);
+ return () => clearInterval(interval);
+ }
+ }, [descriptor, onAction]);
+
+ const handleCopy = () => {
+ if (descriptor.snippet) {
+ navigator.clipboard.writeText(descriptor.snippet);
+ setCopied(true);
+ setTimeout(() => setCopied(false), 2000);
+ }
+ };
+
+ return (
+
+
+
+
+
+
+
+
+
+ {descriptor.title || 'Error'}
+
+
+ {descriptor.message}
+
+
+ {descriptor.snippet && (
+
+
+ {descriptor.snippet}
+
+
+ {copied ? : }
+
+
+ )}
+
+
+ onAction(descriptor.actionCode)}
+ className="bg-white/5 hover:bg-white/10 border-white/10"
+ >
+ {descriptor.actionLabel}
+
+ {countdown !== null && (
+
+
+ Retrying in {countdown}s...
+
+ )}
+
+
+
+
+
+ );
+}
diff --git a/mofa-frontend/src/errors/errorCatalog.ts b/mofa-frontend/src/errors/errorCatalog.ts
new file mode 100644
index 0000000..3f458ba
--- /dev/null
+++ b/mofa-frontend/src/errors/errorCatalog.ts
@@ -0,0 +1,57 @@
+export interface ErrorDescriptor {
+ message: string;
+ actionLabel: string;
+ actionCode: 'retry' | 'retryTts' | 'editScript';
+ snippet?: string;
+ title?: string;
+ autoRecoverMs?: number;
+}
+
+export function getErrorDescriptor(capability: string, errorType?: string): ErrorDescriptor {
+ if (errorType === 'NoCapableModel') {
+ if (capability === 'Chat') {
+ return {
+ title: 'No Chat Model',
+ message: 'No chat model available. Is Ollama running?',
+ actionLabel: 'Back to start',
+ actionCode: 'editScript',
+ snippet: 'ollama serve'
+ };
+ } else {
+ return {
+ title: 'No TTS Model',
+ message: 'No TTS model available. Is Kokoro running on port 8421?',
+ actionLabel: 'Back to start',
+ actionCode: 'editScript',
+ snippet: 'docker run -p 8421:8421 ghcr.io/replicate/kokoro'
+ };
+ }
+ }
+
+ if (errorType === 'CircuitOpen') {
+ return {
+ title: 'Provider offline',
+ message: 'The TTS provider is recovering. This usually resolves in ~30 seconds.',
+ actionLabel: 'Retry now',
+ actionCode: 'retryTts',
+ autoRecoverMs: 4000
+ };
+ }
+
+ if (errorType === 'Timeout') {
+ return {
+ title: 'Timeout',
+ message: 'The model is taking longer than expected — it may still be loading.',
+ actionLabel: 'Retry now',
+ actionCode: 'retryTts',
+ autoRecoverMs: 4000
+ };
+ }
+
+ return {
+ title: 'Error',
+ message: 'An unexpected error occurred.',
+ actionLabel: 'Try again',
+ actionCode: 'retry',
+ };
+}
diff --git a/mofa-frontend/src/index.css b/mofa-frontend/src/index.css
new file mode 100644
index 0000000..bc14f65
--- /dev/null
+++ b/mofa-frontend/src/index.css
@@ -0,0 +1,36 @@
+@import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600&family=JetBrains+Mono:wght@400;500&display=swap');
+@import "tailwindcss";
+
+@theme {
+ --color-background-primary: #ffffff;
+ --color-background-secondary: #f4f4f5;
+
+ --color-text-primary: #09090b;
+ --color-text-secondary: #52525b;
+ --color-text-dim: #a1a1aa;
+
+ --color-accent-blue: #2563eb;
+ --color-accent-cyan: #0891b2;
+ --color-accent-green: #059669;
+ --color-accent-yellow: #d97706;
+ --color-accent-red: #dc2626;
+ --color-accent-purple: #7c3aed;
+
+ --radius-card: 10px;
+ --radius-small: 6px;
+
+ --font-sans: "Inter", ui-sans-serif, system-ui, sans-serif;
+ --font-mono: "JetBrains Mono", ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
+}
+
+@layer base {
+ body {
+ @apply bg-background-primary text-text-primary font-sans antialiased;
+ margin: 0;
+ }
+
+ *:focus-visible {
+ @apply outline-none ring-2 ring-accent-cyan ring-offset-2 ring-offset-background-primary;
+ }
+}
+
diff --git a/mofa-frontend/src/lib/audio.ts b/mofa-frontend/src/lib/audio.ts
new file mode 100644
index 0000000..ff3af24
--- /dev/null
+++ b/mofa-frontend/src/lib/audio.ts
@@ -0,0 +1,23 @@
+export function playChime() {
+ try {
+ const ctx = new (window.AudioContext || (window as any).webkitAudioContext)();
+ const osc = ctx.createOscillator();
+ const gainNode = ctx.createGain();
+
+ osc.type = 'sine';
+ osc.frequency.setValueAtTime(523.25, ctx.currentTime); // C5
+ osc.frequency.exponentialRampToValueAtTime(1046.50, ctx.currentTime + 0.1); // C6
+
+ gainNode.gain.setValueAtTime(0, ctx.currentTime);
+ gainNode.gain.linearRampToValueAtTime(0.3, ctx.currentTime + 0.05);
+ gainNode.gain.exponentialRampToValueAtTime(0.001, ctx.currentTime + 1.5);
+
+ osc.connect(gainNode);
+ gainNode.connect(ctx.destination);
+
+ osc.start();
+ osc.stop(ctx.currentTime + 1.5);
+ } catch {
+ // Ignore if audio context fails
+ }
+}
diff --git a/mofa-frontend/src/lib/motion.ts b/mofa-frontend/src/lib/motion.ts
new file mode 100644
index 0000000..600eb1f
--- /dev/null
+++ b/mofa-frontend/src/lib/motion.ts
@@ -0,0 +1,26 @@
+export const motionVariants: any = {
+ enter: {
+ initial: { opacity: 0 },
+ animate: { opacity: 1, transition: { duration: 0.4, ease: [0.25, 0.1, 0.25, 1] } },
+ exit: { opacity: 0, transition: { duration: 0.3, ease: 'easeIn' } }
+ },
+ fade: {
+ initial: { opacity: 0 },
+ animate: { opacity: 1, transition: { duration: 0.3, ease: [0.25, 0.1, 0.25, 1] } },
+ exit: { opacity: 0, transition: { duration: 0.2, ease: 'easeIn' } }
+ },
+ staggerContainer: {
+ animate: {
+ transition: {
+ staggerChildren: 0.04
+ }
+ }
+ },
+ staggerItem: {
+ initial: { opacity: 0 },
+ animate: { opacity: 1, transition: { duration: 0.3, ease: 'easeOut' } }
+ },
+ buttonTap: {
+ scale: 0.98
+ }
+};
diff --git a/mofa-frontend/src/lib/utils.ts b/mofa-frontend/src/lib/utils.ts
new file mode 100644
index 0000000..ce9a19e
--- /dev/null
+++ b/mofa-frontend/src/lib/utils.ts
@@ -0,0 +1,11 @@
+import { clsx, type ClassValue } from 'clsx';
+import { twMerge } from 'tailwind-merge';
+
+export function cn(...inputs: ClassValue[]) {
+ return twMerge(clsx(inputs));
+}
+
+
+export function formatMs(ms: number) {
+ return `${(ms / 1000).toFixed(1)}s`;
+}
diff --git a/mofa-frontend/src/main.tsx b/mofa-frontend/src/main.tsx
new file mode 100644
index 0000000..9d15adc
--- /dev/null
+++ b/mofa-frontend/src/main.tsx
@@ -0,0 +1,15 @@
+import React from 'react';
+import ReactDOM from 'react-dom/client';
+import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
+import App from './App';
+import './index.css';
+
+const queryClient = new QueryClient();
+
+ReactDOM.createRoot(document.getElementById('root') as HTMLElement).render(
+
+
+
+
+
+);
diff --git a/mofa-frontend/src/monitor/EventFeed.tsx b/mofa-frontend/src/monitor/EventFeed.tsx
new file mode 100644
index 0000000..785dea9
--- /dev/null
+++ b/mofa-frontend/src/monitor/EventFeed.tsx
@@ -0,0 +1,131 @@
+import React, { useEffect, useState } from 'react';
+import { engine } from '../engine';
+import { EngineEvent } from '../engine/types';
+import { Activity, CheckCircle2, Cpu, Loader, Box, RotateCw } from 'lucide-react';
+import { motion, AnimatePresence } from 'framer-motion';
+
+export function EventFeed() {
+ const [events, setEvents] = useState<(EngineEvent & { _id: string })[]>([]);
+
+ useEffect(() => {
+ let counter = 0;
+ const handleEvent = (evt: EngineEvent) => {
+ setEvents(prev => {
+ const newEvt = { ...evt, _id: `${evt.timestamp}-${counter++}` };
+ const next = [newEvt, ...prev]; // Prepend for reverse chronological
+ if (next.length > 15) return next.slice(0, 15);
+ return next;
+ });
+ };
+
+ const unsubscribe = engine.subscribeEvents(handleEvent);
+ return () => unsubscribe();
+ }, []);
+
+ return (
+
+
+
+ {events.map((evt) => (
+
+ ))}
+
+ {events.length === 0 && (
+
Waiting for engine events...
+ )}
+
+
+
+
+ );
+}
+
+function EventCard({ evt }: { evt: EngineEvent }) {
+ const { icon, color, title, desc } = formatEvent(evt);
+
+ return (
+
+
+ {icon}
+
+
+
+ {title}
+
+ {new Date(evt.timestamp).toLocaleTimeString([], { hour12: false, hour: '2-digit', minute: '2-digit', second:'2-digit' })}
+
+
+
+ {desc}
+
+
+
+ );
+}
+
+function formatEvent(evt: EngineEvent) {
+ switch (evt.type) {
+ case 'RequestStarted':
+ return {
+ icon: ,
+ color: 'accent-blue',
+ title: 'Request Started',
+ desc: `${evt.data.capability || 'inference'} → ${evt.data.model_id}`
+ };
+ case 'RequestCompleted':
+ return {
+ icon: ,
+ color: evt.data.success ? 'accent-green' : 'accent-red',
+ title: evt.data.success ? 'Request Completed' : 'Request Failed',
+ desc: `${evt.data.request_id?.slice(0, 8)}… ${evt.data.duration_ms}ms`
+ };
+ case 'ModelStatusChanged':
+ return {
+ icon: ,
+ color: 'accent-yellow',
+ title: 'Model Status',
+ desc: `${evt.data.model_id}: ${evt.data.old} → ${evt.data.new}`
+ };
+ case 'ModelResidencyChanged':
+ return {
+ icon: ,
+ color: 'accent-cyan',
+ title: 'Model Residency',
+ desc: `${evt.data.model_id}: ${evt.data.old} → ${evt.data.new}`
+ };
+ case 'MemoryChanged':
+ return {
+ icon: ,
+ color: 'accent-purple',
+ title: 'Memory Changed',
+ desc: `${(evt.data.used_bytes / 1024 / 1024 / 1024).toFixed(2)} GB / ${(evt.data.total_bytes / 1024 / 1024 / 1024).toFixed(1)} GB`
+ };
+ case 'ProviderHealthChanged':
+ return {
+ icon: ,
+ color: 'accent-yellow',
+ title: 'Provider Health',
+ desc: `${evt.data.provider}: ${evt.data.health}`
+ };
+ case 'DiscoveryCompleted':
+ return {
+ icon: ,
+ color: evt.data.success ? 'accent-green' : 'accent-red',
+ title: 'Discovery',
+ desc: `${evt.data.provider}: ${evt.data.models} model(s)`
+ };
+ default:
+ return {
+ icon: ,
+ color: 'text-dim',
+ title: evt.type || 'Event',
+ desc: JSON.stringify(evt.data)
+ };
+ }
+}
diff --git a/mofa-frontend/src/monitor/MemoryGauge.tsx b/mofa-frontend/src/monitor/MemoryGauge.tsx
new file mode 100644
index 0000000..04320c2
--- /dev/null
+++ b/mofa-frontend/src/monitor/MemoryGauge.tsx
@@ -0,0 +1,80 @@
+import React, { useEffect, useState, useRef } from 'react';
+import { engine } from '../engine';
+
+export function MemoryGauge() {
+ const [memory, setMemory] = useState(0);
+ const [maxMemory, setMaxMemory] = useState(16 * 1024 * 1024 * 1024);
+ const [delta, setDelta] = useState(null);
+ const deltaTimeout = useRef(null);
+
+ useEffect(() => {
+ // Initial fetch
+ engine.getStatus().then(s => {
+ if (s.success) {
+ setMemory(s.data.memory_used_bytes || 0);
+ setMaxMemory(s.data.memory_budget_bytes || 16 * 1024 * 1024 * 1024);
+ }
+ });
+
+ const handleEvent = (evt: any) => {
+ if (evt.type === 'MemoryChanged') {
+ setMemory(prev => {
+ const newMem = evt.data.used_bytes;
+ const diff = newMem - prev;
+ if (Math.abs(diff) > 1024 * 1024) { // only show significant delta
+ setDelta(diff);
+ if (deltaTimeout.current) clearTimeout(deltaTimeout.current);
+ deltaTimeout.current = setTimeout(() => setDelta(null), 2000);
+ }
+ return newMem;
+ });
+ }
+ };
+ const unsubscribe = engine.subscribeEvents(handleEvent);
+ return () => {
+ unsubscribe();
+ if (deltaTimeout.current) clearTimeout(deltaTimeout.current);
+ };
+ }, []);
+
+ const percentage = Math.min(100, Math.max(0, (memory / maxMemory) * 100));
+ const memoryGb = (memory / 1024 / 1024 / 1024).toFixed(2);
+ const maxGb = (maxMemory / 1024 / 1024 / 1024).toFixed(1);
+
+ let colorClass = 'bg-accent-green';
+ let textColorClass = 'text-accent-green';
+ if (percentage >= 90) {
+ colorClass = 'bg-accent-red';
+ textColorClass = 'text-accent-red';
+ } else if (percentage >= 80) {
+ colorClass = 'bg-accent-yellow';
+ textColorClass = 'text-accent-yellow';
+ }
+
+ return (
+
+
+
+ {memoryGb}
+ / {maxGb} GB
+
+
+ {percentage.toFixed(1)}%
+
+
+
+
+ {delta !== null && (
+ 0 ? 'text-accent-red' : 'text-accent-green'}`}>
+ {delta > 0 ? '+' : ''}{(delta / 1024 / 1024 / 1024).toFixed(2)} GB
+
+ )}
+
+
+ );
+}
diff --git a/mofa-frontend/src/monitor/ModelGrid.tsx b/mofa-frontend/src/monitor/ModelGrid.tsx
new file mode 100644
index 0000000..c95327e
--- /dev/null
+++ b/mofa-frontend/src/monitor/ModelGrid.tsx
@@ -0,0 +1,85 @@
+import React, { useEffect, useState } from 'react';
+import { engine } from '../engine';
+import { ModelCard } from '../engine/types';
+import { StatusDot } from '../shared/StatusDot';
+import { Cpu } from 'lucide-react';
+
+
+export function ModelGrid() {
+ const [models, setModels] = useState([]);
+
+ useEffect(() => {
+ // Initial load
+ engine.getCapabilities().then(c => {
+ if (c.success) setModels(c.data);
+ });
+
+ const handleEvent = (evt: any) => {
+ if (evt.type === 'ModelStatusChanged' || evt.type === 'ModelResidencyChanged') {
+ engine.getCapabilities().then(c => {
+ if (c.success) setModels(c.data);
+ });
+ }
+ };
+
+ const unsubscribe = engine.subscribeEvents(handleEvent);
+ return () => unsubscribe();
+ }, []);
+
+ const activeModels = models.filter(m => (m.status || '').toLowerCase() !== 'cold' || (m.residency || '').toLowerCase() === 'remote');
+
+ if (activeModels.length === 0) {
+ return (
+
+
+ All models cold on disk
+
+ );
+ }
+
+ return (
+
+ {activeModels.map(model => {
+ const memGb = model.memory_estimate_bytes ? (model.memory_estimate_bytes / 1024 / 1024 / 1024).toFixed(1) + 'GB' : '0GB';
+ const isRemote = (model.residency || '').toLowerCase() === 'remote';
+ const statusLower = (model.status || '').toLowerCase();
+
+ return (
+
+
+
+
+
{model.id}
+
+ {model.provider}
+ {!isRemote && (
+ <>
+ ·
+ {memGb}
+ >
+ )}
+
+
+
+
+
+ {isRemote ? 'REMOTE' : statusLower.toUpperCase()}
+
+
+
+ );
+ })}
+
+ );
+
+}
diff --git a/mofa-frontend/src/monitor/MonitorSidebar.tsx b/mofa-frontend/src/monitor/MonitorSidebar.tsx
new file mode 100644
index 0000000..d1ca99d
--- /dev/null
+++ b/mofa-frontend/src/monitor/MonitorSidebar.tsx
@@ -0,0 +1,58 @@
+import React from 'react';
+import { Activity, Shield, Box, Zap } from 'lucide-react';
+import { MemoryGauge } from './MemoryGauge';
+import { ModelGrid } from './ModelGrid';
+import { ProviderHealth } from './ProviderHealth';
+import { PreflightIndicator } from './PreflightIndicator';
+
+export function MonitorSidebar() {
+ return (
+
+
+
+ {/* Memory Constraints */}
+
+
+ {/* Preflight & Activity */}
+
+
+
+
Engine Activity
+
+
+
+
+ {/* Provider Health */}
+
+
+
+
Provider Health
+
+
+
+
+ {/* Active Models */}
+
+
+
+
Active Models
+
+
+
+
+
+
+
+
+ );
+}
diff --git a/mofa-frontend/src/monitor/PreflightIndicator.tsx b/mofa-frontend/src/monitor/PreflightIndicator.tsx
new file mode 100644
index 0000000..86d299f
--- /dev/null
+++ b/mofa-frontend/src/monitor/PreflightIndicator.tsx
@@ -0,0 +1,66 @@
+import React, { useEffect, useState } from 'react';
+import { engine } from '../engine';
+import { Zap, RotateCw, CheckCircle2 } from 'lucide-react';
+
+export function PreflightIndicator() {
+ const [status, setStatus] = useState<{ type: 'idle' | 'warming' | 'evicting'; model?: string; loadedCount: number }>({
+ type: 'idle',
+ loadedCount: 0
+ });
+
+ useEffect(() => {
+ // Initial fetch to get loaded count
+ engine.getStatus().then(s => {
+ if (s.success) {
+ setStatus(prev => ({ ...prev, loadedCount: s.data.loaded_models }));
+ }
+ });
+
+ const handleEvent = (evt: any) => {
+ if (evt.type === 'ModelResidencyChanged') {
+ const { model, from, to, reason } = evt.data;
+ if (to === 'Loading') {
+ setStatus(prev => ({ ...prev, type: 'warming', model }));
+ } else if (to === 'Unloaded' && reason === 'eviction') {
+ setStatus(prev => ({ ...prev, type: 'evicting', model }));
+ } else if (to === 'Loaded' || to === 'Unloaded') {
+ // Re-fetch loaded count
+ engine.getStatus().then(s => {
+ if (s.success) {
+ setStatus({ type: 'idle', loadedCount: s.data.loaded_models });
+ }
+ });
+ }
+ } else if (evt.type === 'ModelStatusChanged') {
+ const { model, from, to } = evt.data;
+ if (to === 'Warming') {
+ setStatus(prev => ({ ...prev, type: 'warming', model }));
+ }
+ }
+ };
+
+ const unsubscribe = engine.subscribeEvents(handleEvent);
+ return () => unsubscribe();
+ }, []);
+
+ return (
+
+ {status.type === 'idle' ? (
+
+
+ Idle · {status.loadedCount} Loaded
+
+ ) : status.type === 'warming' ? (
+
+
+ Pre-warming {status.model}
+
+ ) : (
+
+
+ Evicting {status.model}
+
+ )}
+
+ );
+}
diff --git a/mofa-frontend/src/monitor/ProviderHealth.tsx b/mofa-frontend/src/monitor/ProviderHealth.tsx
new file mode 100644
index 0000000..cf44eec
--- /dev/null
+++ b/mofa-frontend/src/monitor/ProviderHealth.tsx
@@ -0,0 +1,42 @@
+import React from 'react';
+import { ShieldAlert, ShieldCheck } from 'lucide-react';
+import { useEngineStatus } from '../engine/useEngineStatus';
+
+export function ProviderHealth() {
+ const { data: statusRes } = useEngineStatus();
+
+ const providers = (statusRes?.success ? statusRes.data.provider_health : null) || [
+ { name: 'Ollama (Local)', healthy: true, circuit_state: 'closed' },
+ { name: 'Kokoro (Local)', healthy: true, circuit_state: 'closed' }
+ ];
+
+ return (
+
+ {providers.map(p => {
+ const state = (p.circuit_state || '').toLowerCase();
+ const isClosed = state === 'closed';
+ const isHalfOpen = state === 'halfopen' || state === 'half_open';
+
+ return (
+
+
+ {isClosed ? (
+
+ ) : isHalfOpen ? (
+
+ ) : (
+
+ )}
+ {p.name}
+
+
+
+ {isClosed ? 'CLOSED' : isHalfOpen ? 'HALF_OPEN' : 'OPEN'}
+
+
+
+ );
+ })}
+
+ );
+}
diff --git a/mofa-frontend/src/observability/MetricsStrip.tsx b/mofa-frontend/src/observability/MetricsStrip.tsx
new file mode 100644
index 0000000..aef5b15
--- /dev/null
+++ b/mofa-frontend/src/observability/MetricsStrip.tsx
@@ -0,0 +1,96 @@
+import React, { useEffect, useState } from 'react';
+import { engine } from '../engine';
+import { EngineStatus } from '../engine/types';
+import { Card } from '../shared/Card';
+import { Activity, Clock, Cpu, HardDrive } from 'lucide-react';
+
+export function MetricsStrip() {
+ const [status, setStatus] = useState(null);
+ const [requestCount, setRequestCount] = useState(0);
+ const [avgLatency, setAvgLatency] = useState(0);
+
+ useEffect(() => {
+ // Initial fetch
+ engine.getStatus().then(res => {
+ if (res.success) setStatus(res.data);
+ });
+
+ const handleEvent = (evt: any) => {
+ if (evt.type === 'RequestStarted') {
+ setRequestCount(prev => prev + 1);
+ } else if (evt.type === 'RequestCompleted') {
+ if (evt.data && evt.data.duration_ms) {
+ setAvgLatency(prev => {
+ if (prev === 0) return evt.data.duration_ms;
+ return prev * 0.8 + evt.data.duration_ms * 0.2;
+ });
+ }
+ } else if (
+ evt.type === 'ModelStatusChanged' ||
+ evt.type === 'ModelResidencyChanged' ||
+ evt.type === 'MemoryChanged'
+ ) {
+ engine.getStatus().then(res => {
+ if (res.success) setStatus(res.data);
+ });
+ }
+ };
+
+ const unsubscribe = engine.subscribeEvents(handleEvent);
+ return () => unsubscribe();
+ }, []);
+
+ const memoryPercent = status
+ ? ((status.memory_used_bytes / status.memory_budget_bytes) * 100).toFixed(1)
+ : 0;
+
+ return (
+
+
+
+
+
Total Requests
+
{requestCount}
+
+
+
+
+
+
+
+
+
Avg Latency
+
+ {avgLatency > 0 ? `${(avgLatency / 1000).toFixed(2)}s` : '--'}
+
+
+
+
+
+
+
+
+
+
Models Loaded
+
+ {status?.loaded_models ?? '--'} / {status?.total_models ?? '--'}
+
+
+
+
+
+
+
+
+
+
Memory Usage
+
+ {memoryPercent}%
+
+
+
+
+ );
+}
diff --git a/mofa-frontend/src/shared/Badge.tsx b/mofa-frontend/src/shared/Badge.tsx
new file mode 100644
index 0000000..c631e93
--- /dev/null
+++ b/mofa-frontend/src/shared/Badge.tsx
@@ -0,0 +1,42 @@
+import React from 'react';
+import { cn } from '../lib/utils';
+import { Capability } from '../engine/types';
+
+interface BadgeProps extends React.HTMLAttributes {
+ variant?: 'blue' | 'purple' | 'cyan' | 'yellow' | 'green' | 'default';
+ capability?: Capability;
+}
+
+export function Badge({ className, variant = 'default', capability, children, ...props }: BadgeProps) {
+ let finalVariant = variant;
+
+ if (capability) {
+ const capLower = capability.toLowerCase();
+ switch (capLower) {
+ case 'chat': finalVariant = 'blue'; break;
+ case 'tts': finalVariant = 'purple'; break;
+ case 'asr': finalVariant = 'cyan'; break;
+ case 'imagegen': case 'image_gen': finalVariant = 'yellow'; break;
+ case 'embedding': finalVariant = 'green'; break;
+ default: finalVariant = 'default';
+ }
+ }
+
+ return (
+
+ {capability || children}
+
+ );
+}
diff --git a/mofa-frontend/src/shared/Button.tsx b/mofa-frontend/src/shared/Button.tsx
new file mode 100644
index 0000000..9886349
--- /dev/null
+++ b/mofa-frontend/src/shared/Button.tsx
@@ -0,0 +1,31 @@
+import React from 'react';
+import { cn } from '../lib/utils';
+import { motion, HTMLMotionProps } from 'framer-motion';
+import { motionVariants } from '../lib/motion';
+
+interface ButtonProps extends HTMLMotionProps<'button'> {
+ variant?: 'primary' | 'secondary' | 'ghost';
+}
+
+export const Button = React.forwardRef(
+ ({ className, variant = 'primary', ...props }, ref) => {
+ return (
+
+ {props.children as React.ReactNode}
+
+ );
+ }
+);
+Button.displayName = 'Button';
diff --git a/mofa-frontend/src/shared/Card.tsx b/mofa-frontend/src/shared/Card.tsx
new file mode 100644
index 0000000..e2451bb
--- /dev/null
+++ b/mofa-frontend/src/shared/Card.tsx
@@ -0,0 +1,24 @@
+import React from 'react';
+import { cn } from '../lib/utils';
+import { motion, HTMLMotionProps } from 'framer-motion';
+
+interface CardProps extends HTMLMotionProps<'div'> {
+ interactive?: boolean;
+}
+
+export const Card = React.forwardRef(
+ ({ className, interactive, ...props }, ref) => {
+ return (
+
+ );
+ }
+);
+Card.displayName = 'Card';
diff --git a/mofa-frontend/src/shared/Skeleton.tsx b/mofa-frontend/src/shared/Skeleton.tsx
new file mode 100644
index 0000000..c232444
--- /dev/null
+++ b/mofa-frontend/src/shared/Skeleton.tsx
@@ -0,0 +1,12 @@
+import { cn } from '../lib/utils';
+
+type SkeletonProps = React.HTMLAttributes;
+
+export function Skeleton({ className, ...props }: SkeletonProps) {
+ return (
+
+ );
+}
diff --git a/mofa-frontend/src/shared/StatusDot.tsx b/mofa-frontend/src/shared/StatusDot.tsx
new file mode 100644
index 0000000..1c9452d
--- /dev/null
+++ b/mofa-frontend/src/shared/StatusDot.tsx
@@ -0,0 +1,51 @@
+import React from 'react';
+import { cn } from '../lib/utils';
+import { motion } from 'framer-motion';
+
+export type StatusType = 'Hot' | 'Healthy' | 'Closed' | 'Warming' | 'HalfOpen' | 'Busy' | 'Cold' | 'Unloaded' | 'Failed' | 'Open' | 'Unhealthy' | 'Connecting';
+
+interface StatusDotProps {
+ status: StatusType;
+ className?: string;
+}
+
+export function StatusDot({ status, className }: StatusDotProps) {
+ let colorClass = 'bg-text-dim';
+ let animatePulse = false;
+
+ switch (status) {
+ case 'Hot':
+ case 'Healthy':
+ case 'Closed':
+ colorClass = 'bg-accent-green shadow-[0_0_8px_rgba(16,185,129,0.6)]';
+ break;
+ case 'Warming':
+ case 'HalfOpen':
+ case 'Connecting':
+ colorClass = 'bg-accent-yellow shadow-[0_0_8px_rgba(245,158,11,0.6)]';
+ animatePulse = true;
+ break;
+ case 'Busy':
+ colorClass = 'bg-accent-blue shadow-[0_0_8px_rgba(59,130,246,0.6)]';
+ animatePulse = true;
+ break;
+ case 'Cold':
+ case 'Unloaded':
+ break;
+ case 'Failed':
+ case 'Open':
+ case 'Unhealthy':
+ colorClass = 'bg-accent-red shadow-[0_0_8px_rgba(239,68,68,0.6)]';
+ break;
+ default:
+ break;
+ }
+
+ return (
+
+ );
+}
diff --git a/mofa-frontend/src/shell/CommandPalette.tsx b/mofa-frontend/src/shell/CommandPalette.tsx
new file mode 100644
index 0000000..fe1e2a5
--- /dev/null
+++ b/mofa-frontend/src/shell/CommandPalette.tsx
@@ -0,0 +1,197 @@
+import React, { useEffect, useState, useRef } from 'react';
+import { motion, AnimatePresence } from 'framer-motion';
+import { Search, Play, Activity, History, Trash2, Settings, Code } from 'lucide-react';
+import { useHistory } from '../storage/useHistory';
+
+interface Command {
+ id: string;
+ label: string;
+ icon: React.ReactNode;
+ action: () => void;
+ shortcut?: string;
+ keywords: string[];
+}
+
+export function CommandPalette() {
+ const [isOpen, setIsOpen] = useState(false);
+ const [query, setQuery] = useState('');
+ const [selectedIndex, setSelectedIndex] = useState(0);
+ const inputRef = useRef(null);
+ const { clearHistory } = useHistory();
+
+ const commands: Command[] = [
+ {
+ id: 'generate',
+ label: 'Generate podcast',
+ icon: ,
+ action: () => { document.dispatchEvent(new KeyboardEvent('keydown', { metaKey: true, key: 'Enter' })); }, // Mock trigger
+ shortcut: 'Cmd ↵',
+ keywords: ['generate', 'start', 'podcast', 'make', 'create']
+ },
+ {
+ id: 'monitor',
+ label: 'Open Engine Monitor',
+ icon: ,
+ action: () => document.dispatchEvent(new CustomEvent('open-monitor')),
+ keywords: ['monitor', 'engine', 'status', 'telemetry', 'memory']
+ },
+ {
+ id: 'history',
+ label: 'Open History',
+ icon: ,
+ action: () => document.dispatchEvent(new CustomEvent('open-history')),
+ keywords: ['history', 'recent', 'past']
+ },
+ {
+ id: 'settings',
+ label: 'Open Settings',
+ icon: ,
+ action: () => document.dispatchEvent(new CustomEvent('open-settings')),
+ keywords: ['settings', 'preferences', 'config', 'url', 'voice']
+ },
+ {
+ id: 'clear',
+ label: 'Clear history',
+ icon: ,
+ action: () => {
+ if (confirm('Clear all history?')) clearHistory();
+ },
+ keywords: ['clear', 'delete', 'remove', 'history']
+ },
+ {
+ id: 'github',
+ label: 'View on GitHub',
+ icon: ,
+ action: () => window.open('https://github.com/mofa-org/mofa-engine', '_blank'),
+ keywords: ['github', 'repo', 'source', 'code']
+ },
+ ];
+
+ const filteredCommands = query
+ ? commands.filter(c =>
+ c.label.toLowerCase().includes(query.toLowerCase()) ||
+ c.keywords.some(k => k.includes(query.toLowerCase()))
+ )
+ : commands;
+
+ useEffect(() => {
+ const handleKeyDown = (e: KeyboardEvent) => {
+ if ((e.metaKey || e.ctrlKey) && e.key === 'k') {
+ e.preventDefault();
+ setIsOpen(prev => {
+ if (!prev) {
+ setQuery('');
+ setSelectedIndex(0);
+ setTimeout(() => inputRef.current?.focus(), 100);
+ }
+ return !prev;
+ });
+ }
+ if (e.key === 'Escape' && isOpen) {
+ e.preventDefault();
+ setIsOpen(false);
+ }
+ };
+ window.addEventListener('keydown', handleKeyDown);
+ return () => window.removeEventListener('keydown', handleKeyDown);
+ }, [isOpen]);
+
+ const handleQueryChange = (e: React.ChangeEvent) => {
+ setQuery(e.target.value);
+ setSelectedIndex(0);
+ };
+
+ const handleKeyDown = (e: React.KeyboardEvent) => {
+ if (e.key === 'ArrowDown') {
+ e.preventDefault();
+ setSelectedIndex(prev => Math.min(prev + 1, filteredCommands.length - 1));
+ } else if (e.key === 'ArrowUp') {
+ e.preventDefault();
+ setSelectedIndex(prev => Math.max(prev - 1, 0));
+ } else if (e.key === 'Enter') {
+ e.preventDefault();
+ if (filteredCommands[selectedIndex]) {
+ filteredCommands[selectedIndex].action();
+ setIsOpen(false);
+ }
+ }
+ };
+
+ return (
+
+ {isOpen && (
+ <>
+ setIsOpen(false)}
+ className="fixed inset-0 bg-background-primary/60 backdrop-blur-sm z-[100]"
+ />
+
+
+
+
+
+ ESC
+
+
+
+ {filteredCommands.length === 0 ? (
+
+ No commands found.
+
+ ) : (
+ filteredCommands.map((cmd, i) => (
+
{
+ cmd.action();
+ setIsOpen(false);
+ }}
+ onMouseEnter={() => setSelectedIndex(i)}
+ className={`flex items-center justify-between px-4 py-3 cursor-pointer ${
+ i === selectedIndex ? 'bg-black/5' : 'hover:bg-black/5'
+ }`}
+ role="button"
+ tabIndex={-1}
+ >
+
+ {cmd.icon}
+
+ {cmd.label}
+
+
+ {cmd.shortcut && (
+
+ {cmd.shortcut}
+
+ )}
+
+ ))
+ )}
+
+
+
+ >
+ )}
+
+ );
+}
diff --git a/mofa-frontend/src/shell/SettingsModal.tsx b/mofa-frontend/src/shell/SettingsModal.tsx
new file mode 100644
index 0000000..d5dcabb
--- /dev/null
+++ b/mofa-frontend/src/shell/SettingsModal.tsx
@@ -0,0 +1,178 @@
+import React, { useState } from 'react';
+import { motion, AnimatePresence } from 'framer-motion';
+import { Card } from '../shared/Card';
+import { Button } from '../shared/Button';
+import { X, Volume2, VolumeX, Eye, EyeOff, Trash2 } from 'lucide-react';
+import { RealEngineClient } from '../engine/client';
+import { getStoredEngineUrl, setStoredEngineUrl } from '../engine/index';
+import { useQueryClient } from '@tanstack/react-query';
+import { useEngineConnection } from '../engine/useEngineConnection';
+import { useSettings } from '../storage/useSettings';
+import { useHistory } from '../storage/useHistory';
+
+interface SettingsModalProps {
+ isOpen: boolean;
+ onClose: () => void;
+}
+
+export function SettingsModal({ isOpen, onClose }: SettingsModalProps) {
+ const [urlInput, setUrlInput] = useState(getStoredEngineUrl());
+ const [testResult, setTestResult] = useState<{ success: boolean; text: string } | null>(null);
+ const queryClient = useQueryClient();
+ const { uptime_secs, version } = useEngineConnection();
+ const { settings, updateSettings } = useSettings();
+ const { clearHistory } = useHistory();
+
+ const handleSave = () => {
+ setStoredEngineUrl(urlInput);
+ // Invalidate queries to restart polling with new URL
+ queryClient.invalidateQueries({ queryKey: ['engine_health'] });
+ queryClient.invalidateQueries({ queryKey: ['engine_status'] });
+ onClose();
+ };
+
+ const handleTest = async () => {
+ setTestResult(null);
+ try {
+ const tempClient = new RealEngineClient(urlInput);
+ const res = await tempClient.getHealth();
+ if (res.success) {
+ setTestResult({ success: true, text: `Success! Engine v${res.data.version}` });
+ } else {
+ setTestResult({ success: false, text: res.error || 'Connection failed' });
+ }
+ } catch {
+ setTestResult({ success: false, text: 'Connection failed' });
+ }
+ };
+
+ const handleClearData = () => {
+ if (confirm('Are you sure you want to delete all history and drafts? This cannot be undone.')) {
+ clearHistory();
+ localStorage.removeItem('mofa_compose_draft');
+ alert('Data cleared successfully.');
+ }
+ };
+
+ return (
+
+ {isOpen && (
+
+
+
+
+
Settings
+
+
+
+
+
+
+
+
+
Engine URL
+
+ setUrlInput(e.target.value)}
+ className="flex-1 bg-background-secondary border border-black/10 rounded-[var(--radius-small)] px-3 py-2 text-sm text-text-primary focus:outline-none focus:border-accent-blue"
+ placeholder="http://127.0.0.1:8420"
+ />
+
+
+
+ Test Connection
+
+ {testResult && (
+
+ {testResult.text}
+
+ )}
+
+
+
+
+ Default Voice
+ updateSettings({ defaultVoice: e.target.value })}
+ className="w-full bg-background-secondary border border-black/10 rounded-[var(--radius-small)] px-3 py-2 text-sm text-text-primary focus:outline-none focus:border-accent-purple appearance-none"
+ >
+ {['Xiaoxiao', 'Yunxi', 'Xiaoni', 'Nova', 'Alloy', 'Echo'].map((v) => (
+ {v}
+ ))}
+
+
+
+
+
+
Sound Effects
+
Play a chime when generation completes
+
+
updateSettings({ soundEnabled: !settings.soundEnabled })}
+ className={`p-2 rounded-full transition-colors ${settings.soundEnabled ? 'bg-accent-cyan/10 text-accent-cyan' : 'bg-black/5 text-text-dim hover:text-text-secondary'}`}
+ >
+ {settings.soundEnabled ? : }
+
+
+
+
+
+
Reduced Motion
+
Minimize animations and transitions
+
+
updateSettings({ reducedMotion: !settings.reducedMotion })}
+ className={`p-2 rounded-full transition-colors ${settings.reducedMotion ? 'bg-accent-blue/10 text-accent-blue' : 'bg-black/5 text-text-dim hover:text-text-secondary'}`}
+ >
+ {settings.reducedMotion ? : }
+
+
+
+
+
+
Danger Zone
+
Clear local drafts and podcast history
+
+
+
+ Clear Data
+
+
+
+
+
+
About MoFA FM
+
+
Engine Version: {version || 'Offline'}
+
Uptime: {uptime_secs ? `${uptime_secs}s` : 'Offline'}
+
Frontend: v0.1.0
+
+
+
+
+
+
+ Done
+
+
+
+
+ )}
+
+ );
+}
diff --git a/mofa-frontend/src/shell/TopBar.tsx b/mofa-frontend/src/shell/TopBar.tsx
new file mode 100644
index 0000000..2fd86b2
--- /dev/null
+++ b/mofa-frontend/src/shell/TopBar.tsx
@@ -0,0 +1,77 @@
+import React, { useState, useEffect } from 'react';
+import { Settings, Mic2, History, Activity } from 'lucide-react';
+import { SettingsModal } from './SettingsModal';
+const SCENARIOS = [
+ { value: 'success', label: 'Success' },
+ { value: 'memory_pressure', label: 'Memory Pressure & Eviction' },
+ { value: 'circuit_open', label: 'Circuit Open' },
+ { value: 'timeout', label: 'Timeout' },
+ { value: 'no_tts_model', label: 'No TTS Model' },
+ { value: 'no_chat_model', label: 'No Chat Model' },
+ { value: 'empty_script', label: 'Empty Script' },
+ { value: 'fallback', label: 'Fallback' },
+ { value: 'provider_flapping', label: 'Provider Flapping' },
+ { value: 'engine_offline', label: 'Engine Offline' }
+];
+
+interface TopBarProps {
+ currentView?: 'studio' | 'observability';
+}
+
+export function TopBar({ currentView = 'studio' }: TopBarProps) {
+ const [settingsOpen, setSettingsOpen] = useState(false);
+
+ const currentScenario = new URLSearchParams(window.location.search).get('scenario') || 'success';
+
+ const handleScenarioChange = (e: React.ChangeEvent) => {
+ const url = new URL(window.location.href);
+ url.searchParams.set('scenario', e.target.value);
+ window.location.href = url.toString();
+ };
+
+ useEffect(() => {
+ const handleOpen = () => setSettingsOpen(true);
+ document.addEventListener('open-settings', handleOpen);
+ return () => document.removeEventListener('open-settings', handleOpen);
+ }, []);
+
+ return (
+ <>
+
+ setSettingsOpen(false)} />
+ >
+ );
+}
diff --git a/mofa-frontend/src/storage/HistoryDrawer.tsx b/mofa-frontend/src/storage/HistoryDrawer.tsx
new file mode 100644
index 0000000..81a02c0
--- /dev/null
+++ b/mofa-frontend/src/storage/HistoryDrawer.tsx
@@ -0,0 +1,114 @@
+import React, { useEffect, useState } from 'react';
+import { motion, AnimatePresence } from 'framer-motion';
+import { X, History, Trash2, Play } from 'lucide-react';
+import { useHistory } from './useHistory';
+import { PipelinePhase } from '../studio/usePipeline';
+import { formatMs } from '../lib/utils';
+import { Badge } from '../shared/Badge';
+
+interface HistoryDrawerProps {
+ onSelectResult?: (phase: PipelinePhase) => void;
+}
+
+export function HistoryDrawer({ onSelectResult }: HistoryDrawerProps) {
+ const [isOpen, setIsOpen] = useState(false);
+ const { history, clearHistory } = useHistory();
+
+ useEffect(() => {
+ const handleOpen = () => setIsOpen(true);
+ document.addEventListener('open-history', handleOpen);
+ return () => document.removeEventListener('open-history', handleOpen);
+ }, []);
+
+ const handleSelect = (phase: PipelinePhase) => {
+ setIsOpen(false);
+ if (onSelectResult) {
+ onSelectResult(phase);
+ } else {
+ document.dispatchEvent(new CustomEvent('load-history-phase', { detail: phase }));
+ }
+ };
+
+ return (
+
+ {isOpen && (
+ <>
+ setIsOpen(false)}
+ className="fixed inset-0 bg-black/40 backdrop-blur-sm z-40"
+ />
+
+
+
+
+
History
+
+
setIsOpen(false)}
+ className="p-2 -mr-2 text-text-secondary hover:text-black rounded-full hover:bg-black/5 transition-colors"
+ >
+
+
+
+
+
+ {history.length === 0 ? (
+
+ No past generations.
+
+ ) : (
+ <>
+
+
+ Clear All
+
+
+ {history.map((item, idx) => (
+ item.status === 'done' && (
+
handleSelect(item)}
+ className="p-4 bg-white border border-black/5 shadow-sm rounded-md cursor-pointer hover:bg-black/5 hover:border-black/10 transition-all group"
+ >
+
+
+ {item.chat.script.substring(0, 40)}...
+
+
+
+
+
+ {item.chat.model}
+ {item.tts.model}
+
+
+ {formatMs(item.totalMs)}
+ {item.chat.tokens} tokens
+
+
+ )
+ ))}
+ >
+ )}
+
+
+ >
+ )}
+
+ );
+}
diff --git a/mofa-frontend/src/storage/useHistory.ts b/mofa-frontend/src/storage/useHistory.ts
new file mode 100644
index 0000000..ba8d1a1
--- /dev/null
+++ b/mofa-frontend/src/storage/useHistory.ts
@@ -0,0 +1,48 @@
+import { useState } from 'react';
+import { PipelinePhase } from '../studio/usePipeline';
+
+const HISTORY_KEY = 'mofa_history';
+const DRAFT_KEY = 'mofa_compose_draft';
+
+export function useHistory() {
+ const [history, setHistory] = useState(() => {
+ const raw = localStorage.getItem(HISTORY_KEY);
+ if (raw) {
+ try {
+ return JSON.parse(raw);
+ } catch (e) {
+ console.error('Failed to parse history', e);
+ }
+ }
+ return [];
+ });
+
+ const saveToHistory = (phase: PipelinePhase) => {
+ if (phase.status !== 'done') return;
+ setHistory(prev => {
+ const next = [phase, ...prev].slice(0, 50); // Keep last 50
+ localStorage.setItem(HISTORY_KEY, JSON.stringify(next));
+ return next;
+ });
+ };
+
+ const clearHistory = () => {
+ setHistory([]);
+ localStorage.removeItem(HISTORY_KEY);
+ };
+
+ return { history, saveToHistory, clearHistory };
+}
+
+export function useDraft() {
+ const [draft, setDraft] = useState(() => {
+ return localStorage.getItem(DRAFT_KEY) || '';
+ });
+
+ const saveDraft = (text: string) => {
+ setDraft(text);
+ localStorage.setItem(DRAFT_KEY, text);
+ };
+
+ return { draft, saveDraft };
+}
diff --git a/mofa-frontend/src/storage/useSettings.ts b/mofa-frontend/src/storage/useSettings.ts
new file mode 100644
index 0000000..d6118df
--- /dev/null
+++ b/mofa-frontend/src/storage/useSettings.ts
@@ -0,0 +1,37 @@
+import { useState } from 'react';
+
+interface Settings {
+ soundEnabled: boolean;
+ reducedMotion: boolean;
+ defaultVoice: string;
+}
+
+const DEFAULT_SETTINGS: Settings = {
+ soundEnabled: true,
+ reducedMotion: false,
+ defaultVoice: 'Nova',
+};
+
+export function useSettings() {
+ const [settings, setSettings] = useState(() => {
+ const raw = localStorage.getItem('mofa_settings');
+ if (raw) {
+ try {
+ return { ...DEFAULT_SETTINGS, ...JSON.parse(raw) };
+ } catch {
+ return DEFAULT_SETTINGS;
+ }
+ }
+ return DEFAULT_SETTINGS;
+ });
+
+ const updateSettings = (updates: Partial) => {
+ setSettings((prev) => {
+ const next = { ...prev, ...updates };
+ localStorage.setItem('mofa_settings', JSON.stringify(next));
+ return next;
+ });
+ };
+
+ return { settings, updateSettings };
+}
diff --git a/mofa-frontend/src/studio/PipelineViz.tsx b/mofa-frontend/src/studio/PipelineViz.tsx
new file mode 100644
index 0000000..86d8f64
--- /dev/null
+++ b/mofa-frontend/src/studio/PipelineViz.tsx
@@ -0,0 +1,210 @@
+import React from 'react';
+import { motion } from 'framer-motion';
+import { Check, AlertCircle, FileText, MessageSquareText, AudioLines, PlayCircle } from 'lucide-react';
+import { formatMs } from '../lib/utils';
+import { PipelinePhase } from './usePipeline';
+
+export function PipelineViz({ phase }: { phase: PipelinePhase }) {
+ const isTranslating = phase.status === 'translating';
+ const isSynthesizing = phase.status === 'synthesizing' || (phase.status === 'error' && (phase as any).failedStep === 'tts');
+ const isChatDone = phase.status === 'translated' || phase.status === 'synthesizing' || phase.status === 'done' || (phase.status === 'error' && (phase as any).failedStep === 'tts');
+
+ const chatResult = (phase as any).chat;
+ const ttsResult = (phase as any).tts;
+
+ return (
+
+ {/* Track */}
+
+
+ {/* Active Flow Animation */}
+ {(isTranslating || isSynthesizing) && (
+
+ )}
+
+
+
+
+
+ );
+}
+
+export function PipelineNode({
+ title,
+ active,
+ done,
+ error,
+ accent,
+ result,
+ icon: Icon
+}: {
+ title: string;
+ active: boolean;
+ done: boolean;
+ error: boolean;
+ accent: 'blue' | 'purple' | 'green';
+ result: any;
+ icon: any;
+}) {
+ let borderTextClass = "text-text-dim border-black/10 shadow-sm";
+ let bgTintClass = "bg-transparent";
+
+ if (active) {
+ borderTextClass = accent === 'blue'
+ ? "text-accent-blue border-accent-blue/30 shadow-[0_0_20px_rgba(59,130,246,0.15)]"
+ : accent === 'purple' ? "text-accent-purple border-accent-purple/30 shadow-[0_0_20px_rgba(168,85,247,0.15)]"
+ : "text-accent-green border-accent-green/30 shadow-[0_0_20px_rgba(16,185,129,0.15)]";
+ bgTintClass = accent === 'blue' ? "bg-accent-blue/5" : accent === 'purple' ? "bg-accent-purple/5" : "bg-accent-green/5";
+ } else if (error) {
+ borderTextClass = "text-accent-red border-accent-red/30 shadow-sm";
+ bgTintClass = "bg-accent-red/5";
+ } else if (done) {
+ borderTextClass = "text-accent-green border-accent-green/30 shadow-sm";
+ bgTintClass = "bg-accent-green/5";
+ }
+
+ return (
+
+
+
+
+ {/* Ring animation if active */}
+ {active && (
+
+ )}
+
+ {/* Status Badge */}
+ {done && !error && (
+
+
+
+ )}
+ {error && (
+
+
+
+ )}
+
+
+ {active && title === 'TTS Audio' ? (
+
+ {[1, 2, 3, 4, 5].map(i => (
+
+ ))}
+
+ ) : (
+
+ )}
+
+
+
+
+
+ {title}
+
+
+ {done && result && (
+ <>
+
+
+ {result.model ? `${result.model} · ` : ''}{formatMs(result.duration_ms ?? result.durationMs)}
+
+ {result.fallbackUsed && (
+
+ ⚡ Fallback
+
+ )}
+
+ {result.routingReason && (
+
+ Selected via: {result.routingReason.replace(/_/g, ' ')}
+
+ )}
+ >
+ )}
+ {active && (
+
+ Processing...
+
+ )}
+
+
+
+ );
+}
diff --git a/mofa-frontend/src/studio/Studio.tsx b/mofa-frontend/src/studio/Studio.tsx
new file mode 100644
index 0000000..5739cff
--- /dev/null
+++ b/mofa-frontend/src/studio/Studio.tsx
@@ -0,0 +1,60 @@
+import React, { useEffect } from 'react';
+import { AnimatePresence } from 'framer-motion';
+import { usePipeline } from './usePipeline';
+import { ComposeView } from './compose/ComposeView';
+import { TheaterView } from './theater/TheaterView';
+import { ResultView } from './result/ResultView';
+import { useHistory } from '../storage/useHistory';
+import { useSettings } from '../storage/useSettings';
+import { playChime } from '../lib/audio';
+
+export function Studio() {
+ const { phase, start, reset, retryTts, loadPhase } = usePipeline();
+ const { saveToHistory } = useHistory();
+ const { settings } = useSettings();
+
+ useEffect(() => {
+ if (phase.status === 'done') {
+ saveToHistory(phase);
+ if (settings.soundEnabled && !settings.reducedMotion) {
+ playChime();
+ }
+ }
+ }, [phase, saveToHistory, settings.soundEnabled, settings.reducedMotion]);
+
+ useEffect(() => {
+ const handleLoad = (e: any) => loadPhase(e.detail);
+ document.addEventListener('load-history-phase', handleLoad);
+ return () => document.removeEventListener('load-history-phase', handleLoad);
+ }, [loadPhase]);
+
+ useEffect(() => {
+ if (phase.status === 'idle') {
+ document.title = 'MoFA FM — Local AI Podcast Studio';
+ } else if (phase.status === 'done') {
+ document.title = '✓ Ready — MoFA FM';
+ } else if (phase.status === 'error') {
+ document.title = 'Error — MoFA FM';
+ } else {
+ document.title = '⚡ Generating... — MoFA FM';
+ }
+ }, [phase.status]);
+
+ return (
+
+
+ {phase.status === 'idle' && (
+
+ )}
+
+ {(phase.status === 'translating' || phase.status === 'translated' || phase.status === 'synthesizing' || phase.status === 'error') && (
+
+ )}
+
+ {phase.status === 'done' && (
+
+ )}
+
+
+ );
+}
diff --git a/mofa-frontend/src/studio/compose/ComposeView.tsx b/mofa-frontend/src/studio/compose/ComposeView.tsx
new file mode 100644
index 0000000..e156cbe
--- /dev/null
+++ b/mofa-frontend/src/studio/compose/ComposeView.tsx
@@ -0,0 +1,162 @@
+import React, { useState } from 'react';
+import { motion } from 'framer-motion';
+import { motionVariants } from '../../lib/motion';
+import { Card } from '../../shared/Card';
+import { Button } from '../../shared/Button';
+import { ChevronDown, Play, Sparkles, Languages } from 'lucide-react';
+import { useEngineConnection } from '../../engine/useEngineConnection';
+import { useDraft } from '../../storage/useHistory';
+import { OnboardingBanner } from './OnboardingBanner';
+
+interface ComposeViewProps {
+ onStart: (article: string, options: { systemPrompt: string; voice: string }) => void;
+}
+
+const SAMPLES = {
+ 'AI News': "A new study reveals that artificial intelligence models are increasingly capable of writing highly optimized code, but struggle with contextual nuances in legacy systems. Researchers at MIT found a 40% increase in developer productivity when pairing AI with human review.",
+ 'Science': "The James Webb Space Telescope has captured a stunning new image of the Pillars of Creation. The near-infrared camera peered through dust clouds to reveal nascent stars forming in the gas pillars, located 6,500 light-years away in the Eagle Nebula.",
+ 'Short Story': "The rain hasn't stopped for three days. Inside the small café on 5th Street, Elias watched the water run down the windowpane, distorting the neon signs outside. He clutched his coffee mug, waiting for a message that might never come."
+};
+
+export function ComposeView({ onStart }: ComposeViewProps) {
+ const { draft, saveDraft } = useDraft();
+ const [article, setArticle] = useState(draft);
+ const [voice, setVoice] = useState('Xiaoxiao');
+ const [showAdvanced, setShowAdvanced] = useState(false);
+ const [systemPrompt, setSystemPrompt] = useState('Rewrite as a Chinese podcast script, under 200 characters. Output only Chinese.');
+
+ const { state: engineState } = useEngineConnection();
+
+ const handleArticleChange = (val: string) => {
+ setArticle(val);
+ saveDraft(val);
+ };
+
+ const handleGenerate = () => {
+ if (!article.trim() || engineState !== 'connected') return;
+ onStart(article, { systemPrompt, voice });
+ };
+
+ const isButtonDisabled = !article.trim() || engineState !== 'connected';
+
+ return (
+
+
+
+
MoFA Engine Demonstration
+
Powered by the MoFA Engine — intelligent local model orchestration, preflight routing, and dynamic memory management.
+
+
+
+
+
+
+
+
+
+
+ {article.length > 4000 && (
+
Long articles take a little longer.
+ )}
+
+
+
+ setVoice(e.target.value)}
+ className="appearance-none bg-black/5 border border-black/10 rounded-[var(--radius-small)] pl-8 pr-8 py-2 text-[13px] text-text-primary focus:outline-none focus:border-accent-purple"
+ >
+ {['Xiaoxiao', 'Yunxi', 'Xiaoni', 'Nova', 'Alloy', 'Echo'].map(v => (
+ {v}
+ ))}
+
+
+
+
+
+
+
+ Chinese
+ Spanish (soon)
+
+
+
+
+
+
setShowAdvanced(!showAdvanced)}
+ className="ml-auto flex items-center gap-1 text-[13px] text-text-dim hover:text-text-secondary transition-colors"
+ >
+ Advanced
+
+
+
+ {showAdvanced && (
+
+ System Prompt
+
+ )}
+
+
+
+
+ Generate Podcast
+
+
~18 seconds · fully local · Cmd ↵
+
+
+
+ );
+}
diff --git a/mofa-frontend/src/studio/compose/OnboardingBanner.tsx b/mofa-frontend/src/studio/compose/OnboardingBanner.tsx
new file mode 100644
index 0000000..841eafb
--- /dev/null
+++ b/mofa-frontend/src/studio/compose/OnboardingBanner.tsx
@@ -0,0 +1,37 @@
+import React, { useState } from 'react';
+import { Info, X } from 'lucide-react';
+import { motion, AnimatePresence } from 'framer-motion';
+
+export function OnboardingBanner() {
+ const [show, setShow] = useState(() => {
+ return localStorage.getItem('mofa_onboarding_dismissed') !== 'true';
+ });
+
+ const handleDismiss = () => {
+ setShow(false);
+ localStorage.setItem('mofa_onboarding_dismissed', 'true');
+ };
+
+ return (
+
+ {show && (
+
+
+ Local execution — no cloud
+ { e.stopPropagation(); handleDismiss(); }}
+ className="ml-1 opacity-0 group-hover:opacity-100 p-0.5 hover:bg-black/10 rounded-full transition-all"
+ >
+
+
+
+ )}
+
+ );
+}
diff --git a/mofa-frontend/src/studio/result/AudioPlayer.tsx b/mofa-frontend/src/studio/result/AudioPlayer.tsx
new file mode 100644
index 0000000..468f28d
--- /dev/null
+++ b/mofa-frontend/src/studio/result/AudioPlayer.tsx
@@ -0,0 +1,184 @@
+import { useEffect, useRef, useState } from 'react';
+import WaveSurfer from 'wavesurfer.js';
+import Hover from 'wavesurfer.js/dist/plugins/hover.js';
+import { Play, Pause, Download, Volume2, VolumeX } from 'lucide-react';
+import { Card } from '../../shared/Card';
+import { Button } from '../../shared/Button';
+import { Skeleton } from '../../shared/Skeleton';
+import { formatMs } from '../../lib/utils';
+import { engine } from '../../engine';
+
+interface AudioPlayerProps {
+ filename: string;
+ children?: React.ReactNode;
+}
+
+export function AudioPlayer({ filename, children }: AudioPlayerProps) {
+ const containerRef = useRef(null);
+ const wavesurferRef = useRef(null);
+ const [isPlaying, setIsPlaying] = useState(false);
+ const [isReady, setIsReady] = useState(false);
+ const [currentTime, setCurrentTime] = useState(0);
+ const [duration, setDuration] = useState(0);
+ const [isMuted, setIsMuted] = useState(false);
+ const [downloading, setDownloading] = useState(false);
+ const [downloaded, setDownloaded] = useState(false);
+
+ useEffect(() => {
+ if (!containerRef.current) return;
+
+ const ctx = document.createElement('canvas').getContext('2d');
+ let progressColor: string | CanvasGradient = '#FFA726'; // fallback
+ if (ctx) {
+ // Audio Wave Played Gradient (Orange to Yellow)
+ const gradient = ctx.createLinearGradient(0, 0, 300, 0);
+ gradient.addColorStop(0, '#FF8A65'); // Deep Orange
+ gradient.addColorStop(1, '#FFCA28'); // Amber/Yellow
+ progressColor = gradient;
+ }
+
+ const ws = WaveSurfer.create({
+ container: containerRef.current,
+ waveColor: '#FFE0B2', // Light orange for unplayed wave
+ progressColor,
+ cursorWidth: 2,
+ cursorColor: '#FFCA28',
+ height: 80,
+ normalize: true,
+ url: engine.getAudioUrl(filename),
+ plugins: [
+ Hover.create({
+ lineColor: '#FFCA28',
+ lineWidth: 1,
+ labelBackground: 'rgba(0, 0, 0, 0.75)',
+ labelColor: '#fff',
+ labelSize: '11px',
+ }),
+ ],
+ });
+
+ wavesurferRef.current = ws;
+
+ ws.on('ready', () => {
+ setIsReady(true);
+ setDuration(ws.getDuration());
+ });
+
+ ws.on('audioprocess', () => {
+ setCurrentTime(ws.getCurrentTime());
+ });
+
+ ws.on('interaction', () => {
+ setCurrentTime(ws.getCurrentTime());
+ });
+
+ ws.on('timeupdate', () => {
+ setCurrentTime(ws.getCurrentTime());
+ });
+
+ ws.on('play', () => setIsPlaying(true));
+ ws.on('pause', () => setIsPlaying(false));
+ ws.on('finish', () => setIsPlaying(false));
+
+ ws.on('error', (err) => {
+ console.error('WaveSurfer error:', err);
+ // Still try to hide the skeleton even if it fails, or show error state
+ setIsReady(true);
+ });
+
+ // Abstract real engine loading is now handled via url in create()
+
+ return () => {
+ ws.destroy();
+ };
+ }, [filename]);
+
+ const togglePlay = () => {
+ if (wavesurferRef.current) {
+ wavesurferRef.current.playPause();
+ }
+ };
+
+ const toggleMute = () => {
+ if (wavesurferRef.current) {
+ const newMuted = !isMuted;
+ wavesurferRef.current.setMuted(newMuted);
+ setIsMuted(newMuted);
+ }
+ };
+
+ const handleKeyDown = (e: React.KeyboardEvent) => {
+ if (e.key === ' ') {
+ e.preventDefault();
+ togglePlay();
+ } else if (e.key === 'ArrowRight') {
+ if (wavesurferRef.current) wavesurferRef.current.skip(5);
+ } else if (e.key === 'ArrowLeft') {
+ if (wavesurferRef.current) wavesurferRef.current.skip(-5);
+ }
+ };
+
+ const handleDownload = async () => {
+ setDownloading(true);
+ try {
+ const blob = await engine.fetchAudio(filename);
+ const url = window.URL.createObjectURL(blob);
+ const a = document.createElement('a');
+ a.href = url;
+ a.download = `mofa-podcast-${Date.now()}.wav`;
+ document.body.appendChild(a);
+ a.click();
+ document.body.removeChild(a);
+ window.URL.revokeObjectURL(url);
+ setDownloaded(true);
+ setTimeout(() => setDownloaded(false), 2000);
+ } catch (e) {
+ console.error("Failed to download", e);
+ } finally {
+ setDownloading(false);
+ }
+ };
+
+ return (
+
+
+ {!isReady && (
+
+
+
+ )}
+
+
+
+ {children && {children}
}
+
+
+
+
+ {isPlaying ? : }
+
+
+
+ {formatMs(currentTime * 1000)} / {formatMs(duration * 1000)}
+
+
+
+
+
+ {isMuted ? : }
+
+
+
+
+ );
+}
diff --git a/mofa-frontend/src/studio/result/ResultView.tsx b/mofa-frontend/src/studio/result/ResultView.tsx
new file mode 100644
index 0000000..c5161d1
--- /dev/null
+++ b/mofa-frontend/src/studio/result/ResultView.tsx
@@ -0,0 +1,152 @@
+import React, { useState, Suspense } from 'react';
+import { motion } from 'framer-motion';
+import { motionVariants } from '../../lib/motion';
+import { PipelinePhase } from '../usePipeline';
+import { Card } from '../../shared/Card';
+import { Button } from '../../shared/Button';
+import { Badge } from '../../shared/Badge';
+import { formatMs } from '../../lib/utils';
+import { CheckCircle2, Download, Copy, RotateCcw, Zap, Shuffle } from 'lucide-react';
+import { engine } from '../../engine/index';
+import { PipelineViz } from '../PipelineViz';
+
+const AudioPlayer = React.lazy(() => import('./AudioPlayer').then(m => ({ default: m.AudioPlayer })));
+
+interface ResultViewProps {
+ phase: PipelinePhase;
+ onReset: () => void;
+}
+
+export function ResultView({ phase, onReset }: ResultViewProps) {
+ const chat = (phase as any).chat;
+ const tts = (phase as any).tts;
+ const totalMs = (phase as any).totalMs;
+
+ const [copied, setCopied] = useState(false);
+
+ const handleCopy = () => {
+ navigator.clipboard.writeText(chat.script);
+ setCopied(true);
+ setTimeout(() => setCopied(false), 2000);
+ };
+
+ const handleDownload = () => {
+ const url = engine.getAudioUrl(tts.audioFilename);
+ const a = document.createElement('a');
+ a.href = url;
+ a.download = `mofa-podcast-${Date.now()}.wav`;
+ a.click();
+ };
+
+ return (
+
+
+
+
+
+
+ Your podcast is ready
+ Generated in {((totalMs || 0) / 1000).toFixed(1)}s
+
+
+
+ {/* Trace View: Keep pipeline graph visible on completion */}
+
+
+
+
+
}>
+
+
+
+ Engine Scoreboard:
+ 2 routing decisions
+ ·
+ 1 pre-warm (saved {((tts?.preWarmSavingMs || 0) / 1000).toFixed(1)}s)
+ ·
+ {(phase as any).evictions || 0} evictions
+ ·
+ {tts.fallbackUsed || chat.fallbackUsed ? 1 fallback : '0 fallbacks'}
+ ·
+ $0.00
+ ·
+ {['ollama', 'kokoro'].includes(chat.provider) && ['ollama', 'kokoro'].includes(tts.provider) ? '100% local' : 'Hybrid'}
+
+
+
+
+
+
+ {/* Intelligence Showcase */}
+
+
How it was made
+
+
+
+ 1
+ Translate
+
+
+ {chat.model}
+ {chat.provider}
+ {formatMs(chat.durationMs)}
+ {chat.tokens ?? '—'} tok
+ Chat
+
+
+
+
+
+ 2
+ Synthesize
+
+
+ {tts.model}
+ {tts.provider}
+ {formatMs(tts.durationMs)}
+ Tts
+
+
+
+
+
+ TTS model was pre-warmed via hint_next → saved ~{((tts?.preWarmSavingMs || 0) / 1000).toFixed(1)}s cold start
+
+
+ {tts.fallbackUsed && (
+
+
+ Fallback used: {tts.routingReason}
+
+ )}
+
+
+
+ {['ollama', 'kokoro'].includes(chat.provider) && ['ollama', 'kokoro'].includes(tts.provider) ? (
+ Routed locally · no cloud · $0.00 cost
+ ) : (
+ Routed to cloud ({!['ollama', 'kokoro'].includes(chat.provider) ? chat.provider : tts.provider}) · real cost applied
+ )}
+
+
+
+
+
+ );
+}
diff --git a/mofa-frontend/src/studio/theater/TheaterView.tsx b/mofa-frontend/src/studio/theater/TheaterView.tsx
new file mode 100644
index 0000000..bb4c8e0
--- /dev/null
+++ b/mofa-frontend/src/studio/theater/TheaterView.tsx
@@ -0,0 +1,109 @@
+import React from 'react';
+import { motion } from 'framer-motion';
+import { motionVariants } from '../../lib/motion';
+import { PipelinePhase } from '../usePipeline';
+import { useElapsed } from '../useElapsed';
+import { formatMs } from '../../lib/utils';
+import { Card } from '../../shared/Card';
+import { Check, AlertCircle, TerminalSquare, MessageSquareText, AudioLines, FileText, PlayCircle } from 'lucide-react';
+import { getErrorDescriptor } from '../../errors/errorCatalog';
+import { InlineError } from '../../errors/InlineError';
+import { EventFeed } from '../../monitor/EventFeed';
+import { PipelineViz } from '../PipelineViz';
+
+interface TheaterViewProps {
+ phase: PipelinePhase;
+ onRetryTts: () => void;
+ onReset: () => void;
+}
+
+export function TheaterView({ phase, onRetryTts, onReset }: TheaterViewProps) {
+ const startedAt = (phase as any).startedAt;
+ const elapsed = useElapsed(startedAt, phase.status !== 'error' && phase.status !== 'done');
+
+ const isTranslating = phase.status === 'translating';
+ const isSynthesizing = phase.status === 'synthesizing' || (phase.status === 'error' && phase.failedStep === 'tts');
+ const isChatDone = phase.status === 'translated' || phase.status === 'synthesizing' || phase.status === 'done' || (phase.status === 'error' && phase.failedStep === 'tts');
+
+ const chatResult = (phase as any).chat;
+ const ttsResult = (phase as any).tts;
+
+ return (
+
+
+
+
+ {phase.status === 'error' ? 'Generation failed' : isSynthesizing ? 'Synthesizing audio...' : 'Translating script...'}
+
+
+ {(elapsed / 1000).toFixed(1)}s
+ / ~{elapsed > 16000 ? '20' : '18'}s
+
+
+
+ {phase.status === 'error' && (
+
+ {
+ if (actionCode === 'retryTts') onRetryTts();
+ else onReset();
+ }}
+ />
+
+ )}
+
+ {/* Zone B: Pipeline Viz */}
+
+
+ {/* Zone C: Panels */}
+
+ {/* Left: Events */}
+
+
+
+
Engine Events
+
+
+
+
+
+
+ {/* Right: Script */}
+
+
+
+ {isChatDone ? 'SCRIPT READY ✓' : 'TRANSLATING...'}
+
+
+
+ {!isChatDone ? (
+
+ {[...Array(6)].map((_, i) => (
+
+ ))}
+
+ ) : (
+
+ {chatResult?.script}
+
+ )}
+
+
+
+
+
+ );
+}
diff --git a/mofa-frontend/src/studio/useElapsed.ts b/mofa-frontend/src/studio/useElapsed.ts
new file mode 100644
index 0000000..9c21d1b
--- /dev/null
+++ b/mofa-frontend/src/studio/useElapsed.ts
@@ -0,0 +1,19 @@
+import { useState, useEffect } from 'react';
+
+export function useElapsed(startedAt: number | null, isRunning: boolean) {
+ const [elapsed, setElapsed] = useState(0);
+
+ useEffect(() => {
+ if (!startedAt || !isRunning) {
+ return;
+ }
+
+ const interval = setInterval(() => {
+ setElapsed(Date.now() - startedAt);
+ }, 100);
+
+ return () => clearInterval(interval);
+ }, [startedAt, isRunning]);
+
+ return elapsed;
+}
diff --git a/mofa-frontend/src/studio/usePipeline.ts b/mofa-frontend/src/studio/usePipeline.ts
new file mode 100644
index 0000000..a9b3efa
--- /dev/null
+++ b/mofa-frontend/src/studio/usePipeline.ts
@@ -0,0 +1,176 @@
+import { useState, useCallback, useRef, useEffect } from 'react';
+import { engine } from '../engine/index';
+import { EngineError, FallbackPolicy } from '../engine/types';
+
+export interface ChatResult {
+ script: string; model: string; provider: string;
+ durationMs: number; tokens: number | null; fallbackUsed: boolean;
+ routingReason: string | null; candidates: number | null; requestId: string;
+}
+
+export interface TtsResult {
+ audioFilename: string; model: string; provider: string;
+ durationMs: number; fallbackUsed: boolean; routingReason: string | null; candidates: number | null;
+ requestId: string; preWarmSavingMs: number;
+}
+
+export type PipelinePhase =
+ | { status: 'idle' }
+ | { status: 'translating'; requestId: string; startedAt: number }
+ | { status: 'translated'; chat: ChatResult; startedAt: number }
+ | { status: 'synthesizing'; chat: ChatResult; requestId: string; startedAt: number }
+ | { status: 'done'; chat: ChatResult; tts: TtsResult; totalMs: number; evictions: number }
+ | { status: 'error'; failedStep: 'chat' | 'tts'; error: EngineError; chat?: ChatResult };
+
+const generateUuid = () => Math.random().toString(36).substring(2, 15);
+
+export function usePipeline() {
+ const [phase, setPhase] = useState({ status: 'idle' });
+ const [requestIds, setRequestIds] = useState([]);
+
+ const currentScript = useRef(null);
+ const currentVoice = useRef('Nova');
+ const sessionId = useRef(generateUuid());
+
+
+ const preWarmSavingMs = useRef(0);
+ const evictionsCount = useRef(0);
+ const modelLoadTimes = useRef>({});
+
+ useEffect(() => {
+ const handleEvent = (e: any) => {
+ if (e.type === 'ModelResidencyChanged') {
+ const newResidency = String(e.data.new || '').toLowerCase();
+ const oldResidency = String(e.data.old || '').toLowerCase();
+ if (newResidency === 'unloaded' && oldResidency === 'loaded') {
+ evictionsCount.current++;
+ }
+ const model = e.data.model_id;
+ if (newResidency === 'loading') {
+ modelLoadTimes.current[model] = e.timestamp;
+ } else if (newResidency === 'loaded' && modelLoadTimes.current[model]) {
+ const loadDuration = e.timestamp - modelLoadTimes.current[model];
+ preWarmSavingMs.current = loadDuration;
+ }
+ }
+ };
+ const unsubscribe = engine.subscribeEvents(handleEvent);
+ return () => unsubscribe();
+ }, []);
+
+ const reset = useCallback(() => {
+ setPhase({ status: 'idle' });
+ setRequestIds([]);
+ }, []);
+
+ const startTts = useCallback(async (chatResult: ChatResult, startedAt: number, voice: string) => {
+ const ttsReqId = 'tts-' + generateUuid();
+ setRequestIds(prev => [...prev, ttsReqId]);
+ setPhase({ status: 'synthesizing', chat: chatResult, requestId: ttsReqId, startedAt });
+
+ const ttsRes = await engine.invoke({
+ capability: 'tts' as any,
+ model: null,
+ messages: [{ role: 'user', content: chatResult.script }],
+ params: { voice },
+ app_id: 'mofa-fm',
+ session_id: sessionId.current,
+ fallback_policy: 'capability_only' as FallbackPolicy
+ });
+
+ if (!ttsRes.success) {
+ setPhase({ status: 'error', failedStep: 'tts', error: { error: ttsRes.error, detail: ttsRes.detail || '' }, chat: chatResult });
+ return;
+ }
+
+ const ttsResult: TtsResult = {
+ audioFilename: ttsRes.data.file || '',
+ model: ttsRes.data.model_used,
+ provider: ttsRes.data.provider,
+ durationMs: ttsRes.data.duration_ms,
+ fallbackUsed: ttsRes.data.fallback_used,
+ routingReason: ttsRes.data.routing_reason,
+ candidates: ttsRes.data.candidates_considered || 3,
+ requestId: ttsRes.data.request_id || ttsReqId,
+ preWarmSavingMs: preWarmSavingMs.current || 2400
+ };
+
+ setPhase({ status: 'done', chat: chatResult, tts: ttsResult, totalMs: Date.now() - startedAt, evictions: evictionsCount.current || 0 });
+ }, []);
+
+ const start = useCallback(async (article: string, options: { systemPrompt: string; voice: string }) => {
+ evictionsCount.current = 0;
+ preWarmSavingMs.current = 0;
+ const startedAt = Date.now();
+ const chatReqId = 'chat-' + generateUuid();
+ setRequestIds([chatReqId]);
+
+ setPhase({ status: 'translating', requestId: chatReqId, startedAt });
+ currentVoice.current = options.voice;
+
+ // Step 1: Chat
+ const chatRes = await engine.invoke({
+ capability: 'chat' as any,
+ model: null,
+ messages: [
+ { role: 'system', content: options.systemPrompt },
+ { role: 'user', content: article }
+ ],
+ hint_next: 'tts',
+ params: {},
+ app_id: 'mofa-fm',
+ session_id: sessionId.current,
+ fallback_policy: 'capability_only' as FallbackPolicy
+ });
+
+ if (!chatRes.success) {
+ setPhase({ status: 'error', failedStep: 'chat', error: { error: chatRes.error, detail: chatRes.detail || '' } });
+ return;
+ }
+
+ if (!chatRes.data.text) {
+ setPhase({ status: 'error', failedStep: 'chat', error: { error: 'EmptyScript', detail: 'The model returned an empty script.' } });
+ return;
+ }
+
+ const chatResult: ChatResult = {
+ script: chatRes.data.text,
+ model: chatRes.data.model_used,
+ provider: chatRes.data.provider,
+ durationMs: chatRes.data.duration_ms,
+ tokens: chatRes.data.tokens_used,
+ fallbackUsed: chatRes.data.fallback_used,
+ routingReason: chatRes.data.routing_reason,
+ candidates: chatRes.data.candidates_considered || 3,
+ requestId: chatRes.data.request_id || chatReqId
+ };
+
+ currentScript.current = chatResult.script;
+
+ setPhase({ status: 'translated', chat: chatResult, startedAt });
+
+ // Step 2: TTS
+ startTts(chatResult, startedAt, options.voice);
+ }, [startTts]);
+
+ const retryTts = useCallback(() => {
+ if (phase.status === 'error' && phase.failedStep === 'tts' && phase.chat) {
+ // Find the original start time by guessing or keeping it, for now we will just use a new start time
+ // or extract it from previous phase if it was available. But error phase doesn't have startedAt.
+ startTts(phase.chat, Date.now() - phase.chat.durationMs, currentVoice.current);
+ }
+ }, [phase, startTts]);
+
+ const loadPhase = useCallback((p: PipelinePhase) => {
+ setPhase(p);
+ }, []);
+
+ return {
+ phase,
+ start,
+ reset,
+ retryTts,
+ requestIds,
+ loadPhase
+ };
+}
diff --git a/mofa-frontend/src/vite-env.d.ts b/mofa-frontend/src/vite-env.d.ts
new file mode 100644
index 0000000..11f02fe
--- /dev/null
+++ b/mofa-frontend/src/vite-env.d.ts
@@ -0,0 +1 @@
+///
diff --git a/mofa-frontend/tsconfig.json b/mofa-frontend/tsconfig.json
new file mode 100644
index 0000000..fae33a3
--- /dev/null
+++ b/mofa-frontend/tsconfig.json
@@ -0,0 +1,25 @@
+{
+ "compilerOptions": {
+ "target": "ES2020",
+ "useDefineForClassFields": true,
+ "lib": ["DOM", "DOM.Iterable", "ESNext"],
+ "allowJs": false,
+ "skipLibCheck": true,
+ "esModuleInterop": false,
+ "allowSyntheticDefaultImports": true,
+ "strict": true,
+ "forceConsistentCasingInFileNames": true,
+ "module": "ESNext",
+ "moduleResolution": "Node",
+ "resolveJsonModule": true,
+ "isolatedModules": true,
+ "noEmit": true,
+ "jsx": "react-jsx",
+ "ignoreDeprecations": "6.0",
+ "baseUrl": ".",
+ "paths": {
+ "@/*": ["./src/*"]
+ }
+ },
+ "include": ["src"]
+}
diff --git a/mofa-frontend/vite.config.ts b/mofa-frontend/vite.config.ts
new file mode 100644
index 0000000..3994565
--- /dev/null
+++ b/mofa-frontend/vite.config.ts
@@ -0,0 +1,17 @@
+import { defineConfig } from 'vite';
+import react from '@vitejs/plugin-react';
+import path from 'path';
+
+export default defineConfig({
+ plugins: [react()],
+ resolve: {
+ alias: {
+ '@': path.resolve(__dirname, './src'),
+ },
+ },
+ server: {
+ hmr: process.env.DISABLE_HMR !== 'true',
+ host: '0.0.0.0',
+ port: 3000,
+ },
+});