diff --git a/.gitignore b/.gitignore index e69de29..7cf7be9 100644 --- a/.gitignore +++ b/.gitignore @@ -0,0 +1,143 @@ +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +lerna-debug.log* + +# Diagnostic reports (https://nodejs.org/api/report.html) +report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json + +# Runtime data +pids +*.pid +*.seed +*.pid.lock + +# Directory for instrumented libs generated by jscoverage/JSCover +lib-cov + +# Coverage directory used by tools like istanbul +coverage +*.lcov + +# nyc test coverage +.nyc_output + +# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) +.grunt + +# Bower dependency directory (https://bower.io/) +bower_components + +# node-waf configuration +.lock-wscript + +# Compiled binary addons (https://nodejs.org/api/addons.html) +build/Release + +# Dependency directories +node_modules/ +jspm_packages/ + +# Snowpack dependency directory (https://snowpack.dev/) +web_modules/ + +# TypeScript cache +*.tsbuildinfo + +# Optional npm cache directory +.npm + +# Optional eslint cache +.eslintcache + +# Optional stylelint cache +.stylelintcache + +# Optional REPL history +.node_repl_history + +# Output of 'npm pack' +*.tgz + +# Yarn Integrity file +.yarn-integrity + +# dotenv environment variable files +.env +.env.* +!.env.example + +# parcel-bundler cache (https://parceljs.org/) +.cache +.parcel-cache + +# Next.js build output +.next +out + +# Nuxt.js build / generate output +.nuxt +dist +.output + +# Gatsby files +.cache/ +# Comment in the public line in if your project uses Gatsby and not Next.js +# https://nextjs.org/blog/next-9-1#public-directory-support +# public + +# vuepress build output +.vuepress/dist + +# vuepress v2.x temp directory +.temp + +# Sveltekit cache directory +.svelte-kit/ + +# vitepress build output +**/.vitepress/dist + +# vitepress cache directory +**/.vitepress/cache + +# Docusaurus cache and generated files +.docusaurus + +# Serverless directories +.serverless/ + +# FuseBox cache +.fusebox/ + +# DynamoDB Local files +.dynamodb/ + +# Firebase cache directory +.firebase/ + +# TernJS port file +.tern-port + +# Stores VSCode versions used for testing VSCode extensions +.vscode-test + +# pnpm +.pnpm-store + +# yarn v3 +.pnp.* +.yarn/* +!.yarn/patches +!.yarn/plugins +!.yarn/releases +!.yarn/sdks +!.yarn/versions + +# Vite files +vite.config.js.timestamp-* +vite.config.ts.timestamp-* +.vite/ \ No newline at end of file diff --git a/TODO.md b/TODO.md new file mode 100644 index 0000000..f9b56c3 --- /dev/null +++ b/TODO.md @@ -0,0 +1,42 @@ +# TODO + +## Prochaines tâches frontend + +### Édition des nœuds Contrôle (nesting) + +Le modèle de graphe actuel est plat (`script: Block[]`, aucun champ `children`). +L'API accepte déjà `children: PipelineNode[]` sur chaque nœud — on envoie `[]` partout pour l'instant. + +Ce qu'il faut faire : +- Ajouter `children: Block[]` sur le type `Block` dans `blockHelpers.ts` +- Gérer l'imbrication dans le store (`addBlock` dans un parent, `deleteBlock` récursif, etc.) +- Rendre les blocs Contrôle avec une zone enfant dans le canvas +- Envoyer les vrais enfants dans le payload API +- Vérifier `BlockDetail.children_allowed` pour conditionner l'UI + +### Adapter params → segments + +`BlockDetail.params` est un objet libre (`additionalProperties: true` dans le spec). +L'adaptateur dans `src/api/client.ts` (`adaptParam`) fait des hypothèses sur la forme de chaque valeur (`{ default, options? }`). + +À valider dès qu'un vrai catalogue backend est disponible — ajuster si la structure diffère. + +### Bouton "Générer le code" + +`generatePipelineCode(id)` est câblé dans `src/api/client.ts` (`POST /api/pipelines/{id}/generate`). +Pas encore exposé dans l'UI. Ajouter un bouton dans `EditorHeader` qui affiche le code généré. + +### Retry dans la modal "Éditeur non disponible" + +La modal affiche un message d'erreur précis (réseau vs. réponse inattendue) mais propose seulement "Retour". +Ajouter un bouton "Réessayer" qui relance `fetchCatalog()` sans repasser par la home. + +### Nom du pipeline + +Le nom est fixé à `'mon-premier-modèle'` dans `useBlockRunner.ts`. +À terme : champ éditable dans `EditorHeader` (lié à `useAppStore`), envoyé dans `PipelineCreate.name`. + +### Auth + +Aucune gestion de token pour l'instant. `src/api/client.ts` reste sans headers d'auth. +À ajouter quand le backend expose des routes protégées. diff --git a/frontend/.env.example b/frontend/.env.example new file mode 100644 index 0000000..c2058b5 --- /dev/null +++ b/frontend/.env.example @@ -0,0 +1 @@ +VITE_API_BASE_URL=http://localhost:8000 diff --git a/frontend/.gitignore b/frontend/.gitignore new file mode 100644 index 0000000..f06235c --- /dev/null +++ b/frontend/.gitignore @@ -0,0 +1,2 @@ +node_modules +dist diff --git a/frontend/index.html b/frontend/index.html new file mode 100644 index 0000000..3570690 --- /dev/null +++ b/frontend/index.html @@ -0,0 +1,15 @@ + + + + + + MLBlock + + + + + +
+ + + diff --git a/frontend/package-lock.json b/frontend/package-lock.json new file mode 100644 index 0000000..4b0be2f --- /dev/null +++ b/frontend/package-lock.json @@ -0,0 +1,2108 @@ +{ + "name": "mlblock-frontend", + "version": "0.1.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "mlblock-frontend", + "version": "0.1.0", + "dependencies": { + "axios": "^1.7.2", + "react": "^18.3.1", + "react-dom": "^18.3.1", + "zustand": "^4.5.4" + }, + "devDependencies": { + "@types/react": "^19.2.17", + "@types/react-dom": "^19.2.3", + "@vitejs/plugin-react": "^4.3.1", + "typescript": "^6.0.3", + "vite": "^5.3.5" + } + }, + "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/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-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-plugin-utils": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.29.7.tgz", + "integrity": "sha512-G7sHYigPY17oO5SYWnfD/0MTBwVR781S/JI643e/JhUYgVgWE/61SoW3NH9KWUKyKq5LVh3npif99Wkt6j86Jw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.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/plugin-transform-react-jsx-self": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.29.7.tgz", + "integrity": "sha512-TL0hMc9xzy86VD31nUiwzd5otRAcyEPcsegCxolO0PvcXuH1v0kECe/UIznYFihpkvU5wg/jk4v0TTEFfm53fw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx-source": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-source/-/plugin-transform-react-jsx-source-7.29.7.tgz", + "integrity": "sha512-06IyK09H3wi4cGbhDBwp5gUGo0IKtnYa8tyTiephirPCK6fbobVGiXMMI5zLQ4aKEYP3wZ3ArU44o+8KMrSG/Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.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/@esbuild/aix-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz", + "integrity": "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.21.5.tgz", + "integrity": "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.21.5.tgz", + "integrity": "sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.21.5.tgz", + "integrity": "sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.21.5.tgz", + "integrity": "sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.21.5.tgz", + "integrity": "sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.21.5.tgz", + "integrity": "sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.21.5.tgz", + "integrity": "sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.21.5.tgz", + "integrity": "sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.21.5.tgz", + "integrity": "sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.21.5.tgz", + "integrity": "sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.21.5.tgz", + "integrity": "sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.21.5.tgz", + "integrity": "sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.21.5.tgz", + "integrity": "sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.21.5.tgz", + "integrity": "sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.21.5.tgz", + "integrity": "sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.21.5.tgz", + "integrity": "sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz", + "integrity": "sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz", + "integrity": "sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.21.5.tgz", + "integrity": "sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.21.5.tgz", + "integrity": "sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.21.5.tgz", + "integrity": "sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.21.5.tgz", + "integrity": "sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "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/@rolldown/pluginutils": { + "version": "1.0.0-beta.27", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.27.tgz", + "integrity": "sha512-+d0F4MKMCbeVUJwG96uQ4SgAznZNSq93I3V+9NHA4OpvqG8mRCpGdKmK8l/dl02h2CCDHwW2FqilnTyDcAnqjA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.62.2.tgz", + "integrity": "sha512-6o7ZLZK+BeenkZCFNDXqpbjw9bD6nuWonvS/lwQJp7NoVVxm6p3qE7qQ5jGuBjiFsgvqjD8mZAU5oWxTmbOeOg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.62.2.tgz", + "integrity": "sha512-BaH7BllCACHoH1LguOU56UItGfUWjujlO65kS9LAodViaN4bwIKd7oeW/ZHJ/4ljr/7MIiENnNy3HJ0zXv8Zkw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.62.2.tgz", + "integrity": "sha512-v39RCCvj4He82I9sFmk+M1VZ0PLM9sfsLVikjfx2hYBNALhrrOR2D3JjQA6AhlaSOgcR+RzrKY7e1+bT6SUO/A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.62.2.tgz", + "integrity": "sha512-yl0y2vq3S3lHeuXhEdss6TWfKW8vkujImO12tn4ZkG/4oghr09LvdYm2RElVjokTQiUvDUGXLGsYeLqUMCKpGA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.62.2.tgz", + "integrity": "sha512-tT4pvt4qXD+vEoezupCWi+a1F0vvDiksiHc+PxRlYTOH1I6/X4id9jPxTP+Fg+545euaFT1jJVs4CEdHZAU1vw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.62.2.tgz", + "integrity": "sha512-6nU5F2wCW+qvCBhTn1pdIU3bzsIoF7EUwsCDRxilWGprQR6yd508YnH9+OKFCwpfS8pjZqDUmnCAr7exax0XCg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.62.2.tgz", + "integrity": "sha512-n1GJHPOvpIfhi3TmrCeh6S6URt9BFCt0KQE3qvexyGCTAKpR4Lg+eWvNZEqu7epxwus/8ElT3hacYEucm49SZg==", + "cpu": [ + "arm" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.62.2.tgz", + "integrity": "sha512-JqgflS8wEB+UXV/vS1RpRbifGBeN4D5lz8D8oOFbFZw4vedvdOgCFAjfBmIMdW3yL10XpQQ0Ambepw6MXrhOnA==", + "cpu": [ + "arm" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.62.2.tgz", + "integrity": "sha512-wnFJkogWvN4jm/hQRF2UBaeUmk20j5+DmHvoyWii2b8HJDyvz1MF2OU/6ynXt2KR63rbZLWkFpoytpdc/yBuSA==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.62.2.tgz", + "integrity": "sha512-HVu2bp0zhvJ8xHEV9+UUs7S90VadmBSY3LcIMvozbPo4AuMGDWlz3ymHLHZPX4hR67TKTt8Qp5PJ5RBg/i+RMQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.62.2.tgz", + "integrity": "sha512-mQqqAV8QaoSgr9I2fKDLY2BAVvmKjWoGiu/cSYQonsLvtqwEn1E4QYfnCOcp5zoEqNhsDYin1s6jx/VJmrxlZg==", + "cpu": [ + "loong64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-musl": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.62.2.tgz", + "integrity": "sha512-IxKLoxCQ2IWi6bT2akyDUBGsOImDKB+sPp4EsTmwFQ/fMwpCKm8uLSSgP/Kx/QYUgKis6SEZ5/Nlhup0DIA0PQ==", + "cpu": [ + "loong64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.62.2.tgz", + "integrity": "sha512-Mk5ha2RQSgyFfmYYLkBpPnUk8D8FriBxesO1u9O75X0mHgXL1UQcH5Itl2lurWL2tj0RxV9b9tJgipac0hRY9A==", + "cpu": [ + "ppc64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-musl": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.62.2.tgz", + "integrity": "sha512-CjvEnqJL/0/TQ3TXX3OPIJ/kmBellrWd4heXUmHeJlTnmwjKpSJzoehLaL6Xk0ZnMHBu9dZuFADNOrtjF4v+2w==", + "cpu": [ + "ppc64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.62.2.tgz", + "integrity": "sha512-1SiZbzwdkaDURsew/tSOrooKiYy7EQGT6m8ufavAi9NEyQb/6VuIxFXAL1fqa4iZe3g4NbNk4P7J32z2tw5Mgg==", + "cpu": [ + "riscv64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.62.2.tgz", + "integrity": "sha512-nQts12zJ3NQRoE6uYljOH89v7szzLDvG2JD/vsX+vGXU8w/At1GowTZ5/7qeFQ8m7L55rpR8Okugnuo5bgjy2Q==", + "cpu": [ + "riscv64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.62.2.tgz", + "integrity": "sha512-E9/ll019jhPIJgpzfZoIkBGhcz+kKNgVWYRY0zr9srBdPPFVpvOKW8VaJKUbeK+eZXyQF9ltME+Kk6affeaPgg==", + "cpu": [ + "s390x" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.62.2.tgz", + "integrity": "sha512-5BqxR/pshjey51iliyzTD5Xi3EN0aLmQ2lZ3lvefVV9c82BvrLo2/6OT55iifpWBufs6kdwWbuOKS841DrmK9A==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.62.2.tgz", + "integrity": "sha512-uNN83XxQrRAh/w0/pmAfibcwyb6YWt4gP+dpnQKPVJshAloQ785ii8CT8ZCIxkGg9opVsvAlGhFitSm6D1Jjpg==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-openbsd-x64": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.62.2.tgz", + "integrity": "sha512-srjEIxSH3LRnJN6THczDHWQplqEMFiAJrTab0msUryh9kwNpkICf3Ea6q6MN/2cZwRFUNx5w+h6Hpi4QuHS6Zg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ] + }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.62.2.tgz", + "integrity": "sha512-8hOJnxgbyObnCm5AlRA3A931xX19xq80RjVTKgJOvEKWqJruP/Uf12IbAOaDjjEXYRewwHLfmF0YRIdK3OwKWA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.62.2.tgz", + "integrity": "sha512-mmF4AY1i0hG/bLWUctUq59gtmgaSIRa3cu/A3JFRp/sCNEme2bgDEiDS22P9FbnJB8NJNF4jPJiSP5RHQpUTDg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.62.2.tgz", + "integrity": "sha512-DZgkknc6jhHrk46V25vbAM0zZkyP0nSDkJB8/dRkLTxv470dOmWDqGoEJl/9A0dFfS7yE3REOwNDxpHwSLSt0Q==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.62.2.tgz", + "integrity": "sha512-T6xr6ucWSFto+VGajA8YH26LdpHRuP4YLHEKAtCWvJDOlnmWcDZVCI2Jmjr+IFHDlt2zRaTAKE4tfjTaWLgJBg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.62.2.tgz", + "integrity": "sha512-BfzEnDJOt9T8M989/lA37EcJgat01wLRnoi5dQf3QzOH7jzpqTAzdDbVfRljVr5r+jzKqpbHeyOfAaXxAd0PAA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@types/babel__core": { + "version": "7.20.5", + "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", + "integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.20.7", + "@babel/types": "^7.20.7", + "@types/babel__generator": "*", + "@types/babel__template": "*", + "@types/babel__traverse": "*" + } + }, + "node_modules/@types/babel__generator": { + "version": "7.27.0", + "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.27.0.tgz", + "integrity": "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__template": { + "version": "7.4.4", + "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.4.tgz", + "integrity": "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.1.0", + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__traverse": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.28.0.tgz", + "integrity": "sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.28.2" + } + }, + "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/react": { + "version": "19.2.17", + "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.17.tgz", + "integrity": "sha512-MXfmqaVPEVgkBT/aY0aGCkRWWtByiYQXo3xdQ8r5RzuFrPiRn8Gar2tQdXSUQ2GKV3bkXckek89V8wQBY2Q/Aw==", + "devOptional": 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/@vitejs/plugin-react": { + "version": "4.7.0", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-4.7.0.tgz", + "integrity": "sha512-gUu9hwfWvvEDBBmgtAowQCojwZmJ5mcLn3aufeCsitijs3+f2NsrPtlAWIR6OPiqljl96GVCUbLe0HyqIpVaoA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.28.0", + "@babel/plugin-transform-react-jsx-self": "^7.27.1", + "@babel/plugin-transform-react-jsx-source": "^7.27.1", + "@rolldown/pluginutils": "1.0.0-beta.27", + "@types/babel__core": "^7.20.5", + "react-refresh": "^0.17.0" + }, + "engines": { + "node": "^14.18.0 || >=16.0.0" + }, + "peerDependencies": { + "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0" + } + }, + "node_modules/agent-base": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", + "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", + "license": "MIT", + "dependencies": { + "debug": "4" + }, + "engines": { + "node": ">= 6.0.0" + } + }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", + "license": "MIT" + }, + "node_modules/axios": { + "version": "1.18.1", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.18.1.tgz", + "integrity": "sha512-3nTvFlvpn9Zu/RkHUqtc7/+al4UpRW5az71ap5zccp6e8RAYEzhMTecX8Dz1wWDYrPpUoB1HAQEGEAEvUr7S9g==", + "license": "MIT", + "dependencies": { + "follow-redirects": "^1.16.0", + "form-data": "^4.0.5", + "https-proxy-agent": "^5.0.1", + "proxy-from-env": "^2.1.0" + } + }, + "node_modules/baseline-browser-mapping": { + "version": "2.10.38", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.38.tgz", + "integrity": "sha512-31/02mVB4yuQU6adKk5SlY6m+mxDwUq5KZkyYgnLrrKl7TEm1+3PyDtDBz2kOv/wxZz41GHsvV1A/u6RmiyBvw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "baseline-browser-mapping": "dist/cli.cjs" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/browserslist": { + "version": "4.28.4", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.4.tgz", + "integrity": "sha512-MTc8i/x9jBQd1iMw2CFGS+rwMa07eYjLR0CCTLDACl9xhxy+nIs3KeML/biicXtk9JrZ6dnnTatmc7ErPXIxqw==", + "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.38", + "caniuse-lite": "^1.0.30001799", + "electron-to-chromium": "^1.5.376", + "node-releases": "^2.0.48", + "update-browserslist-db": "^1.2.3" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001799", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001799.tgz", + "integrity": "sha512-hG1bReV+OUU+MOqK4t/ZWI0tZOyz3rqS9XuhOUz1cIcbwBKjOyJEJuw9ER5JuNyqxNk8u/JUVbGibBOL1yrjFw==", + "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/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "license": "MIT", + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "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/csstype": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", + "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", + "devOptional": 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==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "license": "MIT", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/electron-to-chromium": { + "version": "1.5.378", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.378.tgz", + "integrity": "sha512-VinvOAuuPmdD1guEgGv5f2Qp7/vlfqOrUOMYNnOD4wj3pit8kRsQHzfIf6teyUGWo15Tg5+bOJaRunvyltpVWQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-object-atoms": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.2.tgz", + "integrity": "sha512-HWcBoN6NileqtSydK2FqHbS/LoDd2pqrnQHLyJzBj4kOp/ky2MWMN694xOfkK8/SnUsW2DH7EfyVlydKCsm1Zw==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-set-tostringtag": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", + "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/esbuild": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.21.5.tgz", + "integrity": "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=12" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.21.5", + "@esbuild/android-arm": "0.21.5", + "@esbuild/android-arm64": "0.21.5", + "@esbuild/android-x64": "0.21.5", + "@esbuild/darwin-arm64": "0.21.5", + "@esbuild/darwin-x64": "0.21.5", + "@esbuild/freebsd-arm64": "0.21.5", + "@esbuild/freebsd-x64": "0.21.5", + "@esbuild/linux-arm": "0.21.5", + "@esbuild/linux-arm64": "0.21.5", + "@esbuild/linux-ia32": "0.21.5", + "@esbuild/linux-loong64": "0.21.5", + "@esbuild/linux-mips64el": "0.21.5", + "@esbuild/linux-ppc64": "0.21.5", + "@esbuild/linux-riscv64": "0.21.5", + "@esbuild/linux-s390x": "0.21.5", + "@esbuild/linux-x64": "0.21.5", + "@esbuild/netbsd-x64": "0.21.5", + "@esbuild/openbsd-x64": "0.21.5", + "@esbuild/sunos-x64": "0.21.5", + "@esbuild/win32-arm64": "0.21.5", + "@esbuild/win32-ia32": "0.21.5", + "@esbuild/win32-x64": "0.21.5" + } + }, + "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/follow-redirects": { + "version": "1.16.0", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.16.0.tgz", + "integrity": "sha512-y5rN/uOsadFT/JfYwhxRS5R7Qce+g3zG97+JrtFZlC9klX/W5hD7iiLzScI4nZqUS7DNUdhPgw4xI8W2LuXlUw==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/RubenVerborgh" + } + ], + "license": "MIT", + "engines": { + "node": ">=4.0" + }, + "peerDependenciesMeta": { + "debug": { + "optional": true + } + } + }, + "node_modules/form-data": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.6.tgz", + "integrity": "sha512-vKatAh4SlVfgbv+YtmhiRjhEMJsYpsG1Y2rMQtR+SVSbytsSD1YGzDIcrAJmdFec88u/+VoGmxnl+80gL1tRCQ==", + "license": "MIT", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "es-set-tostringtag": "^2.1.0", + "hasown": "^2.0.4", + "mime-types": "^2.1.35" + }, + "engines": { + "node": ">= 6" + } + }, + "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/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "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/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-tostringtag": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", + "license": "MIT", + "dependencies": { + "has-symbols": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.4.tgz", + "integrity": "sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==", + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/https-proxy-agent": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz", + "integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==", + "license": "MIT", + "dependencies": { + "agent-base": "6", + "debug": "4" + }, + "engines": { + "node": ">= 6" + } + }, + "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==", + "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/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/loose-envify": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", + "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", + "license": "MIT", + "dependencies": { + "js-tokens": "^3.0.0 || ^4.0.0" + }, + "bin": { + "loose-envify": "cli.js" + } + }, + "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/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "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==", + "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/node-releases": { + "version": "2.0.49", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.49.tgz", + "integrity": "sha512-f06bl1D+8ZDkn2oOQQKAh5/otFWqVnM1Q5oerA8Pex7UfT66Tx4IPHIqVVFKqFT3FUtaDstdgkM7yT7JWhqxfw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "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/postcss": { + "version": "8.5.15", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.15.tgz", + "integrity": "sha512-FfR8sjd4em2T6fb3I2MwAJU7HWVMr9zba+enmQeeWFfCbm+UOC/0X4DS8XtpUTMwWMGbjKYP7xjfNekzyGmB3A==", + "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/proxy-from-env": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-2.1.0.tgz", + "integrity": "sha512-cJ+oHTW1VAEa8cJslgmUZrc+sjRKgAKl3Zyse6+PV38hZe/V6Z14TbCuXcan9F9ghlz4QrFr2c92TNF82UkYHA==", + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/react": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz", + "integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-dom": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz", + "integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.1.0", + "scheduler": "^0.23.2" + }, + "peerDependencies": { + "react": "^18.3.1" + } + }, + "node_modules/react-refresh": { + "version": "0.17.0", + "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.17.0.tgz", + "integrity": "sha512-z6F7K9bV85EfseRCp2bzrpyQ0Gkw1uLoCel9XBVWPg/TjRj94SkJzUTGfOa4bs7iJvBWtQG0Wq7wnI0syw3EBQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/rollup": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.62.2.tgz", + "integrity": "sha512-RFnrW4lhXA3s3eqHDZvN654g8OTjzRfqpIRJYczCGB6HzphckVAi/Qh4tbPUbRuDi7s1Llv8g/NspLkttY3gTA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "1.0.9" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.62.2", + "@rollup/rollup-android-arm64": "4.62.2", + "@rollup/rollup-darwin-arm64": "4.62.2", + "@rollup/rollup-darwin-x64": "4.62.2", + "@rollup/rollup-freebsd-arm64": "4.62.2", + "@rollup/rollup-freebsd-x64": "4.62.2", + "@rollup/rollup-linux-arm-gnueabihf": "4.62.2", + "@rollup/rollup-linux-arm-musleabihf": "4.62.2", + "@rollup/rollup-linux-arm64-gnu": "4.62.2", + "@rollup/rollup-linux-arm64-musl": "4.62.2", + "@rollup/rollup-linux-loong64-gnu": "4.62.2", + "@rollup/rollup-linux-loong64-musl": "4.62.2", + "@rollup/rollup-linux-ppc64-gnu": "4.62.2", + "@rollup/rollup-linux-ppc64-musl": "4.62.2", + "@rollup/rollup-linux-riscv64-gnu": "4.62.2", + "@rollup/rollup-linux-riscv64-musl": "4.62.2", + "@rollup/rollup-linux-s390x-gnu": "4.62.2", + "@rollup/rollup-linux-x64-gnu": "4.62.2", + "@rollup/rollup-linux-x64-musl": "4.62.2", + "@rollup/rollup-openbsd-x64": "4.62.2", + "@rollup/rollup-openharmony-arm64": "4.62.2", + "@rollup/rollup-win32-arm64-msvc": "4.62.2", + "@rollup/rollup-win32-ia32-msvc": "4.62.2", + "@rollup/rollup-win32-x64-gnu": "4.62.2", + "@rollup/rollup-win32-x64-msvc": "4.62.2", + "fsevents": "~2.3.2" + } + }, + "node_modules/scheduler": { + "version": "0.23.2", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.2.tgz", + "integrity": "sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.1.0" + } + }, + "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/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/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/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/use-sync-external-store": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.6.0.tgz", + "integrity": "sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w==", + "license": "MIT", + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, + "node_modules/vite": { + "version": "5.4.21", + "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.21.tgz", + "integrity": "sha512-o5a9xKjbtuhY6Bi5S3+HvbRERmouabWbyUcpXXUA1u+GNUKoROi9byOJ8M0nHbHYHkYICiMlqxkg1KkYmm25Sw==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "^0.21.3", + "postcss": "^8.4.43", + "rollup": "^4.20.0" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^18.0.0 || >=20.0.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^18.0.0 || >=20.0.0", + "less": "*", + "lightningcss": "^1.21.0", + "sass": "*", + "sass-embedded": "*", + "stylus": "*", + "sugarss": "*", + "terser": "^5.4.0" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + } + } + }, + "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/zustand": { + "version": "4.5.7", + "resolved": "https://registry.npmjs.org/zustand/-/zustand-4.5.7.tgz", + "integrity": "sha512-CHOUy7mu3lbD6o6LJLfllpjkzhHXSBlX8B9+qPddUsIfeF5S/UZ5q0kmCsnRqT1UHFQZchNFDDzMbQsuesHWlw==", + "license": "MIT", + "dependencies": { + "use-sync-external-store": "^1.2.2" + }, + "engines": { + "node": ">=12.7.0" + }, + "peerDependencies": { + "@types/react": ">=16.8", + "immer": ">=9.0.6", + "react": ">=16.8" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "immer": { + "optional": true + }, + "react": { + "optional": true + } + } + } + } +} diff --git a/frontend/package.json b/frontend/package.json new file mode 100644 index 0000000..450d279 --- /dev/null +++ b/frontend/package.json @@ -0,0 +1,24 @@ +{ + "name": "mlblock-frontend", + "version": "0.1.0", + "private": true, + "type": "module", + "scripts": { + "dev": "vite", + "build": "vite build", + "preview": "vite preview" + }, + "dependencies": { + "axios": "^1.7.2", + "react": "^18.3.1", + "react-dom": "^18.3.1", + "zustand": "^4.5.4" + }, + "devDependencies": { + "@types/react": "^19.2.17", + "@types/react-dom": "^19.2.3", + "@vitejs/plugin-react": "^4.3.1", + "typescript": "^6.0.3", + "vite": "^5.3.5" + } +} diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx new file mode 100644 index 0000000..79e8335 --- /dev/null +++ b/frontend/src/App.tsx @@ -0,0 +1,8 @@ +import useAppStore from './store/useAppStore' +import HomePage from './pages/HomePage' +import EditorPage from './pages/EditorPage' + +export default function App() { + const screen = useAppStore(s => s.screen) + return screen === 'build' ? : +} diff --git a/frontend/src/api/client.ts b/frontend/src/api/client.ts new file mode 100644 index 0000000..34971fb --- /dev/null +++ b/frontend/src/api/client.ts @@ -0,0 +1,131 @@ +import axios from 'axios' +import type { + BlockDetail, + BlockSummary, + BlockDef, + BlockDefMap, + Category, + InternalCatalog, + PageResult, + PipelineCreate, + PipelineDetail, + PipelineNode, + PipelineEdge, + ValidationResponse, + BuildResponse, + GenerateResponse, + Segment, +} from '../types/catalog' + +const BASE = import.meta.env.VITE_API_BASE_URL ?? 'http://localhost:8000' + +const http = axios.create({ baseURL: BASE, timeout: 10_000 }) + +const CATEGORY_META: Record = { + data: { name: 'Données', color: '#5B8DEF' }, + prep: { name: 'Prétraitement', color: '#7C67E5' }, + model: { name: 'Modèle', color: '#D97757' }, + train: { name: 'Entraînement', color: '#E8A44A' }, + eval: { name: 'Évaluation', color: '#66C7B0' }, + control: { name: 'Contrôle', color: '#C06B8A' }, +} + +const FALLBACK_COLOR = '#888888' + +function adaptParam(key: string, raw: unknown): Segment { + if (raw !== null && typeof raw === 'object') { + const p = raw as Record + const def = String(p.default ?? '') + if (Array.isArray(p.options) && p.options.length > 0) { + return { t: 'sel', k: key, def, opts: p.options.map(String) } + } + return { t: 'num', k: key, def } + } + return { t: 'num', k: key, def: '' } +} + +function adaptBlockDetail(detail: BlockDetail): { type: string; def: BlockDef } { + const segs: Segment[] = [{ t: 'text', v: detail.label }] + for (const [key, raw] of Object.entries(detail.params)) { + segs.push(adaptParam(key, raw)) + } + return { type: detail.type, def: { cat: detail.category, segs } } +} + +function adaptCategories(apiCategories: Record): Category[] { + return Object.keys(apiCategories).map(id => { + const meta = CATEGORY_META[id] + return { id, name: meta?.name ?? id, color: meta?.color ?? FALLBACK_COLOR } + }) +} + +async function fetchBlockSummariesPage(page: number, size: number): Promise> { + const { data } = await http.get>('/api/blocks', { params: { page, size } }) + return data +} + +async function fetchAllBlockSummaries(): Promise { + const first = await fetchBlockSummariesPage(1, 100) + if (first.pages <= 1) return first.items + const rest = await Promise.all( + Array.from({ length: first.pages - 1 }, (_, i) => fetchBlockSummariesPage(i + 2, 100)) + ) + return [first.items, ...rest.map(p => p.items)].flat() +} + +async function fetchBlockCategories(): Promise> { + const { data } = await http.get>('/api/blocks/categories') + return data +} + +export async function fetchBlockDetail(typeName: string): Promise { + const { data } = await http.get(`/api/blocks/${encodeURIComponent(typeName)}`) + return data +} + +export async function fetchCatalog(): Promise { + const [apiCategories, summaries] = await Promise.all([ + fetchBlockCategories(), + fetchAllBlockSummaries(), + ]) + + const types = [...new Set(summaries.map(s => s.type))] + const details = await Promise.all(types.map(fetchBlockDetail)) + + const blocks: BlockDefMap = {} + for (const detail of details) { + const { type, def } = adaptBlockDetail(detail) + blocks[type] = def + } + + return { categories: adaptCategories(apiCategories), blocks } +} + +export async function createPipeline(data: PipelineCreate): Promise { + const { data: res } = await http.post('/api/pipelines', data) + return res +} + +export async function updatePipeline(id: number, data: PipelineCreate): Promise { + const { data: res } = await http.put(`/api/pipelines/${id}`, data) + return res +} + +export async function deletePipeline(id: number): Promise { + await http.delete(`/api/pipelines/${id}`) +} + +export async function validateGraph(nodes: PipelineNode[], edges: PipelineEdge[]): Promise { + const { data } = await http.post('/api/validate', { nodes, edges }) + return data +} + +export async function buildPipeline(id: number): Promise { + const { data } = await http.post(`/api/pipelines/${id}/build`) + return data +} + +export async function generatePipelineCode(id: number): Promise { + const { data } = await http.post(`/api/pipelines/${id}/generate`) + return data +} diff --git a/frontend/src/components/blocks/.gitkeep b/frontend/src/components/blocks/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/frontend/src/components/blocks/BlockSegments.tsx b/frontend/src/components/blocks/BlockSegments.tsx new file mode 100644 index 0000000..99d1f9f --- /dev/null +++ b/frontend/src/components/blocks/BlockSegments.tsx @@ -0,0 +1,50 @@ +import React from 'react' +import type { Segment } from '../../types/catalog' + +const inputBase: React.CSSProperties = { + background: 'rgba(255,255,255,.9)', border: 'none', borderRadius: 7, + padding: '3px 5px', textAlign: 'center', color: '#2a211c', + fontWeight: 800, fontSize: 13, outline: 'none', +} +const selectBase: React.CSSProperties = { + background: 'rgba(255,255,255,.9)', border: 'none', borderRadius: 7, + padding: '3px 6px', color: '#2a211c', fontWeight: 800, fontSize: 13, + outline: 'none', cursor: 'pointer', +} +const fieldPill: React.CSSProperties = { + background: 'rgba(255,255,255,.85)', padding: '2px 8px', borderRadius: 7, fontWeight: 800, +} + +type BlockSegmentsProps = { + segs: Segment[] + fields?: Record + blockId?: string + onUpdate?: (id: string, k: string, v: string) => void +} + +export default function BlockSegments({ segs, fields, blockId, onUpdate }: BlockSegmentsProps): React.ReactNode { + return segs.map((s, i) => { + if (s.t === 'text') return {s.v} + if (!onUpdate) return {s.def} + if (s.t === 'num') return ( + onUpdate(blockId!, s.k, e.target.value)} + style={{ ...inputBase, width: (s.w || 44) + 'px' }} + /> + ) + if (s.t === 'sel') return ( + + ) + return null + }) +} diff --git a/frontend/src/components/blocks/HatBlock.tsx b/frontend/src/components/blocks/HatBlock.tsx new file mode 100644 index 0000000..6aac3aa --- /dev/null +++ b/frontend/src/components/blocks/HatBlock.tsx @@ -0,0 +1,41 @@ +import { hatBorderRadius } from '../../utils/snapLogic' + +const ACCENT = '#D97757' + +type HatBlockProps = { + hatRef: React.RefObject + hatBand: number | null + n: number + band0: number | null +} + +export default function HatBlock({ hatRef, hatBand, n, band0 }: HatBlockProps) { + const radius = hatBorderRadius(n, hatBand, band0) + const minWidth = hatBand != null ? hatBand + 'px' : undefined + + return ( +
+ + Démarrer le projet +
+
+ ) +} diff --git a/frontend/src/components/blocks/ScriptBlock.tsx b/frontend/src/components/blocks/ScriptBlock.tsx new file mode 100644 index 0000000..4e56cb0 --- /dev/null +++ b/frontend/src/components/blocks/ScriptBlock.tsx @@ -0,0 +1,67 @@ +import useAppStore from '../../store/useAppStore' +import { colorFor } from '../../utils/blockHelpers' +import { blockBorderRadius } from '../../utils/snapLogic' +import BlockSegments from './BlockSegments' +import type { Block } from '../../utils/blockHelpers' + +const blockBase: React.CSSProperties = { + position: 'relative', display: 'flex', alignItems: 'center', + flexWrap: 'wrap', gap: 6, color: '#2a211c', fontWeight: 800, + fontSize: 14, boxShadow: '0 2px 0 rgba(0,0,0,.18)', userSelect: 'none', +} + +type ScriptBlockProps = { + block: Block + index: number + n: number + bands: (number | null)[] + hatBand: number | null + blockElsRef: React.MutableRefObject> + startBlockDrag: (id: string, e: React.PointerEvent) => void +} + +export default function ScriptBlock({ block, index, n, bands, hatBand, blockElsRef, startBlockDrag }: ScriptBlockProps) { + const runningId = useAppStore(s => s.runningId) + const drag = useAppStore(s => s.drag) + const updateField = useAppStore(s => s.updateField) + const deleteBlock = useAppStore(s => s.deleteBlock) + const catalog = useAppStore(s => s.catalog) + + const d = catalog?.blocks[block.type] + if (!d) return null + + const color = colorFor(d.cat, catalog!.categories) + const isRunning = runningId === block.id + const dragging = drag?.source === 'script' && drag?.id === block.id + const dropHere = drag?.active && drag?.overCanvas && drag?.moved && drag?.insertIndex === index + + return ( +
{ if (el) blockElsRef.current[block.id] = el }} + onPointerDown={e => startBlockDrag(block.id, e)} + style={{ + ...blockBase, + background: color, + padding: '15px 32px 14px 17px', + borderRadius: blockBorderRadius(index, n, bands, hatBand), + minWidth: bands[index] != null ? bands[index] + 'px' : undefined, + marginTop: dropHere ? '34px' : '0px', + opacity: dragging ? 0.3 : 1, + cursor: 'grab', + minHeight: 42, + zIndex: dragging ? 1000 : (n - index), + animation: isRunning ? 'mlbGlow .9s ease-in-out infinite' : 'none', + }} + > +
+ {index < n - 1 && ( +
+ )} + + +
+ ) +} diff --git a/frontend/src/components/canvas/.gitkeep b/frontend/src/components/canvas/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/frontend/src/components/canvas/Canvas.tsx b/frontend/src/components/canvas/Canvas.tsx new file mode 100644 index 0000000..a227725 --- /dev/null +++ b/frontend/src/components/canvas/Canvas.tsx @@ -0,0 +1,54 @@ +import useAppStore from '../../store/useAppStore' +import HatBlock from '../blocks/HatBlock' +import ScriptBlock from '../blocks/ScriptBlock' +import DropIndicator from './DropIndicator' +import EmptyCanvas from './EmptyCanvas' +import ConsolePanel from '../ui/ConsolePanel' +import DragGhost from '../ui/DragGhost' + +const canvasStyle: React.CSSProperties = { + position: 'absolute', inset: 0, overflow: 'auto', padding: 36, + background: 'radial-gradient(rgba(255,255,255,.05) 1.4px, transparent 1.4px) 0 0 / 22px 22px, #1b1613', +} + +type CanvasProps = { + canvasRef: React.RefObject + hatRef: React.RefObject + blockElsRef: React.MutableRefObject> + startBlockDrag: (id: string, e: React.PointerEvent) => void + bands: (number | null)[] + hatBand: number | null +} + +export default function Canvas({ canvasRef, hatRef, blockElsRef, startBlockDrag, bands, hatBand }: CanvasProps) { + const script = useAppStore(s => s.script) + const drag = useAppStore(s => s.drag) + const n = script.length + const dropEnd = drag?.active && drag?.overCanvas && drag?.moved && drag?.insertIndex >= n + + return ( +
+
+
+ + {script.map((block, i) => ( + + ))} + {dropEnd && } + {n === 0 && } +
+
+ + +
+ ) +} diff --git a/frontend/src/components/canvas/DropIndicator.tsx b/frontend/src/components/canvas/DropIndicator.tsx new file mode 100644 index 0000000..b8eefdb --- /dev/null +++ b/frontend/src/components/canvas/DropIndicator.tsx @@ -0,0 +1,9 @@ +export default function DropIndicator() { + return ( +
+ ) +} diff --git a/frontend/src/components/canvas/EmptyCanvas.tsx b/frontend/src/components/canvas/EmptyCanvas.tsx new file mode 100644 index 0000000..ca8ca73 --- /dev/null +++ b/frontend/src/components/canvas/EmptyCanvas.tsx @@ -0,0 +1,11 @@ +export default function EmptyCanvas() { + return ( +
+ Glisse des blocs depuis la palette pour bâtir ton modèle. +
+ ) +} diff --git a/frontend/src/components/editor/EditorHeader.tsx b/frontend/src/components/editor/EditorHeader.tsx new file mode 100644 index 0000000..737692f --- /dev/null +++ b/frontend/src/components/editor/EditorHeader.tsx @@ -0,0 +1,48 @@ +import useAppStore from '../../store/useAppStore' + +const ACCENT = '#D97757' + +const ghostBtn: React.CSSProperties = { background: 'rgba(255,255,255,.06)', color: '#e8e0d8', border: '1px solid rgba(255,255,255,.1)', padding: '8px 14px', borderRadius: 10, fontWeight: 700, fontSize: 13.5, cursor: 'pointer', display: 'inline-flex', alignItems: 'center', gap: 7 } +const actionBtn: React.CSSProperties = { ...ghostBtn, color: '#cfc6bd', padding: '9px 14px' } + +type EditorHeaderProps = { + onRun: () => void + onStop: () => void + onClear: () => void +} + +export default function EditorHeader({ onRun, onStop, onClear }: EditorHeaderProps) { + const goHome = useAppStore(s => s.goHome) + const projectName = useAppStore(s => 'mon-premier-modèle') + const running = useAppStore(s => s.running) + + return ( +
+
+
+
+
+
+ MLBlock +
+
+
+ + {projectName} +
+ + +
+
+ + + + +
+
+ ) +} diff --git a/frontend/src/components/editor/EditorLayout.tsx b/frontend/src/components/editor/EditorLayout.tsx new file mode 100644 index 0000000..9096fdd --- /dev/null +++ b/frontend/src/components/editor/EditorLayout.tsx @@ -0,0 +1,39 @@ +import { useRef, useEffect } from 'react' +import useAppStore from '../../store/useAppStore' +import { useDragDrop } from '../../hooks/useDragDrop' +import { useBlockWidths } from '../../hooks/useBlockWidths' +import CategoryBar from '../sidebar/CategoryBar' +import BlockPalette from '../sidebar/BlockPalette' +import Canvas from '../canvas/Canvas' + +export default function EditorLayout() { + const script = useAppStore(s => s.script) + const blockElsRef = useRef>({}) + const canvasRef = useRef(null) + const hatRef = useRef(null) + + const { startPaletteDrag, startBlockDrag } = useDragDrop({ blockElsRef, canvasRef }) + const { bands, hatBand } = useBlockWidths({ script, blockElsRef, hatRef }) + + useEffect(() => { + const ids = new Set(script.map(b => b.id)) + Object.keys(blockElsRef.current).forEach(id => { + if (!ids.has(id)) delete blockElsRef.current[id] + }) + }, [script]) + + return ( +
+ + + +
+ ) +} diff --git a/frontend/src/components/landing/FeaturesSection.tsx b/frontend/src/components/landing/FeaturesSection.tsx new file mode 100644 index 0000000..199b7d0 --- /dev/null +++ b/frontend/src/components/landing/FeaturesSection.tsx @@ -0,0 +1,56 @@ +import React from 'react' + +type Feature = { color: string; icon: React.ReactNode; title: string; desc: string } + +const FEATURES: Feature[] = [ + { + color: '#E59060', + icon:
, + title: 'Comme un jeu de construction', + desc: "Attrape un bloc, dépose-le dans ton projet. Il s'emboîte tout seul à la bonne place. Aucune ligne à taper.", + }, + { + color: '#B6A0E3', + icon: ( +
+
+
+
+
+ ), + title: 'Toutes les IA', + desc: 'Images, textes, tableaux de chiffres : reconnais, classe et prédis avec des modèles simples ou de vrais réseaux de neurones.', + }, + { + color: '#7DAFEA', + icon: , + title: 'Vois-le apprendre', + desc: "Appuie sur Lancer et regarde, tour après tour, ton modèle se tromper de moins en moins et devenir de plus en plus précis.", + }, +] + +export default function FeaturesSection() { + return ( +
+
+

+ L'intelligence artificielle, en pièces à assembler +

+

+ Chaque étape pour faire apprendre une machine devient un bloc coloré. +

+
+ {FEATURES.map(({ color, icon, title, desc }) => ( +
+
+ {icon} +
+

{title}

+

{desc}

+
+ ))} +
+
+
+ ) +} diff --git a/frontend/src/components/landing/HeroBlockStack.tsx b/frontend/src/components/landing/HeroBlockStack.tsx new file mode 100644 index 0000000..be0966b --- /dev/null +++ b/frontend/src/components/landing/HeroBlockStack.tsx @@ -0,0 +1,81 @@ +import React, { useLayoutEffect } from 'react' +import { buildClusters, snapW, blockBorderRadius } from '../../utils/snapLogic' + +type HeroBlock = { + key: number + bg: string + color: string + label: React.ReactNode + isHat?: boolean + isLast?: boolean +} + +const BLOCKS: HeroBlock[] = [ + { key: 0, bg: '#D97757', color: '#fff', label: <>▶ Démarrer le projet, isHat: true }, + { key: 1, bg: '#E59060', color: '#2a211c', label: <>Charger Photos }, + { key: 2, bg: '#66C7B0', color: '#2a211c', label: <>Mettre à la même échelle }, + { key: 3, bg: '#B6A0E3', color: '#2a211c', label: <>Réseau de 128 neurones }, + { key: 4, bg: '#7DAFEA', color: '#2a211c', label: <>Apprendre 10 tours, isLast: true }, +] + +export default function HeroBlockStack() { + useLayoutEffect(() => { + const timer = setTimeout(() => { + const els = [...document.querySelectorAll('[data-hero-block]')] + if (!els.length) return + const R = 12 + const widths = els.map(el => el.offsetWidth) + const clusters = buildClusters(widths) + const bands = widths.map(w => snapW(w, clusters) ?? w) + const n = els.length + els.forEach((el, i) => { + el.style.minWidth = bands[i] + 'px' + if (i === 0) { + const br = bands[0] === bands[1] ? '0px' : R + 'px' + el.style.borderRadius = `14px ${br} ${br} 0px` + } else { + el.style.borderRadius = blockBorderRadius(i - 1, n - 1, bands.slice(1), bands[0]) + } + }) + }, 120) + return () => clearTimeout(timer) + }, []) + + return ( +
+
+
+ + + +
+
+ {BLOCKS.map(({ key, bg, color, label, isHat, isLast }) => ( +
+ {!isHat &&
} + {label} +
+
+ ))} +
+
+
+ ) +} diff --git a/frontend/src/components/landing/HeroSection.tsx b/frontend/src/components/landing/HeroSection.tsx new file mode 100644 index 0000000..aa86f0a --- /dev/null +++ b/frontend/src/components/landing/HeroSection.tsx @@ -0,0 +1,38 @@ +import useAppStore from '../../store/useAppStore' +import HeroBlockStack from './HeroBlockStack' + +export default function HeroSection() { + const goBuild = useAppStore(s => s.goBuild) + + return ( +
+
+
+ + SANS CODE · POUR APPRENDRE L'IA +
+

+ Crée ton intelligence
artificielle, bloc par bloc. +

+

+ Empile des blocs colorés pour construire un modèle qui apprend tout seul : reconnaître des images, comprendre des phrases, deviner des réponses. Pas besoin de savoir programmer, il suffit d'assembler. +

+
+ + +
+
+ +
+ ) +} diff --git a/frontend/src/components/landing/HomeFooter.tsx b/frontend/src/components/landing/HomeFooter.tsx new file mode 100644 index 0000000..4c135de --- /dev/null +++ b/frontend/src/components/landing/HomeFooter.tsx @@ -0,0 +1,8 @@ +export default function HomeFooter() { + return ( +
+ MLBlock — le machine learning en blocs + Fait pour apprendre en s'amusant 🧱 +
+ ) +} diff --git a/frontend/src/components/landing/HomeNav.tsx b/frontend/src/components/landing/HomeNav.tsx new file mode 100644 index 0000000..0f84354 --- /dev/null +++ b/frontend/src/components/landing/HomeNav.tsx @@ -0,0 +1,27 @@ +import useAppStore from '../../store/useAppStore' + +export default function HomeNav() { + const goBuild = useAppStore(s => s.goBuild) + + return ( + + ) +} diff --git a/frontend/src/components/sidebar/BlockPalette.tsx b/frontend/src/components/sidebar/BlockPalette.tsx new file mode 100644 index 0000000..ef05c1d --- /dev/null +++ b/frontend/src/components/sidebar/BlockPalette.tsx @@ -0,0 +1,33 @@ +import useAppStore from '../../store/useAppStore' +import { colorFor } from '../../utils/blockHelpers' +import PaletteBlock from './PaletteBlock' + +type BlockPaletteProps = { + startPaletteDrag: (type: string, e: React.PointerEvent) => void +} + +export default function BlockPalette({ startPaletteDrag }: BlockPaletteProps) { + const selectedCat = useAppStore(s => s.category) + const catalog = useAppStore(s => s.catalog) + + if (!catalog) return null + + const cat = catalog.categories.find(c => c.id === selectedCat) + const types = Object.keys(catalog.blocks).filter(t => catalog.blocks[t].cat === selectedCat) + + return ( +
+
+ + + {cat?.name} + +
+
+ {types.map(type => ( + + ))} +
+
+ ) +} diff --git a/frontend/src/components/sidebar/CategoryBar.tsx b/frontend/src/components/sidebar/CategoryBar.tsx new file mode 100644 index 0000000..abe1be0 --- /dev/null +++ b/frontend/src/components/sidebar/CategoryBar.tsx @@ -0,0 +1,27 @@ +import useAppStore from '../../store/useAppStore' +import CategoryIcon from './CategoryIcon' + +export default function CategoryBar() { + const selected = useAppStore(s => s.category) + const setCategory = useAppStore(s => s.setCategory) + const catalog = useAppStore(s => s.catalog) + + if (!catalog) return null + + return ( +
+ {catalog.categories.map(cat => ( + setCategory(cat.id) }} + selected={selected === cat.id} + /> + ))} +
+ ) +} diff --git a/frontend/src/components/sidebar/CategoryIcon.tsx b/frontend/src/components/sidebar/CategoryIcon.tsx new file mode 100644 index 0000000..f21f41e --- /dev/null +++ b/frontend/src/components/sidebar/CategoryIcon.tsx @@ -0,0 +1,77 @@ +import React from 'react' +import type { Category } from '../../types/catalog' + +const ICONS: Record = { + data: ( + + + + + + ), + prep: ( + + + + + + + + + ), + model: ( + + + + + + + + + + ), + train: ( + + + + ), + eval: ( + + + + + ), + control: ( + + + + ), +} + +type CategoryIconProps = { + cat: Category & { onClick: () => void } + selected: boolean +} + +export default function CategoryIcon({ cat, selected }: CategoryIconProps) { + return ( +
+
+ {ICONS[cat.id]} +
+ {cat.name} +
+ ) +} diff --git a/frontend/src/components/sidebar/PaletteBlock.tsx b/frontend/src/components/sidebar/PaletteBlock.tsx new file mode 100644 index 0000000..9c67de2 --- /dev/null +++ b/frontend/src/components/sidebar/PaletteBlock.tsx @@ -0,0 +1,36 @@ +import useAppStore from '../../store/useAppStore' +import { colorFor } from '../../utils/blockHelpers' +import BlockSegments from '../blocks/BlockSegments' + +type PaletteBlockProps = { + type: string + startPaletteDrag: (type: string, e: React.PointerEvent) => void +} + +export default function PaletteBlock({ type, startPaletteDrag }: PaletteBlockProps) { + const catalog = useAppStore(s => s.catalog) + if (!catalog) return null + + const d = catalog.blocks[type] + if (!d) return null + + const color = colorFor(d.cat, catalog.categories) + + return ( +
startPaletteDrag(type, e)} + style={{ + position: 'relative', + display: 'flex', alignItems: 'center', flexWrap: 'wrap', gap: 6, + color: '#2a211c', fontWeight: 800, fontSize: 14, + background: color, padding: '14px 15px 13px', borderRadius: 12, + marginBottom: 18, cursor: 'grab', + boxShadow: '0 2px 0 rgba(0,0,0,.18)', userSelect: 'none', + }} + > +
+
+ +
+ ) +} diff --git a/frontend/src/components/ui/.gitkeep b/frontend/src/components/ui/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/frontend/src/components/ui/ConsolePanel.tsx b/frontend/src/components/ui/ConsolePanel.tsx new file mode 100644 index 0000000..1eabf28 --- /dev/null +++ b/frontend/src/components/ui/ConsolePanel.tsx @@ -0,0 +1,48 @@ +import { useRef, useEffect } from 'react' +import useAppStore from '../../store/useAppStore' + +const COLORS: Record = { sys: '#f0e9e3', info: '#9aa0c4', ok: '#8fd1a8', epoch: '#E8C77A' } + +export default function ConsolePanel() { + const consoleLines = useAppStore(s => s.consoleLines) + const running = useAppStore(s => s.running) + const result = useAppStore(s => s.result) + const scrollRef = useRef(null) + + useEffect(() => { + if (running && scrollRef.current) { + scrollRef.current.scrollTop = scrollRef.current.scrollHeight + } + }, [consoleLines, running]) + + if (consoleLines.length === 0) return null + + return ( +
+
+
+ + Ce qui se passe +
+ {result !== null && ( +
+ ✓ Terminé +
+ )} +
+
+ {consoleLines.map((line, i) => ( +
{line.t}
+ ))} +
+
+ ) +} diff --git a/frontend/src/components/ui/DragGhost.tsx b/frontend/src/components/ui/DragGhost.tsx new file mode 100644 index 0000000..8a04ed2 --- /dev/null +++ b/frontend/src/components/ui/DragGhost.tsx @@ -0,0 +1,28 @@ +import useAppStore from '../../store/useAppStore' + +export default function DragGhost() { + const drag = useAppStore(s => s.drag) + if (!drag || !drag.active || !drag.moved) return null + + return ( +
+ {drag.label} +
+ ) +} diff --git a/frontend/src/components/ui/EditorUnavailableModal.tsx b/frontend/src/components/ui/EditorUnavailableModal.tsx new file mode 100644 index 0000000..0d3dcab --- /dev/null +++ b/frontend/src/components/ui/EditorUnavailableModal.tsx @@ -0,0 +1,26 @@ +import useAppStore from '../../store/useAppStore' + +export default function EditorUnavailableModal() { + const goHome = useAppStore(s => s.goHome) + const message = useAppStore(s => s.catalogErrorMessage) + + return ( +
+
+
+
+ Éditeur non disponible +
+
+ {message ?? 'Impossible de joindre le serveur. Vérifie que le backend est lancé et réessaie.'} +
+ +
+
+ ) +} diff --git a/frontend/src/hooks/useBlockRunner.ts b/frontend/src/hooks/useBlockRunner.ts new file mode 100644 index 0000000..18d32fa --- /dev/null +++ b/frontend/src/hooks/useBlockRunner.ts @@ -0,0 +1,91 @@ +import { useCallback } from 'react' +import useAppStore from '../store/useAppStore' +import { validateGraph, createPipeline, updatePipeline, buildPipeline } from '../api/client' +import type { PipelineNode, PipelineEdge } from '../types/catalog' + +const DEFAULT_PIPELINE_NAME = 'mon-premier-modèle' + +function buildNodes(script: { id: string; type: string; fields: Record }[]): PipelineNode[] { + return script.map(b => ({ + id: b.id, + type: b.type, + params: b.fields as Record, + children: [], + })) +} + +export function useBlockRunner() { + const onRun = useCallback(async () => { + const store = useAppStore.getState() + if (store.running) return + if (store.script.length === 0) { + store.appendConsoleLines([{ k: 'sys', t: '⚠ Aucun bloc à exécuter.' }]) + return + } + + store.startRun() + + const nodes: PipelineNode[] = buildNodes(store.script) + const edges: PipelineEdge[] = [] + + try { + const validation = await validateGraph(nodes, edges) + if (!validation.valid) { + useAppStore.getState().appendConsoleLines([ + { k: 'sys', t: '⚠ Graphe invalide :' }, + ...validation.errors.map(e => ({ k: 'sys', t: ` • ${e}` })), + ]) + useAppStore.getState().stopRun() + return + } + + let pipelineId = useAppStore.getState().pipelineId + const payload = { name: DEFAULT_PIPELINE_NAME, description: '', nodes, edges } + + if (pipelineId === null) { + const created = await createPipeline(payload) + pipelineId = created.id + useAppStore.getState().setPipelineId(pipelineId) + } else { + await updatePipeline(pipelineId, payload) + } + + useAppStore.getState().appendConsoleLines([{ k: 'info', t: `📦 Pipeline #${pipelineId} sauvegardé` }]) + + const build = await buildPipeline(pipelineId) + + if (!useAppStore.getState().running) return + + if (build.success) { + const lines = [{ k: 'ok', t: `✓ Build réussi — ${build.layer_count} couche(s)` }] + if (build.output_shape) { + lines.push({ k: 'info', t: ` Forme de sortie : [${build.output_shape.join(', ')}]` }) + } + useAppStore.getState().appendConsoleLines(lines) + useAppStore.getState().finishRun(build) + } else { + useAppStore.getState().appendConsoleLines([ + { k: 'sys', t: `⚠ Erreur de build : ${build.error ?? 'inconnue'}` }, + ]) + useAppStore.getState().stopRun() + } + } catch (err) { + console.error('Pipeline run failed:', err) + if (useAppStore.getState().running) { + useAppStore.getState().stopRun() + useAppStore.getState().appendConsoleLines([{ k: 'sys', t: "⚠ Erreur lors de l'exécution." }]) + } + } + }, []) + + const onStop = useCallback(() => { + if (!useAppStore.getState().running) return + useAppStore.getState().stopRun() + }, []) + + const onClear = useCallback(() => { + useAppStore.getState().clearAll() + }, []) + + return { onRun, onStop, onClear } +} diff --git a/frontend/src/hooks/useBlockWidths.ts b/frontend/src/hooks/useBlockWidths.ts new file mode 100644 index 0000000..db525ac --- /dev/null +++ b/frontend/src/hooks/useBlockWidths.ts @@ -0,0 +1,48 @@ +import { useRef, useReducer, useLayoutEffect } from 'react' +import { buildClusters, snapW } from '../utils/snapLogic' +import type { Block } from '../utils/blockHelpers' + +function contentKey(b: Block): string { + const f = b.fields || {} + return b.type + '|' + Object.keys(f).sort().map(k => k + '=' + f[k]).join('&') +} + +type Params = { + script: Block[] + blockElsRef: React.MutableRefObject> + hatRef: React.RefObject +} + +type Result = { bands: (number | null)[]; hatBand: number | null } + +export function useBlockWidths({ script, blockElsRef, hatRef }: Params): Result { + const naturalW = useRef>({}) + const [, forceUpdate] = useReducer((x: number) => x + 1, 0) + + useLayoutEffect(() => { + let changed = false + if (hatRef.current && naturalW.current['__hat__'] == null) { + naturalW.current['__hat__'] = hatRef.current.offsetWidth + changed = true + } + script.forEach(b => { + const k = contentKey(b) + if (naturalW.current[k] == null) { + const el = blockElsRef.current[b.id] + if (el) { naturalW.current[k] = el.offsetWidth; changed = true } + } + }) + if (changed) forceUpdate() + }) + + const hatNat = naturalW.current['__hat__'] + const sKeys = script.map(contentKey) + const knownW: number[] = [] + if (hatNat != null) knownW.push(hatNat) + sKeys.forEach(k => { if (naturalW.current[k] != null) knownW.push(naturalW.current[k] as number) }) + const clusters = buildClusters(knownW) + const bands = sKeys.map(k => snapW(naturalW.current[k], clusters)) + const hatBand = snapW(hatNat, clusters) + + return { bands, hatBand } +} diff --git a/frontend/src/hooks/useDragDrop.ts b/frontend/src/hooks/useDragDrop.ts new file mode 100644 index 0000000..96b89a6 --- /dev/null +++ b/frontend/src/hooks/useDragDrop.ts @@ -0,0 +1,100 @@ +import { useEffect, useCallback } from 'react' +import useAppStore from '../store/useAppStore' +import { colorFor, titleOf } from '../utils/blockHelpers' + +type Params = { + blockElsRef: React.MutableRefObject> + canvasRef: React.RefObject +} + +export function useDragDrop({ blockElsRef, canvasRef }: Params) { + const computeInsertIndex = useCallback((clientY: number): number => { + const { script } = useAppStore.getState() + let insertIndex = script.length + for (let i = 0; i < script.length; i++) { + const el = blockElsRef.current[script[i].id] + if (!el) continue + const br = el.getBoundingClientRect() + if (clientY < br.top + br.height / 2) { insertIndex = i; break } + } + return insertIndex + }, [blockElsRef]) + + const startPaletteDrag = useCallback((type: string, e: React.PointerEvent): void => { + if (e.button != null && e.button !== 0) return + e.preventDefault() + const catalog = useAppStore.getState().catalog + const d = catalog?.blocks[type] + useAppStore.getState().setDrag({ + active: true, source: 'palette', type, + sx: e.clientX, sy: e.clientY, + x: e.clientX, y: e.clientY, + insertIndex: useAppStore.getState().script.length, + overCanvas: false, moved: false, + color: d && catalog ? colorFor(d.cat, catalog.categories) : '#888', + label: d && catalog ? titleOf(type, catalog.blocks) : type, + }) + }, []) + + const startBlockDrag = useCallback((id: string, e: React.PointerEvent): void => { + const tag = (e.target as HTMLElement).tagName + if (['INPUT', 'SELECT', 'OPTION', 'BUTTON', 'TEXTAREA'].includes(tag)) return + e.preventDefault() + const { script } = useAppStore.getState() + const b = script.find(x => x.id === id) + if (!b) return + const catalog = useAppStore.getState().catalog + const d = catalog?.blocks[b.type] + useAppStore.getState().setDrag({ + active: true, source: 'script', id, type: b.type, + sx: e.clientX, sy: e.clientY, + x: e.clientX, y: e.clientY, + insertIndex: script.length, + overCanvas: true, moved: false, + color: d && catalog ? colorFor(d.cat, catalog.categories) : '#888', + label: d && catalog ? titleOf(b.type, catalog.blocks) : b.type, + }) + }, []) + + useEffect(() => { + const onMove = (e: PointerEvent) => { + const d = useAppStore.getState().drag + if (!d || !d.active) return + const moved = d.moved || Math.abs(e.clientX - d.sx) > 4 || Math.abs(e.clientY - d.sy) > 4 + let overCanvas = false + if (canvasRef.current) { + const r = canvasRef.current.getBoundingClientRect() + overCanvas = e.clientX >= r.left && e.clientX <= r.right && e.clientY >= r.top && e.clientY <= r.bottom + } + const insertIndex = overCanvas ? computeInsertIndex(e.clientY) : useAppStore.getState().script.length + if (d.source === 'palette') { + useAppStore.getState().setDrag({ ...d, x: e.clientX, y: e.clientY, moved, overCanvas, insertIndex }) + } else { + useAppStore.getState().setDrag({ ...d, x: e.clientX, y: e.clientY, moved, overCanvas, insertIndex }) + } + } + + const onUp = () => { + const d = useAppStore.getState().drag + if (!d || !d.active) return + const { addBlock, moveBlock, deleteBlock } = useAppStore.getState() + if (d.source === 'palette') { + if (d.overCanvas) addBlock(d.type, d.insertIndex) + else if (!d.moved) addBlock(d.type, null) + } else if (d.source === 'script') { + if (d.overCanvas) moveBlock(d.id, d.insertIndex) + else if (d.moved) deleteBlock(d.id) + } + useAppStore.getState().clearDrag() + } + + document.addEventListener('pointermove', onMove) + document.addEventListener('pointerup', onUp) + return () => { + document.removeEventListener('pointermove', onMove) + document.removeEventListener('pointerup', onUp) + } + }, [canvasRef, computeInsertIndex]) + + return { startPaletteDrag, startBlockDrag } +} diff --git a/frontend/src/index.css b/frontend/src/index.css new file mode 100644 index 0000000..80fe86f --- /dev/null +++ b/frontend/src/index.css @@ -0,0 +1,35 @@ +* { box-sizing: border-box; } +html, body { margin: 0; padding: 0; } +body { + font-family: 'Nunito', system-ui, sans-serif; + background: #171311; + color: #f0e9e3; + -webkit-font-smoothing: antialiased; +} +input, select, button { font-family: inherit; } + +@keyframes mlbGlow { + 0%, 100% { box-shadow: 0 2px 0 rgba(0,0,0,.18), 0 0 0 0 rgba(255,255,255,0); } + 50% { box-shadow: 0 2px 0 rgba(0,0,0,.18), 0 0 0 4px rgba(255,255,255,.7); } +} +@keyframes mlbFloat { + 0%, 100% { transform: translateY(0); } + 50% { transform: translateY(-10px); } +} +@keyframes mlbBlink { + 0%, 100% { opacity: 1; } + 50% { opacity: .35; } +} + +::-webkit-scrollbar { width: 11px; height: 11px; } +::-webkit-scrollbar-thumb { + background: rgba(255,255,255,.12); + border-radius: 9px; + border: 3px solid transparent; + background-clip: padding-box; +} +::-webkit-scrollbar-thumb:hover { + background: rgba(255,255,255,.2); + background-clip: padding-box; + border: 3px solid transparent; +} diff --git a/frontend/src/main.tsx b/frontend/src/main.tsx new file mode 100644 index 0000000..4a1b150 --- /dev/null +++ b/frontend/src/main.tsx @@ -0,0 +1,10 @@ +import React from 'react' +import ReactDOM from 'react-dom/client' +import App from './App' +import './index.css' + +ReactDOM.createRoot(document.getElementById('root')!).render( + + + +) diff --git a/frontend/src/pages/.gitkeep b/frontend/src/pages/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/frontend/src/pages/EditorPage.tsx b/frontend/src/pages/EditorPage.tsx new file mode 100644 index 0000000..45ae903 --- /dev/null +++ b/frontend/src/pages/EditorPage.tsx @@ -0,0 +1,44 @@ +import { useEffect } from 'react' +import { useBlockRunner } from '../hooks/useBlockRunner' +import useAppStore from '../store/useAppStore' +import { fetchCatalog } from '../api/client' +import EditorHeader from '../components/editor/EditorHeader' +import EditorLayout from '../components/editor/EditorLayout' +import EditorUnavailableModal from '../components/ui/EditorUnavailableModal' + +export default function EditorPage() { + const { onRun, onStop, onClear } = useBlockRunner() + const catalog = useAppStore(s => s.catalog) + const catalogError = useAppStore(s => s.catalogError) + + useEffect(() => { + fetchCatalog() + .then(data => useAppStore.getState().setCatalog(data)) + .catch((err) => { + const isNetworkError = !err?.response + useAppStore.getState().setCatalogError( + true, + isNetworkError + ? 'Impossible de joindre le serveur. Vérifie que le backend est lancé et réessaie.' + : `Réponse inattendue du serveur (${String(err?.response?.status ?? '?')}). Vérifie la version du backend.` + ) + }) + }, []) + + if (catalogError) return + + if (!catalog) { + return ( +
+ Chargement… +
+ ) + } + + return ( +
+ + +
+ ) +} diff --git a/frontend/src/pages/HomePage.tsx b/frontend/src/pages/HomePage.tsx new file mode 100644 index 0000000..878da75 --- /dev/null +++ b/frontend/src/pages/HomePage.tsx @@ -0,0 +1,15 @@ +import HomeNav from '../components/landing/HomeNav' +import HeroSection from '../components/landing/HeroSection' +import FeaturesSection from '../components/landing/FeaturesSection' +import HomeFooter from '../components/landing/HomeFooter' + +export default function HomePage() { + return ( +
+ + + + +
+ ) +} diff --git a/frontend/src/services/.gitkeep b/frontend/src/services/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/frontend/src/store/.gitkeep b/frontend/src/store/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/frontend/src/store/useAppStore.ts b/frontend/src/store/useAppStore.ts new file mode 100644 index 0000000..67ded91 --- /dev/null +++ b/frontend/src/store/useAppStore.ts @@ -0,0 +1,145 @@ +import { create } from 'zustand' +import { instantiate } from '../utils/blockHelpers' +import type { Block } from '../utils/blockHelpers' +import type { InternalCatalog } from '../types/catalog' + +export type ConsoleLine = { k: string; t: string } + +type DragBase = { + active: boolean + type: string + sx: number; sy: number + x: number; y: number + insertIndex: number + overCanvas: boolean + moved: boolean + color: string + label: string +} + +export type DragState = + | (DragBase & { source: 'palette' }) + | (DragBase & { source: 'script'; id: string }) + +type AppState = { + screen: 'home' | 'build' + category: string + script: Block[] + running: boolean + runningId: string | null + consoleLines: ConsoleLine[] + result: unknown + drag: DragState | null + catalog: InternalCatalog | null + catalogError: boolean + catalogErrorMessage: string | null + pipelineId: number | null + + goBuild: () => void + goHome: () => void + setCategory: (id: string) => void + addBlock: (type: string, index: number | null) => void + deleteBlock: (id: string) => void + moveBlock: (id: string, index: number) => void + updateField: (id: string, k: string, v: string) => void + setDrag: (drag: DragState) => void + clearDrag: () => void + appendConsoleLines: (lines: ConsoleLine[]) => void + startRun: () => void + setRunningId: (id: string | null) => void + finishRun: (result: unknown) => void + stopRun: () => void + clearAll: () => void + setCatalog: (catalog: InternalCatalog) => void + setCatalogError: (error: boolean, message?: string) => void + setPipelineId: (id: number | null) => void +} + +const useAppStore = create((set) => ({ + screen: 'home', + category: 'data', + script: [], + running: false, + runningId: null, + consoleLines: [], + result: null, + drag: null, + catalog: null, + catalogError: false, + catalogErrorMessage: null, + pipelineId: null, + + goBuild: () => set({ screen: 'build' }), + goHome: () => set({ screen: 'home', catalog: null, catalogError: false, catalogErrorMessage: null, pipelineId: null }), + setCategory: (id) => set({ category: id }), + + addBlock: (type, index) => set((s) => { + if (!s.catalog) return {} + const b = instantiate(type, s.catalog.blocks) + const sc = s.script.slice() + if (index == null || index < 0 || index > sc.length) sc.push(b) + else sc.splice(index, 0, b) + return { script: sc } + }), + + deleteBlock: (id) => set((s) => ({ script: s.script.filter(b => b.id !== id) })), + + moveBlock: (id, index) => set((s) => { + const sc = s.script.slice() + const from = sc.findIndex(b => b.id === id) + if (from < 0) return {} + const [b] = sc.splice(from, 1) + let idx = from < index ? index - 1 : index + idx = Math.max(0, Math.min(idx, sc.length)) + sc.splice(idx, 0, b) + return { script: sc } + }), + + updateField: (id, k, v) => set((s) => ({ + script: s.script.map(b => b.id === id ? { ...b, fields: { ...b.fields, [k]: v } } : b) + })), + + setDrag: (drag) => set({ drag }), + clearDrag: () => set({ drag: null }), + + appendConsoleLines: (lines) => set((s) => ({ consoleLines: [...s.consoleLines, ...lines] })), + + startRun: () => set({ + running: true, + runningId: null, + consoleLines: [{ k: 'sys', t: `▶ C'est parti !` }], + result: null, + }), + + setRunningId: (id) => set({ runningId: id }), + + finishRun: (result) => set((s) => ({ + running: false, + runningId: null, + result, + consoleLines: [...s.consoleLines, { k: 'ok', t: '✓ Terminé' }], + })), + + stopRun: () => set((s) => ({ + running: false, + runningId: null, + consoleLines: [...s.consoleLines, { k: 'sys', t: '■ Arrêté' }], + })), + + clearAll: () => set({ script: [], consoleLines: [], result: null, running: false, runningId: null }), + + setCatalog: (catalog) => set((s) => { + const firstCat = catalog.categories[0]?.id ?? 'data' + const catExists = catalog.categories.some(c => c.id === s.category) + return { catalog, category: catExists ? s.category : firstCat } + }), + + setCatalogError: (error, message) => set({ + catalogError: error, + catalogErrorMessage: message ?? null, + }), + + setPipelineId: (id) => set({ pipelineId: id }), +})) + +export default useAppStore diff --git a/frontend/src/types/catalog.ts b/frontend/src/types/catalog.ts new file mode 100644 index 0000000..7329ee8 --- /dev/null +++ b/frontend/src/types/catalog.ts @@ -0,0 +1,101 @@ +// Internal rendering types — used by components unchanged +export type TextSeg = { t: 'text'; v: string } +export type NumSeg = { t: 'num'; k: string; def: string; w?: number } +export type SelSeg = { t: 'sel'; k: string; def: string; opts: string[] } +export type Segment = TextSeg | NumSeg | SelSeg + +export type BlockDef = { cat: string; segs: Segment[] } +export type BlockDefMap = Record + +export type Category = { id: string; name: string; color: string } + +// Internal catalog shape held by the store +export interface InternalCatalog { + categories: Category[] + blocks: BlockDefMap +} + +// API wire types — exact match to OpenAPI spec + +export interface BlockSummary { + type: string + label: string + category: string + inputs: number + outputs: number + can_build: boolean +} + +export interface BlockDetail { + type: string + label: string + category: string + params: Record + inputs: Record[] + outputs: Record[] + template: string + children_allowed: boolean + can_build: boolean + generates_class: string | null + class_base: string | null +} + +export interface PageResult { + items: T[] + total: number + page: number + size: number + pages: number +} + +export interface PipelineNode { + id: string + type: string + params: Record + children: PipelineNode[] +} + +export interface PipelineEdge { + source: string + source_port: string + target: string + target_port: string +} + +export interface PipelineCreate { + name: string + description: string + nodes: PipelineNode[] + edges: PipelineEdge[] +} + +export interface PipelineSummary { + id: number + name: string + description: string + created_at: string + updated_at: string + node_count: number +} + +export interface PipelineDetail extends PipelineSummary { + nodes: PipelineNode[] + edges: PipelineEdge[] +} + +export interface ValidationResponse { + valid: boolean + errors: string[] +} + +export interface BuildResponse { + success: boolean + output_shape: number[] | null + output_values: number[][] | null + layer_count: number + error: string | null +} + +export interface GenerateResponse { + code: string +} diff --git a/frontend/src/utils/blockHelpers.ts b/frontend/src/utils/blockHelpers.ts new file mode 100644 index 0000000..ff2e687 --- /dev/null +++ b/frontend/src/utils/blockHelpers.ts @@ -0,0 +1,23 @@ +import type { BlockDefMap, Category } from '../types/catalog' + +export type Block = { id: string; type: string; fields: Record } + +let uid = 0 + +export const colorFor = (cat: string, categories: Category[]): string => { + const c = categories.find(c => c.id === cat) + return c ? c.color : '#888' +} + +export const titleOf = (type: string, defs: BlockDefMap): string => { + const d = defs[type] + if (!d) return type + return d.segs.map(s => s.t === 'text' ? s.v : s.def).join(' ') +} + +export const instantiate = (type: string, defs: BlockDefMap): Block => { + const d = defs[type] + const fields: Record = {} + d.segs.forEach(s => { if ('k' in s) fields[s.k] = s.def }) + return { id: 'b' + (uid++), type, fields } +} diff --git a/frontend/src/utils/snapLogic.ts b/frontend/src/utils/snapLogic.ts new file mode 100644 index 0000000..b6c2889 --- /dev/null +++ b/frontend/src/utils/snapLogic.ts @@ -0,0 +1,38 @@ +const BAND_TOL = 30 +const R = 12 + +export type Cluster = { min: number; max: number } + +export const buildClusters = (widths: number[]): Cluster[] => { + const uniq = [...new Set(widths)].sort((a, b) => a - b) + const clusters: Cluster[] = [] + uniq.forEach(w => { + const last = clusters[clusters.length - 1] + if (last && w - last.min <= BAND_TOL) last.max = w + else clusters.push({ min: w, max: w }) + }) + return clusters +} + +export const snapW = (w: number | null | undefined, clusters: Cluster[]): number | null => { + if (w == null) return null + const c = clusters.find(cl => w >= cl.min && w <= cl.max) + return c ? c.max : w +} + +export const blockBorderRadius = (i: number, n: number, bands: (number | null)[], hatBand: number | null): string => { + const bAbove = i === 0 ? hatBand : bands[i - 1] + const bBelow = i === n - 1 ? null : bands[i + 1] + const sameAbove = bands[i] != null && bAbove != null && bands[i] === bAbove + const sameBelow = bands[i] != null && bBelow != null && bands[i] === bBelow + const isLast = i === n - 1 + const tr = sameAbove ? 0 : R + const br = isLast ? R : (sameBelow ? 0 : R) + const bl = isLast ? R : 0 + return `0px ${tr}px ${br}px ${bl}px` +} + +export const hatBorderRadius = (n: number, hatBand: number | null, band0: number | null): string => { + const hatBR = n === 0 ? 0 : (hatBand != null && band0 != null && hatBand === band0 ? 0 : R) + return `14px 14px ${hatBR}px 0px` +} diff --git a/frontend/src/vite-env.d.ts b/frontend/src/vite-env.d.ts new file mode 100644 index 0000000..8ab4f00 --- /dev/null +++ b/frontend/src/vite-env.d.ts @@ -0,0 +1,9 @@ +/// + +interface ImportMetaEnv { + readonly VITE_API_BASE_URL: string +} + +interface ImportMeta { + readonly env: ImportMetaEnv +} diff --git a/frontend/tsconfig.json b/frontend/tsconfig.json new file mode 100644 index 0000000..42e0521 --- /dev/null +++ b/frontend/tsconfig.json @@ -0,0 +1,17 @@ +{ + "compilerOptions": { + "target": "ES2020", + "useDefineForClassFields": true, + "lib": ["ES2020", "DOM", "DOM.Iterable"], + "module": "ESNext", + "skipLibCheck": true, + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "resolveJsonModule": true, + "isolatedModules": true, + "noEmit": true, + "jsx": "react-jsx", + "strict": true + }, + "include": ["src"] +} diff --git a/frontend/vite.config.ts b/frontend/vite.config.ts new file mode 100644 index 0000000..9ffcc67 --- /dev/null +++ b/frontend/vite.config.ts @@ -0,0 +1,6 @@ +import { defineConfig } from 'vite' +import react from '@vitejs/plugin-react' + +export default defineConfig({ + plugins: [react()], +})