diff --git a/.gitignore b/.gitignore
index 2a1ca9b..bbbcddf 100644
--- a/.gitignore
+++ b/.gitignore
@@ -23,3 +23,5 @@ logs/
# Claude Code 本地运行时状态,不是项目源码
.claude/
+h3-console/frontend/node_modules/
+h3-console/frontend/dist/
diff --git a/h3-console/frontend/index.html b/h3-console/frontend/index.html
new file mode 100644
index 0000000..4d3c038
--- /dev/null
+++ b/h3-console/frontend/index.html
@@ -0,0 +1,15 @@
+
+
+
+
+
+ H3 Console - AI Video Studio
+
+
+
+
+
+
+
diff --git a/h3-console/frontend/package.json b/h3-console/frontend/package.json
new file mode 100644
index 0000000..42af34e
--- /dev/null
+++ b/h3-console/frontend/package.json
@@ -0,0 +1,23 @@
+{
+ "name": "h3-console-ui",
+ "version": "1.0.0",
+ "private": true,
+ "type": "module",
+ "scripts": {
+ "build": "vite build"
+ },
+ "dependencies": {
+ "clsx": "^2.1.1",
+ "lucide-react": "^1.16.0",
+ "react": "^19.0.0",
+ "react-dom": "^19.0.0",
+ "tailwind-merge": "^3.0.2"
+ },
+ "devDependencies": {
+ "@tailwindcss/vite": "^4.0.9",
+ "@vitejs/plugin-react": "^4.3.4",
+ "tailwindcss": "^4.0.9",
+ "vite": "^6.2.0",
+ "vite-plugin-singlefile": "^2.1.0"
+ }
+}
diff --git a/h3-console/frontend/pnpm-lock.yaml b/h3-console/frontend/pnpm-lock.yaml
new file mode 100644
index 0000000..c54b1c6
--- /dev/null
+++ b/h3-console/frontend/pnpm-lock.yaml
@@ -0,0 +1,1498 @@
+lockfileVersion: '9.0'
+
+settings:
+ autoInstallPeers: true
+ excludeLinksFromLockfile: false
+
+importers:
+
+ .:
+ dependencies:
+ clsx:
+ specifier: ^2.1.1
+ version: 2.1.1
+ lucide-react:
+ specifier: ^1.16.0
+ version: 1.64.0(react@19.3.1)
+ react:
+ specifier: ^19.0.0
+ version: 19.3.1
+ react-dom:
+ specifier: ^19.0.0
+ version: 19.3.1(react@19.3.1)
+ tailwind-merge:
+ specifier: ^3.0.2
+ version: 3.7.0
+ devDependencies:
+ '@tailwindcss/vite':
+ specifier: ^4.0.9
+ version: 4.3.3(vite@6.4.5(jiti@2.7.0)(lightningcss@1.32.0))
+ '@vitejs/plugin-react':
+ specifier: ^4.3.4
+ version: 4.7.0(vite@6.4.5(jiti@2.7.0)(lightningcss@1.32.0))
+ tailwindcss:
+ specifier: ^4.0.9
+ version: 4.3.3
+ vite:
+ specifier: ^6.2.0
+ version: 6.4.5(jiti@2.7.0)(lightningcss@1.32.0)
+ vite-plugin-singlefile:
+ specifier: ^2.1.0
+ version: 2.3.4(rollup@4.66.0)(vite@6.4.5(jiti@2.7.0)(lightningcss@1.32.0))
+
+packages:
+
+ '@babel/code-frame@7.29.7':
+ resolution: {integrity: sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw==}
+ engines: {node: '>=6.9.0'}
+
+ '@babel/compat-data@7.29.7':
+ resolution: {integrity: sha512-locTkQyKvwIEgBzVrn8693ebc97F2U8ZHjbXwDXJ5Fn2TCpNwTlKcaKLkdHop5c/icOFE7qt7Q9JC5hnKNa6Gg==}
+ engines: {node: '>=6.9.0'}
+
+ '@babel/core@7.29.7':
+ resolution: {integrity: sha512-RgHBCvtjbOK2gXSNBNIkNoEc9qoVEtau3hj8gEqKQuL3HZAibKarWFEI3Lfm6EYKkLalOh8eSrj9b+ch9H/VBA==}
+ engines: {node: '>=6.9.0'}
+
+ '@babel/generator@7.29.8':
+ resolution: {integrity: sha512-gZbepsdh3WDtgZKWL+vTPh71LSBrm/Y4/QDZBVCcYfmeTEEuoOYwlSy+G1StfJg+/Zy550u/3TATbm7qDbbMtg==}
+ engines: {node: '>=6.9.0'}
+
+ '@babel/helper-compilation-targets@7.29.7':
+ resolution: {integrity: sha512-wem6WaBj4NaVYVdNhLPPVacES6ZJ+KBBfSkTMD3YZxbP3rm3Di85tJU5ljaUNhaOynt+Aj0xruhYuzQBt8n71g==}
+ engines: {node: '>=6.9.0'}
+
+ '@babel/helper-globals@7.29.7':
+ resolution: {integrity: sha512-3nQVUAtvkKH9zahfWgw96Jc/uFOmjACE1kQz82E2lqWmHBgjzbNlsC22nuQTfahmWeQtTq5nQ/4Nnd2A1wj4zA==}
+ engines: {node: '>=6.9.0'}
+
+ '@babel/helper-module-imports@7.29.7':
+ resolution: {integrity: sha512-ejHwrQQYcm9xnTivShn2IDOlIzInN34AXskvq9QicvCtEzq1Vzclu/tKF8Jq1Cg8JG2GL6/EmjgsCT7lXepE3g==}
+ engines: {node: '>=6.9.0'}
+
+ '@babel/helper-module-transforms@7.29.7':
+ resolution: {integrity: sha512-UPUVSyXbOh627KiCIGQSgwWzGeBKLkaJ9PJEdrngIwMSzxLR4jS4+f1f1jb7VzBbg8nFLaYotvVPFCTqdrmTAg==}
+ engines: {node: '>=6.9.0'}
+ peerDependencies:
+ '@babel/core': ^7.0.0
+
+ '@babel/helper-plugin-utils@7.29.7':
+ resolution: {integrity: sha512-G7sHYigPY17oO5SYWnfD/0MTBwVR781S/JI643e/JhUYgVgWE/61SoW3NH9KWUKyKq5LVh3npif99Wkt6j86Jw==}
+ engines: {node: '>=6.9.0'}
+
+ '@babel/helper-string-parser@7.29.11':
+ resolution: {integrity: sha512-snZbO7MEDOof+hvNa97EjEY7qD7h2kwvQr5AJC3ZhQudKW5dnej6k3kP9NQV88hJwaS8vDqRLFo9rivLEhle2g==}
+ engines: {node: '>=6.9.0'}
+
+ '@babel/helper-validator-identifier@7.29.7':
+ resolution: {integrity: sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==}
+ engines: {node: '>=6.9.0'}
+
+ '@babel/helper-validator-option@7.29.7':
+ resolution: {integrity: sha512-N9ZErrD+yW5geCDtBqnOoxmR8+tNKiGuxKlDpuJxfsqpa2dFcexaziGAE/qoHLiDDreVNMupxGmSoNlyvsA3gw==}
+ engines: {node: '>=6.9.0'}
+
+ '@babel/helpers@7.29.10':
+ resolution: {integrity: sha512-EMCx3AGZ3WfAl6ht7AyQoPm2Lh+BGYEMdc4Q6Hf7uSCDOZFuLq44qNwL+iaim8VlyYhRAo4X43+weadK8ZB4Yg==}
+ engines: {node: '>=6.9.0'}
+
+ '@babel/parser@7.29.11':
+ resolution: {integrity: sha512-OZ6+oaK7GMyg3IfCpj8JOmn8n+w2rJbDT0LLfVDTLb1ZFeKDh3AiNELgCMUw175IJHSkLl74hG0M0VLsvx+gag==}
+ engines: {node: '>=6.0.0'}
+ hasBin: true
+
+ '@babel/plugin-transform-react-jsx-self@7.29.7':
+ resolution: {integrity: sha512-TL0hMc9xzy86VD31nUiwzd5otRAcyEPcsegCxolO0PvcXuH1v0kECe/UIznYFihpkvU5wg/jk4v0TTEFfm53fw==}
+ engines: {node: '>=6.9.0'}
+ peerDependencies:
+ '@babel/core': ^7.0.0-0
+
+ '@babel/plugin-transform-react-jsx-source@7.29.7':
+ resolution: {integrity: sha512-06IyK09H3wi4cGbhDBwp5gUGo0IKtnYa8tyTiephirPCK6fbobVGiXMMI5zLQ4aKEYP3wZ3ArU44o+8KMrSG/Q==}
+ engines: {node: '>=6.9.0'}
+ peerDependencies:
+ '@babel/core': ^7.0.0-0
+
+ '@babel/template@7.29.7':
+ resolution: {integrity: sha512-puq+Gf35oI24FeN11LkoUQFqv9uwNeWpxXZi/Ji3rRIoKAzKnxRaZ+Gkj0vKS9ZCiTESfng1N9LyOyXvo+m+Gg==}
+ engines: {node: '>=6.9.0'}
+
+ '@babel/traverse@7.29.10':
+ resolution: {integrity: sha512-B3lYAs2nkKskbgQ1nfy8Ma9ao9Q1rntTAQHrzzyDuj+TSO+X3le6PfWwqDvodsb+OumE10eMFfuMCTQSQ5iuIQ==}
+ engines: {node: '>=6.9.0'}
+
+ '@babel/types@7.29.11':
+ resolution: {integrity: sha512-WCsemW40Cpc6gouC1SrY6PBaZ+oLRlEVY9XBvZBI7il/HyUrb2nJ505wE1MHEFyDZNnrWDEuy4jL50WVHfTTSQ==}
+ engines: {node: '>=6.9.0'}
+
+ '@esbuild/aix-ppc64@0.25.12':
+ resolution: {integrity: sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA==}
+ engines: {node: '>=18'}
+ cpu: [ppc64]
+ os: [aix]
+
+ '@esbuild/android-arm64@0.25.12':
+ resolution: {integrity: sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg==}
+ engines: {node: '>=18'}
+ cpu: [arm64]
+ os: [android]
+
+ '@esbuild/android-arm@0.25.12':
+ resolution: {integrity: sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg==}
+ engines: {node: '>=18'}
+ cpu: [arm]
+ os: [android]
+
+ '@esbuild/android-x64@0.25.12':
+ resolution: {integrity: sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg==}
+ engines: {node: '>=18'}
+ cpu: [x64]
+ os: [android]
+
+ '@esbuild/darwin-arm64@0.25.12':
+ resolution: {integrity: sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg==}
+ engines: {node: '>=18'}
+ cpu: [arm64]
+ os: [darwin]
+
+ '@esbuild/darwin-x64@0.25.12':
+ resolution: {integrity: sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA==}
+ engines: {node: '>=18'}
+ cpu: [x64]
+ os: [darwin]
+
+ '@esbuild/freebsd-arm64@0.25.12':
+ resolution: {integrity: sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg==}
+ engines: {node: '>=18'}
+ cpu: [arm64]
+ os: [freebsd]
+
+ '@esbuild/freebsd-x64@0.25.12':
+ resolution: {integrity: sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ==}
+ engines: {node: '>=18'}
+ cpu: [x64]
+ os: [freebsd]
+
+ '@esbuild/linux-arm64@0.25.12':
+ resolution: {integrity: sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ==}
+ engines: {node: '>=18'}
+ cpu: [arm64]
+ os: [linux]
+
+ '@esbuild/linux-arm@0.25.12':
+ resolution: {integrity: sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw==}
+ engines: {node: '>=18'}
+ cpu: [arm]
+ os: [linux]
+
+ '@esbuild/linux-ia32@0.25.12':
+ resolution: {integrity: sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA==}
+ engines: {node: '>=18'}
+ cpu: [ia32]
+ os: [linux]
+
+ '@esbuild/linux-loong64@0.25.12':
+ resolution: {integrity: sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng==}
+ engines: {node: '>=18'}
+ cpu: [loong64]
+ os: [linux]
+
+ '@esbuild/linux-mips64el@0.25.12':
+ resolution: {integrity: sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw==}
+ engines: {node: '>=18'}
+ cpu: [mips64el]
+ os: [linux]
+
+ '@esbuild/linux-ppc64@0.25.12':
+ resolution: {integrity: sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA==}
+ engines: {node: '>=18'}
+ cpu: [ppc64]
+ os: [linux]
+
+ '@esbuild/linux-riscv64@0.25.12':
+ resolution: {integrity: sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w==}
+ engines: {node: '>=18'}
+ cpu: [riscv64]
+ os: [linux]
+
+ '@esbuild/linux-s390x@0.25.12':
+ resolution: {integrity: sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg==}
+ engines: {node: '>=18'}
+ cpu: [s390x]
+ os: [linux]
+
+ '@esbuild/linux-x64@0.25.12':
+ resolution: {integrity: sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw==}
+ engines: {node: '>=18'}
+ cpu: [x64]
+ os: [linux]
+
+ '@esbuild/netbsd-arm64@0.25.12':
+ resolution: {integrity: sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg==}
+ engines: {node: '>=18'}
+ cpu: [arm64]
+ os: [netbsd]
+
+ '@esbuild/netbsd-x64@0.25.12':
+ resolution: {integrity: sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ==}
+ engines: {node: '>=18'}
+ cpu: [x64]
+ os: [netbsd]
+
+ '@esbuild/openbsd-arm64@0.25.12':
+ resolution: {integrity: sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A==}
+ engines: {node: '>=18'}
+ cpu: [arm64]
+ os: [openbsd]
+
+ '@esbuild/openbsd-x64@0.25.12':
+ resolution: {integrity: sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw==}
+ engines: {node: '>=18'}
+ cpu: [x64]
+ os: [openbsd]
+
+ '@esbuild/openharmony-arm64@0.25.12':
+ resolution: {integrity: sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg==}
+ engines: {node: '>=18'}
+ cpu: [arm64]
+ os: [openharmony]
+
+ '@esbuild/sunos-x64@0.25.12':
+ resolution: {integrity: sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w==}
+ engines: {node: '>=18'}
+ cpu: [x64]
+ os: [sunos]
+
+ '@esbuild/win32-arm64@0.25.12':
+ resolution: {integrity: sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg==}
+ engines: {node: '>=18'}
+ cpu: [arm64]
+ os: [win32]
+
+ '@esbuild/win32-ia32@0.25.12':
+ resolution: {integrity: sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ==}
+ engines: {node: '>=18'}
+ cpu: [ia32]
+ os: [win32]
+
+ '@esbuild/win32-x64@0.25.12':
+ resolution: {integrity: sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA==}
+ engines: {node: '>=18'}
+ cpu: [x64]
+ os: [win32]
+
+ '@jridgewell/gen-mapping@0.3.13':
+ resolution: {integrity: sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==}
+
+ '@jridgewell/remapping@2.3.5':
+ resolution: {integrity: sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==}
+
+ '@jridgewell/resolve-uri@3.1.2':
+ resolution: {integrity: sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==}
+ engines: {node: '>=6.0.0'}
+
+ '@jridgewell/sourcemap-codec@1.6.0':
+ resolution: {integrity: sha512-T7jf+5zgsZHwNJ4lvQ7/aezbyk0nNX+zJVWpmHA7VYsEx7a7qr5Rg5IbtJFqkgze5Y2sruq1RUY8Q837Od7iFw==}
+
+ '@jridgewell/trace-mapping@0.3.31':
+ resolution: {integrity: sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==}
+
+ '@napi-rs/lzma-linux-x64-gnu@1.5.1':
+ resolution: {integrity: sha512-oTXEIha4SsuXdTA4Iyskj0kpdx2yVXdhd75c2v3xGrHFfVMsbhTPZU/nMPL4sWKo4pBHm3aucLaqGlF696dTyQ==}
+ engines: {node: ^22.20 || ^24.12 || >=25}
+ cpu: [x64]
+ os: [linux]
+ libc: [glibc]
+
+ '@rolldown/pluginutils@1.0.0-beta.27':
+ resolution: {integrity: sha512-+d0F4MKMCbeVUJwG96uQ4SgAznZNSq93I3V+9NHA4OpvqG8mRCpGdKmK8l/dl02h2CCDHwW2FqilnTyDcAnqjA==}
+
+ '@rollup/rollup-android-arm-eabi@4.66.0':
+ resolution: {integrity: sha512-7y4XeoUHPecjfUDRmap8U0q6bjfLAPn7uOGT8BKyqy8uNDOjsGS8rlBhOx6sZLVwwjN6hapvGl2mW9/IS6UNww==}
+ cpu: [arm]
+ os: [android]
+
+ '@rollup/rollup-android-arm64@4.66.0':
+ resolution: {integrity: sha512-e8WugYZXpgqrlMXLCudYVRAxAcK7AIY36JxRDwFRD38L9q62G1VsEUW6ijXxzjxYjNB8i1zo3B/ycIQ/qjLC/Q==}
+ cpu: [arm64]
+ os: [android]
+
+ '@rollup/rollup-darwin-arm64@4.66.0':
+ resolution: {integrity: sha512-6vkDCfyDjNLokh+Rt1D2KtNgFFMEPdkWha/mbtP4cSjh9xiN8T/JBS/A7nDYKjmMAXHrvR9J3AzUk2LtFbIBLg==}
+ cpu: [arm64]
+ os: [darwin]
+
+ '@rollup/rollup-darwin-x64@4.66.0':
+ resolution: {integrity: sha512-/mzZAX0+Q6eXxvkR0HB0RZCCrsbmFD8DpsvCQoMYABDfq/DjqkbfytuJ7GvZ4AhR/Mgis2fXbZrGqfbcK2Z5fA==}
+ cpu: [x64]
+ os: [darwin]
+
+ '@rollup/rollup-freebsd-arm64@4.66.0':
+ resolution: {integrity: sha512-JQJx9E8tUjGqed1RevDmoHaS3V30ODdV8uyvGxlwd4K5se6bkPrMeltSybqAtpJ+ltDRfo7FORQaJG2Hd5nGog==}
+ cpu: [arm64]
+ os: [freebsd]
+
+ '@rollup/rollup-freebsd-x64@4.66.0':
+ resolution: {integrity: sha512-GEaUQQjMaG7X1304qMgqLjzCKv6CdDirzJSS+UqUs3xhJ9X2Kjzu0JDbLoUU42FWxwmZqz+SsgU1SlQv9GNRVA==}
+ cpu: [x64]
+ os: [freebsd]
+
+ '@rollup/rollup-linux-arm-gnueabihf@4.66.0':
+ resolution: {integrity: sha512-DgmAU9N0NhDQDzHgFUocDio7zRO8jR/nY/obDGvCR8zbJ6J77Fg6X49yPuExJUpYPbC82EOaRhG969pWzUSnHQ==}
+ cpu: [arm]
+ os: [linux]
+ libc: [glibc]
+
+ '@rollup/rollup-linux-arm-musleabihf@4.66.0':
+ resolution: {integrity: sha512-zG09YZ+sk/HoHq+P9gYB0EseQ3M20RbDuKGWey6fbiMDV1LxfXvhgSW9jDaCcvFPMgfLXpimzXY+UYvA8R2mLQ==}
+ cpu: [arm]
+ os: [linux]
+ libc: [musl]
+
+ '@rollup/rollup-linux-arm64-gnu@4.66.0':
+ resolution: {integrity: sha512-vlArhgyXbcVc1D4WZPoiYuqJIaMiSHBj6etRXua0KlfjlZeuiQyVAPYi0/N49O4mSp3u1iGUBLEjNa8ec81XZg==}
+ cpu: [arm64]
+ os: [linux]
+ libc: [glibc]
+
+ '@rollup/rollup-linux-arm64-musl@4.66.0':
+ resolution: {integrity: sha512-RxJZtk0G87YPotDaRDvRqStV8cbqLSC5TroHvQHHKsqBg0ok64RxKgtXeFsa7FBcy7mb4crBrG3xK8USWIqWNA==}
+ cpu: [arm64]
+ os: [linux]
+ libc: [musl]
+
+ '@rollup/rollup-linux-loong64-gnu@4.66.0':
+ resolution: {integrity: sha512-qlGcw959PrLLkZhiBxF3OZ6SQvj+OKY85mCqGNgSRs4LZlDJ6ICqmz6UdxtINdhcXYwNIbZoapQVBAmZlD2hUQ==}
+ cpu: [loong64]
+ os: [linux]
+ libc: [glibc]
+
+ '@rollup/rollup-linux-loong64-musl@4.66.0':
+ resolution: {integrity: sha512-TvTI/7Cy0WvRA1SXgwzu6bwXBnMpmeT/ln5q7WvSpqmXy1VbmEnY8HVIDQNBiaHJvoWqRr0M4FNo7qiqtyMaFw==}
+ cpu: [loong64]
+ os: [linux]
+ libc: [musl]
+
+ '@rollup/rollup-linux-ppc64-gnu@4.66.0':
+ resolution: {integrity: sha512-UZEDkSoDRZ50NyLj70nO6mpIK3dvs2cPTTvQb1bA/PBDmr9fvn5KiESuGfh6srd7Zi6Lt9Sv2sVrFr1QoHLvPQ==}
+ cpu: [ppc64]
+ os: [linux]
+ libc: [glibc]
+
+ '@rollup/rollup-linux-ppc64-musl@4.66.0':
+ resolution: {integrity: sha512-VRbxTvagLh3HRcysx1CNq+HHcvJbSebNI6fCbk7jwmmWHkXjrSIV3igXBpRHUTFdg0pagWGu50xoSZXnrZHjVQ==}
+ cpu: [ppc64]
+ os: [linux]
+ libc: [musl]
+
+ '@rollup/rollup-linux-riscv64-gnu@4.66.0':
+ resolution: {integrity: sha512-uWdJpvYqh0LE/i+YBL3sMX0a2kUkICENjmrPmyBmMOFc3jMrs6MSQH/pMD+VIpGEoUcr3XkHGLLfymzIqPSRRg==}
+ cpu: [riscv64]
+ os: [linux]
+ libc: [glibc]
+
+ '@rollup/rollup-linux-riscv64-musl@4.66.0':
+ resolution: {integrity: sha512-3FUd88pxgWAjK60OgCrika1AcNG+u8zaCdWjg4TlcZTDkt5TGyci8ZU64l3OJ9ax0MEjw3P80B/3PoSQDCLwJQ==}
+ cpu: [riscv64]
+ os: [linux]
+ libc: [musl]
+
+ '@rollup/rollup-linux-s390x-gnu@4.66.0':
+ resolution: {integrity: sha512-qT2rMmgyej62g5AdwktK+Bt2lBu497pzWZe12TTQ5JmZjOYPnEwcPEAOSLTBxBV+6gIES75rXm219v/QkT397A==}
+ cpu: [s390x]
+ os: [linux]
+ libc: [glibc]
+
+ '@rollup/rollup-linux-x64-gnu@4.66.0':
+ resolution: {integrity: sha512-E46rnZgp7J84Qwe8RMlF+rgimnDo/9fuiGbUArGB0ZkMWf97O7yeSCfKls5x/YJOF0RcuSe9RctuKe1hlxICiA==}
+ cpu: [x64]
+ os: [linux]
+ libc: [glibc]
+
+ '@rollup/rollup-linux-x64-musl@4.66.0':
+ resolution: {integrity: sha512-Uz1HiZe+qBzqgdPsON+z0wRx3WdATuyzwvGT6cr2VZVrqqko0ZJVfkrQJtwDMuDcAfXYdo7XucaxJ7lTLjEcgg==}
+ cpu: [x64]
+ os: [linux]
+ libc: [musl]
+
+ '@rollup/rollup-openbsd-x64@4.66.0':
+ resolution: {integrity: sha512-Ph7TX3Ex5qdfcb7Tc6eEPugVkRfJ501eTovO4T5029YObfpZ7dzn3RiwpFvGp50nk2QUhvBMZ0PFGlEj6pOYTg==}
+ cpu: [x64]
+ os: [openbsd]
+
+ '@rollup/rollup-openharmony-arm64@4.66.0':
+ resolution: {integrity: sha512-nZYPEya7ZkeNxUEiF7qmNZbyLTyRN3MK5cO22KqsaoneUzXJuzBIQRw+7BEQy9LGj2O5vrCE9V3EpK06FYo1tQ==}
+ cpu: [arm64]
+ os: [openharmony]
+
+ '@rollup/rollup-win32-arm64-msvc@4.66.0':
+ resolution: {integrity: sha512-u7aTfS3R6C4hRsqXSzaO6qLZYLVuMkHMvcWlux4IM8IJif1VCl2cgPBSo5gSNoFoStQypQP+qwq8W1IyFBf1QQ==}
+ cpu: [arm64]
+ os: [win32]
+
+ '@rollup/rollup-win32-ia32-msvc@4.66.0':
+ resolution: {integrity: sha512-e+i9ueHrH1Bafb0cf0woAIVJbYcoC8vRaD+vz+Xi8w6jz9cYJSV3vyKO6ELGSZ6g4W4MJOm2WPLN4/nQf77h7w==}
+ cpu: [ia32]
+ os: [win32]
+
+ '@rollup/rollup-win32-x64-gnu@4.66.0':
+ resolution: {integrity: sha512-KfTCbrtr2zXTqH2E1F+uYlFTrYibxHKqPAHobpd8Kfg49cEHmuHebOidCTDRgqG1suaPFX7oejFQeGE71zWKfA==}
+ cpu: [x64]
+ os: [win32]
+
+ '@rollup/rollup-win32-x64-msvc@4.66.0':
+ resolution: {integrity: sha512-xA+neBh1bqkqaS3b48T2UHZnDsTIpb9B0h+TdBzynjF/ymCpYndhfY10RqW0qSKB1HqqQA83mINVhAdwA+mETg==}
+ cpu: [x64]
+ os: [win32]
+
+ '@tailwindcss/node@4.3.3':
+ resolution: {integrity: sha512-/T8IKEsf9VTU6tLjgC7+sv2mOPtQxzE2jMw7u4Tt40Tx+QSZxpzh95/H6cMKoja9XuW7iMdLJYBB0o9G1CaAgg==}
+
+ '@tailwindcss/oxide-android-arm64@4.3.3':
+ resolution: {integrity: sha512-Y85A2gmPSkl5Ve5qR86GL4HT509cFqQh1aes9p3sSkyTPwt0Pppf3GkwGe4JPACcRYjgJIEhQgM6dBClnr0NYw==}
+ engines: {node: '>= 20'}
+ cpu: [arm64]
+ os: [android]
+
+ '@tailwindcss/oxide-darwin-arm64@4.3.3':
+ resolution: {integrity: sha512-BiaWatpBcERQFDlOjRDpIVXuFK5PJez5SA4JMg6VYZdBYU+qKfV/vqjcIs+IYmtitf1xYQZTwXvU/8y4lfZUGw==}
+ engines: {node: '>= 20'}
+ cpu: [arm64]
+ os: [darwin]
+
+ '@tailwindcss/oxide-darwin-x64@4.3.3':
+ resolution: {integrity: sha512-fAeUqfV5ndhxRwai8cXGzdLvul9utWOmeTkv69unv4ZXixjn61Z+p9lCWdwOwA3TYboG3BwdVuN/RDjhBRl0mw==}
+ engines: {node: '>= 20'}
+ cpu: [x64]
+ os: [darwin]
+
+ '@tailwindcss/oxide-freebsd-x64@4.3.3':
+ resolution: {integrity: sha512-iyf5bV6+wnAlflVeEy7R25dupxTNECZN5QMI0qNT6eT+EgaGdZcKhGkr5SdoaWiLJ3spLqIY9VCeSGrwmtg4kw==}
+ engines: {node: '>= 20'}
+ cpu: [x64]
+ os: [freebsd]
+
+ '@tailwindcss/oxide-linux-arm-gnueabihf@4.3.3':
+ resolution: {integrity: sha512-aAYUprJAJQWWbRrPvtjdroZ56Md+JM8pMiopS6xGEwDfLhqj+2ver2p4nU4Mb3CRqcMmNBjo8KkUgcxhkzVQGQ==}
+ engines: {node: '>= 20'}
+ cpu: [arm]
+ os: [linux]
+
+ '@tailwindcss/oxide-linux-arm64-gnu@4.3.3':
+ resolution: {integrity: sha512-nDxldcEENOxZRzC2uu9jrutZdAAQtb+8WWDCSnWL1zvBk1+FN+x6MtDViPB5AJMfttVCUhehGWus3XBPgatM/w==}
+ engines: {node: '>= 20'}
+ cpu: [arm64]
+ os: [linux]
+ libc: [glibc]
+
+ '@tailwindcss/oxide-linux-arm64-musl@4.3.3':
+ resolution: {integrity: sha512-Md44bD6veX/PC5iyF8cDVnw4HBIANZepRZZ7a8DQOvkfo5WUBwcp6iAuCUz23u+4SUkhJlD3eL7hNdW8ezd/kA==}
+ engines: {node: '>= 20'}
+ cpu: [arm64]
+ os: [linux]
+ libc: [musl]
+
+ '@tailwindcss/oxide-linux-x64-gnu@4.3.3':
+ resolution: {integrity: sha512-tx7us1muwOKAKWao2v/GaafFeQboE6aj88vC6ziN2NCGcRm8gWUhwjzg+YdVB1e4boAtdtma4L43onunI6NS4w==}
+ engines: {node: '>= 20'}
+ cpu: [x64]
+ os: [linux]
+ libc: [glibc]
+
+ '@tailwindcss/oxide-linux-x64-musl@4.3.3':
+ resolution: {integrity: sha512-SJxX60smvHgasZoBy11dX6YRjXJFovwWBoedhbQPOBzgFWBHGB+TVPWB9BxzR7TTxU8FQZAI2AyiNCMzFm8Img==}
+ engines: {node: '>= 20'}
+ cpu: [x64]
+ os: [linux]
+ libc: [musl]
+
+ '@tailwindcss/oxide-wasm32-wasi@4.3.3':
+ resolution: {integrity: sha512-jx1+rPhY/5Ympkktd656HBWEBLxP7dH06losBLjjf5vgCODXvi9KhtftWcMIwTFIDqBr7cRnQkdLnAG+IOlGvQ==}
+ engines: {node: '>=14.0.0'}
+ cpu: [wasm32]
+ bundledDependencies:
+ - '@napi-rs/wasm-runtime'
+ - '@emnapi/core'
+ - '@emnapi/runtime'
+ - '@tybys/wasm-util'
+ - '@emnapi/wasi-threads'
+ - tslib
+
+ '@tailwindcss/oxide-win32-arm64-msvc@4.3.3':
+ resolution: {integrity: sha512-3rc292Ca2ceK6Ulcc/bAVnTs/3nDtoPhyEKlgPv+yQJQi/JS/AMJlqzxvlDacL1nekbrcf6bTqp/jV4qgnPxNQ==}
+ engines: {node: '>= 20'}
+ cpu: [arm64]
+ os: [win32]
+
+ '@tailwindcss/oxide-win32-x64-msvc@4.3.3':
+ resolution: {integrity: sha512-yJ0pwIVc/nYeGoV02WtsN8KYyLQv7kyI2wDnkezyJlGGjkd4QLwDGAwl47YpPJeuI0M0ObaXGSPjvWDPeTPggw==}
+ engines: {node: '>= 20'}
+ cpu: [x64]
+ os: [win32]
+
+ '@tailwindcss/oxide@4.3.3':
+ resolution: {integrity: sha512-krXjAikiaFSPaK/FkAQT5UTx3VormQaiZ5hBFlJZ9UFQGB/rwg1MZIhHAG9smMQRTdyJxP6Qt5MwMtdyU5FWrA==}
+ engines: {node: '>= 20'}
+
+ '@tailwindcss/vite@4.3.3':
+ resolution: {integrity: sha512-yYU8cogLeSh/ms2jh8Fj7jaba/EWa7Ja6GoUqYZaraEuCI5YS6ms6ObZgjjedm+jm6XZjdNRWBpPP6Z86oOxcw==}
+ peerDependencies:
+ vite: ^5.2.0 || ^6 || ^7 || ^8
+
+ '@types/babel__core@7.20.5':
+ resolution: {integrity: sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==}
+
+ '@types/babel__generator@7.27.0':
+ resolution: {integrity: sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==}
+
+ '@types/babel__template@7.4.4':
+ resolution: {integrity: sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==}
+
+ '@types/babel__traverse@7.28.0':
+ resolution: {integrity: sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==}
+
+ '@types/estree@1.0.9':
+ resolution: {integrity: sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==}
+
+ '@vitejs/plugin-react@4.7.0':
+ resolution: {integrity: sha512-gUu9hwfWvvEDBBmgtAowQCojwZmJ5mcLn3aufeCsitijs3+f2NsrPtlAWIR6OPiqljl96GVCUbLe0HyqIpVaoA==}
+ engines: {node: ^14.18.0 || >=16.0.0}
+ peerDependencies:
+ vite: ^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0
+
+ baseline-browser-mapping@2.11.34:
+ resolution: {integrity: sha512-Aqr8mA1kIvyJ3UY5YAY7ib8LSo1DmDMbMnVqGPtsKW1LxaFQFwn9Eb3yvq2fUKJsRvaXjnAV7R5osqETapzNiA==}
+ engines: {node: '>=6.0.0'}
+ hasBin: true
+
+ browserslist@4.29.5:
+ resolution: {integrity: sha512-khTmKu5NGYTotqcYo8Bew2k4RAE9PuXRSFRXG2lQBrCEQHyeQ/vX6gKMxfokwywBsLiXZMnHsmWMG1pnO8J7uQ==}
+ engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7}
+ hasBin: true
+
+ caniuse-lite@1.0.30001820:
+ resolution: {integrity: sha512-gfWD6MxqVCqAOwmODfZf7DchWvlrA+SRdNX7ceLfg3xRqe5ssjjpF7EgslC/0tiLiNTajdaNN/q8c/5u/7Gz8w==}
+
+ clsx@2.1.1:
+ resolution: {integrity: sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==}
+ engines: {node: '>=6'}
+
+ convert-source-map@2.0.0:
+ resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==}
+
+ debug@4.4.3:
+ resolution: {integrity: sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==}
+ engines: {node: '>=6.0'}
+ peerDependencies:
+ supports-color: '*'
+ peerDependenciesMeta:
+ supports-color:
+ optional: true
+
+ detect-libc@2.1.2:
+ resolution: {integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==}
+ engines: {node: '>=8'}
+
+ electron-to-chromium@1.5.467:
+ resolution: {integrity: sha512-Nw9X371rtZmYcBSnEijnCDONILjs93biCZO34juC7MBOmK8E+GFnyD4T/WvyrlJkppul7oxg6TwvtKst9SODTw==}
+
+ enhanced-resolve@5.26.0:
+ resolution: {integrity: sha512-9vhedylFonb2YGogzUKX6+Ja72gOJbN1QHAqdrvqLwhdl/QWbKopzoUC9EbQNVsAns/bx/4uyqalrQAoy1IByw==}
+ engines: {node: '>=10.13.0'}
+
+ esbuild@0.25.12:
+ resolution: {integrity: sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg==}
+ engines: {node: '>=18'}
+ hasBin: true
+
+ escalade@3.2.0:
+ resolution: {integrity: sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==}
+ engines: {node: '>=6'}
+
+ fdir@6.5.0:
+ resolution: {integrity: sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==}
+ engines: {node: '>=12.0.0'}
+ peerDependencies:
+ picomatch: ^3 || ^4
+ peerDependenciesMeta:
+ picomatch:
+ optional: true
+
+ fsevents@2.3.3:
+ resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==}
+ engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0}
+ os: [darwin]
+
+ gensync@1.0.0-beta.2:
+ resolution: {integrity: sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==}
+ engines: {node: '>=6.9.0'}
+
+ graceful-fs@4.2.11:
+ resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==}
+
+ jiti@2.7.0:
+ resolution: {integrity: sha512-AC/7JofJvZGrrneWNaEnJeOLUx+JlGt7tNa0wZiRPT4MY1wmfKjt2+6O2p2uz2+skll8OZZmJMNqeke7kKbNgQ==}
+ hasBin: true
+
+ js-tokens@4.0.0:
+ resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==}
+
+ jsesc@3.1.0:
+ resolution: {integrity: sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==}
+ engines: {node: '>=6'}
+ hasBin: true
+
+ json5@2.2.3:
+ resolution: {integrity: sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==}
+ engines: {node: '>=6'}
+ hasBin: true
+
+ lightningcss-android-arm64@1.32.0:
+ resolution: {integrity: sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg==}
+ engines: {node: '>= 12.0.0'}
+ cpu: [arm64]
+ os: [android]
+
+ lightningcss-darwin-arm64@1.32.0:
+ resolution: {integrity: sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ==}
+ engines: {node: '>= 12.0.0'}
+ cpu: [arm64]
+ os: [darwin]
+
+ lightningcss-darwin-x64@1.32.0:
+ resolution: {integrity: sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w==}
+ engines: {node: '>= 12.0.0'}
+ cpu: [x64]
+ os: [darwin]
+
+ lightningcss-freebsd-x64@1.32.0:
+ resolution: {integrity: sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig==}
+ engines: {node: '>= 12.0.0'}
+ cpu: [x64]
+ os: [freebsd]
+
+ lightningcss-linux-arm-gnueabihf@1.32.0:
+ resolution: {integrity: sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw==}
+ engines: {node: '>= 12.0.0'}
+ cpu: [arm]
+ os: [linux]
+
+ lightningcss-linux-arm64-gnu@1.32.0:
+ resolution: {integrity: sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ==}
+ engines: {node: '>= 12.0.0'}
+ cpu: [arm64]
+ os: [linux]
+ libc: [glibc]
+
+ lightningcss-linux-arm64-musl@1.32.0:
+ resolution: {integrity: sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg==}
+ engines: {node: '>= 12.0.0'}
+ cpu: [arm64]
+ os: [linux]
+ libc: [musl]
+
+ lightningcss-linux-x64-gnu@1.32.0:
+ resolution: {integrity: sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA==}
+ engines: {node: '>= 12.0.0'}
+ cpu: [x64]
+ os: [linux]
+ libc: [glibc]
+
+ lightningcss-linux-x64-musl@1.32.0:
+ resolution: {integrity: sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg==}
+ engines: {node: '>= 12.0.0'}
+ cpu: [x64]
+ os: [linux]
+ libc: [musl]
+
+ lightningcss-win32-arm64-msvc@1.32.0:
+ resolution: {integrity: sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw==}
+ engines: {node: '>= 12.0.0'}
+ cpu: [arm64]
+ os: [win32]
+
+ lightningcss-win32-x64-msvc@1.32.0:
+ resolution: {integrity: sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q==}
+ engines: {node: '>= 12.0.0'}
+ cpu: [x64]
+ os: [win32]
+
+ lightningcss@1.32.0:
+ resolution: {integrity: sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ==}
+ engines: {node: '>= 12.0.0'}
+
+ lru-cache@5.1.1:
+ resolution: {integrity: sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==}
+
+ lucide-react@1.64.0:
+ resolution: {integrity: sha512-v626F/bcLNvFom6vzztHILtG9tUgXXbgA5AGkd82YkV401+Ko/NHyrWafbPwGofa+6p/EgDJcvEkIwsMH9098w==}
+ peerDependencies:
+ '@types/react': '*'
+ react: ^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0
+ peerDependenciesMeta:
+ '@types/react':
+ optional: true
+
+ magic-string@0.30.21:
+ resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==}
+
+ ms@2.1.3:
+ resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==}
+
+ nanoid@3.3.20:
+ resolution: {integrity: sha512-uKdg2G3GNCKQn9byYOpxbGqrT2fGO5KRt5J/8b3pok8rT6qxGWF6hxMyJiEYtAf+FVyYuD9hRaDqX5uPFYJ4ZQ==}
+ engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1}
+ hasBin: true
+
+ node-releases@2.0.60:
+ resolution: {integrity: sha512-OmO8+GM1JUZqXET720VrJCwkjUCXmCMQm0Z3UDDRbfNLHS4NEAvY60j5jiFv0tkKw2PNNlqOLhahVmwJIOLDIQ==}
+ engines: {node: '>=18'}
+
+ picocolors@1.1.1:
+ resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==}
+
+ picomatch@4.0.7:
+ resolution: {integrity: sha512-qcJu88Q2IWqJsDD529JKMdwGm/dvInW4HvQnRwiH9JtihJvzGOscDtHE3x1pBKeUOTysQ8kVmLnJ2kJu7yhcGA==}
+ engines: {node: '>=12'}
+
+ postcss@8.5.32:
+ resolution: {integrity: sha512-LJlKCCAUFKOuZxipUdYw1NzIITEsPcgyOG9LkvRJvuKj/bRzbuRY0ARXvL0pWGpsZvn8jku+LMep2eU0seHp8g==}
+ engines: {node: ^10 || ^12 || >=14}
+
+ react-dom@19.3.1:
+ resolution: {integrity: sha512-Y0bARz53lhEC1NMCASs0/j+faB/zqt3R8hpvg1e8+61ZVJ4UL0UdC4cBbXfZMcIuhrfgeILTLe+ZeBCavQPtKQ==}
+ peerDependencies:
+ react: ^19.3.1
+
+ react-refresh@0.17.0:
+ resolution: {integrity: sha512-z6F7K9bV85EfseRCp2bzrpyQ0Gkw1uLoCel9XBVWPg/TjRj94SkJzUTGfOa4bs7iJvBWtQG0Wq7wnI0syw3EBQ==}
+ engines: {node: '>=0.10.0'}
+
+ react@19.3.1:
+ resolution: {integrity: sha512-QNhWdt7nrgVWWQG58QuGsAR3jv3Hbe4mb4vz7w2csnHavciZJgda1CPNaldF6eVxv90ud3jUXpKlHL69gO38yw==}
+ engines: {node: '>=0.10.0'}
+
+ rollup@4.66.0:
+ resolution: {integrity: sha512-Onq6/qO4ARhxIs+jYiHE0pYehadLyMECAiGEqQ+0/aWND3+2PmifJIIqBLYtuBODqHTlOUEQFtIVPyCy0Lck7A==}
+ engines: {node: '>=18.0.0', npm: '>=8.0.0'}
+ hasBin: true
+
+ scheduler@0.28.0:
+ resolution: {integrity: sha512-juorfCmIkIw8tT+p5BXSm6PJjQF/ycEYmKyzURCIt/RaZIhL+PulbQ9Yu2z1HdOJDdqDTlxA1+xKBmHXJsczAw==}
+
+ semver@6.3.1:
+ resolution: {integrity: sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==}
+ hasBin: true
+
+ source-map-js@1.2.2:
+ resolution: {integrity: sha512-KGj/8Y43x35aZVDtt+J4mK1hoLGHULMYfSkODJNQjNDC3oW1PqPoxMwo0pLUsWM/UEGzON/NxeHywEfNXNP3Vw==}
+ engines: {node: '>=0.10.0'}
+
+ tailwind-merge@3.7.0:
+ resolution: {integrity: sha512-XPPUyAc+cvspz3lHTcR/QgPfW2A0lv/xQNIjX3HGhLR+Nq2lHaLq5MtTesHn8GUr3W3DguT2KT5x3NVgRtYwmA==}
+
+ tailwindcss@4.3.3:
+ resolution: {integrity: sha512-gOhV3P7ufE62QDGg1zVaTgCR+EtPv92k2nIhVcVKcLmxT1sUBsQGhnZj175j+MqRt4zLF7ic+sCYjfhxMxj7YQ==}
+
+ tapable@2.3.3:
+ resolution: {integrity: sha512-uxc/zpqFg6x7C8vOE7lh6Lbda8eEL9zmVm/PLeTPBRhh1xCgdWaQ+J1CUieGpIfm2HdtsUpRv+HshiasBMcc6A==}
+ engines: {node: '>=6'}
+
+ tinyglobby@0.2.17:
+ resolution: {integrity: sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==}
+ engines: {node: '>=12.0.0'}
+
+ update-browserslist-db@1.3.4:
+ resolution: {integrity: sha512-t8wonIOdQyOjtPd2VktlBu6QjTVTks6v8bSi7kd0r2Vll8TK1JYDZtRRdbmCHOAMphkZgrzK+L+vXK4pyZ8t4A==}
+ hasBin: true
+ peerDependencies:
+ browserslist: '>= 4.21.0'
+
+ vite-plugin-singlefile@2.3.4:
+ resolution: {integrity: sha512-1FAhlTCL6KJDpSUudrVU+gspapvS+tmKCg70vwNaAU6tyXn3E/C6+NnwQIze06byhBRv7VggZ78K0h+J/Rsdrg==}
+ engines: {node: '>18.0.0'}
+ peerDependencies:
+ rollup: ^4.59.0
+ vite: ^5.4.21 || ^6.0.0 || ^7.0.0 || ^8.0.0
+ peerDependenciesMeta:
+ rollup:
+ optional: true
+
+ vite@6.4.5:
+ resolution: {integrity: sha512-4enwjFfi4IwtEh0w+jPzpLVOFQiPHmmzPxcHXQOyEur47o6u0jE573DmGpjA+7irXoMLYRZD5ZaFUEneS3abPg==}
+ engines: {node: ^18.0.0 || ^20.0.0 || >=22.0.0}
+ hasBin: true
+ peerDependencies:
+ '@types/node': ^18.0.0 || ^20.0.0 || >=22.0.0
+ jiti: '>=1.21.0'
+ less: '*'
+ lightningcss: ^1.21.0
+ sass: '*'
+ sass-embedded: '*'
+ stylus: '*'
+ sugarss: '*'
+ terser: ^5.16.0
+ tsx: ^4.8.1
+ yaml: ^2.4.2
+ peerDependenciesMeta:
+ '@types/node':
+ optional: true
+ jiti:
+ optional: true
+ less:
+ optional: true
+ lightningcss:
+ optional: true
+ sass:
+ optional: true
+ sass-embedded:
+ optional: true
+ stylus:
+ optional: true
+ sugarss:
+ optional: true
+ terser:
+ optional: true
+ tsx:
+ optional: true
+ yaml:
+ optional: true
+
+ yallist@3.1.1:
+ resolution: {integrity: sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==}
+
+snapshots:
+
+ '@babel/code-frame@7.29.7':
+ dependencies:
+ '@babel/helper-validator-identifier': 7.29.7
+ js-tokens: 4.0.0
+ picocolors: 1.1.1
+
+ '@babel/compat-data@7.29.7': {}
+
+ '@babel/core@7.29.7':
+ dependencies:
+ '@babel/code-frame': 7.29.7
+ '@babel/generator': 7.29.8
+ '@babel/helper-compilation-targets': 7.29.7
+ '@babel/helper-module-transforms': 7.29.7(@babel/core@7.29.7)
+ '@babel/helpers': 7.29.10
+ '@babel/parser': 7.29.11
+ '@babel/template': 7.29.7
+ '@babel/traverse': 7.29.10
+ '@babel/types': 7.29.11
+ '@jridgewell/remapping': 2.3.5
+ convert-source-map: 2.0.0
+ debug: 4.4.3
+ gensync: 1.0.0-beta.2
+ json5: 2.2.3
+ semver: 6.3.1
+ transitivePeerDependencies:
+ - supports-color
+
+ '@babel/generator@7.29.8':
+ dependencies:
+ '@babel/parser': 7.29.11
+ '@babel/types': 7.29.11
+ '@jridgewell/gen-mapping': 0.3.13
+ '@jridgewell/trace-mapping': 0.3.31
+ jsesc: 3.1.0
+
+ '@babel/helper-compilation-targets@7.29.7':
+ dependencies:
+ '@babel/compat-data': 7.29.7
+ '@babel/helper-validator-option': 7.29.7
+ browserslist: 4.29.5
+ lru-cache: 5.1.1
+ semver: 6.3.1
+
+ '@babel/helper-globals@7.29.7': {}
+
+ '@babel/helper-module-imports@7.29.7':
+ dependencies:
+ '@babel/traverse': 7.29.10
+ '@babel/types': 7.29.11
+ transitivePeerDependencies:
+ - supports-color
+
+ '@babel/helper-module-transforms@7.29.7(@babel/core@7.29.7)':
+ dependencies:
+ '@babel/core': 7.29.7
+ '@babel/helper-module-imports': 7.29.7
+ '@babel/helper-validator-identifier': 7.29.7
+ '@babel/traverse': 7.29.10
+ transitivePeerDependencies:
+ - supports-color
+
+ '@babel/helper-plugin-utils@7.29.7': {}
+
+ '@babel/helper-string-parser@7.29.11': {}
+
+ '@babel/helper-validator-identifier@7.29.7': {}
+
+ '@babel/helper-validator-option@7.29.7': {}
+
+ '@babel/helpers@7.29.10':
+ dependencies:
+ '@babel/template': 7.29.7
+ '@babel/types': 7.29.11
+
+ '@babel/parser@7.29.11':
+ dependencies:
+ '@babel/types': 7.29.11
+
+ '@babel/plugin-transform-react-jsx-self@7.29.7(@babel/core@7.29.7)':
+ dependencies:
+ '@babel/core': 7.29.7
+ '@babel/helper-plugin-utils': 7.29.7
+
+ '@babel/plugin-transform-react-jsx-source@7.29.7(@babel/core@7.29.7)':
+ dependencies:
+ '@babel/core': 7.29.7
+ '@babel/helper-plugin-utils': 7.29.7
+
+ '@babel/template@7.29.7':
+ dependencies:
+ '@babel/code-frame': 7.29.7
+ '@babel/parser': 7.29.11
+ '@babel/types': 7.29.11
+
+ '@babel/traverse@7.29.10':
+ dependencies:
+ '@babel/code-frame': 7.29.7
+ '@babel/generator': 7.29.8
+ '@babel/helper-globals': 7.29.7
+ '@babel/parser': 7.29.11
+ '@babel/template': 7.29.7
+ '@babel/types': 7.29.11
+ debug: 4.4.3
+ transitivePeerDependencies:
+ - supports-color
+
+ '@babel/types@7.29.11':
+ dependencies:
+ '@babel/helper-string-parser': 7.29.11
+ '@babel/helper-validator-identifier': 7.29.7
+
+ '@esbuild/aix-ppc64@0.25.12':
+ optional: true
+
+ '@esbuild/android-arm64@0.25.12':
+ optional: true
+
+ '@esbuild/android-arm@0.25.12':
+ optional: true
+
+ '@esbuild/android-x64@0.25.12':
+ optional: true
+
+ '@esbuild/darwin-arm64@0.25.12':
+ optional: true
+
+ '@esbuild/darwin-x64@0.25.12':
+ optional: true
+
+ '@esbuild/freebsd-arm64@0.25.12':
+ optional: true
+
+ '@esbuild/freebsd-x64@0.25.12':
+ optional: true
+
+ '@esbuild/linux-arm64@0.25.12':
+ optional: true
+
+ '@esbuild/linux-arm@0.25.12':
+ optional: true
+
+ '@esbuild/linux-ia32@0.25.12':
+ optional: true
+
+ '@esbuild/linux-loong64@0.25.12':
+ optional: true
+
+ '@esbuild/linux-mips64el@0.25.12':
+ optional: true
+
+ '@esbuild/linux-ppc64@0.25.12':
+ optional: true
+
+ '@esbuild/linux-riscv64@0.25.12':
+ optional: true
+
+ '@esbuild/linux-s390x@0.25.12':
+ optional: true
+
+ '@esbuild/linux-x64@0.25.12':
+ optional: true
+
+ '@esbuild/netbsd-arm64@0.25.12':
+ optional: true
+
+ '@esbuild/netbsd-x64@0.25.12':
+ optional: true
+
+ '@esbuild/openbsd-arm64@0.25.12':
+ optional: true
+
+ '@esbuild/openbsd-x64@0.25.12':
+ optional: true
+
+ '@esbuild/openharmony-arm64@0.25.12':
+ optional: true
+
+ '@esbuild/sunos-x64@0.25.12':
+ optional: true
+
+ '@esbuild/win32-arm64@0.25.12':
+ optional: true
+
+ '@esbuild/win32-ia32@0.25.12':
+ optional: true
+
+ '@esbuild/win32-x64@0.25.12':
+ optional: true
+
+ '@jridgewell/gen-mapping@0.3.13':
+ dependencies:
+ '@jridgewell/sourcemap-codec': 1.6.0
+ '@jridgewell/trace-mapping': 0.3.31
+
+ '@jridgewell/remapping@2.3.5':
+ dependencies:
+ '@jridgewell/gen-mapping': 0.3.13
+ '@jridgewell/trace-mapping': 0.3.31
+
+ '@jridgewell/resolve-uri@3.1.2': {}
+
+ '@jridgewell/sourcemap-codec@1.6.0': {}
+
+ '@jridgewell/trace-mapping@0.3.31':
+ dependencies:
+ '@jridgewell/resolve-uri': 3.1.2
+ '@jridgewell/sourcemap-codec': 1.6.0
+
+ '@napi-rs/lzma-linux-x64-gnu@1.5.1':
+ optional: true
+
+ '@rolldown/pluginutils@1.0.0-beta.27': {}
+
+ '@rollup/rollup-android-arm-eabi@4.66.0':
+ optional: true
+
+ '@rollup/rollup-android-arm64@4.66.0':
+ optional: true
+
+ '@rollup/rollup-darwin-arm64@4.66.0':
+ optional: true
+
+ '@rollup/rollup-darwin-x64@4.66.0':
+ optional: true
+
+ '@rollup/rollup-freebsd-arm64@4.66.0':
+ optional: true
+
+ '@rollup/rollup-freebsd-x64@4.66.0':
+ optional: true
+
+ '@rollup/rollup-linux-arm-gnueabihf@4.66.0':
+ optional: true
+
+ '@rollup/rollup-linux-arm-musleabihf@4.66.0':
+ optional: true
+
+ '@rollup/rollup-linux-arm64-gnu@4.66.0':
+ optional: true
+
+ '@rollup/rollup-linux-arm64-musl@4.66.0':
+ optional: true
+
+ '@rollup/rollup-linux-loong64-gnu@4.66.0':
+ optional: true
+
+ '@rollup/rollup-linux-loong64-musl@4.66.0':
+ optional: true
+
+ '@rollup/rollup-linux-ppc64-gnu@4.66.0':
+ optional: true
+
+ '@rollup/rollup-linux-ppc64-musl@4.66.0':
+ optional: true
+
+ '@rollup/rollup-linux-riscv64-gnu@4.66.0':
+ optional: true
+
+ '@rollup/rollup-linux-riscv64-musl@4.66.0':
+ optional: true
+
+ '@rollup/rollup-linux-s390x-gnu@4.66.0':
+ optional: true
+
+ '@rollup/rollup-linux-x64-gnu@4.66.0':
+ optional: true
+
+ '@rollup/rollup-linux-x64-musl@4.66.0':
+ optional: true
+
+ '@rollup/rollup-openbsd-x64@4.66.0':
+ optional: true
+
+ '@rollup/rollup-openharmony-arm64@4.66.0':
+ optional: true
+
+ '@rollup/rollup-win32-arm64-msvc@4.66.0':
+ optional: true
+
+ '@rollup/rollup-win32-ia32-msvc@4.66.0':
+ optional: true
+
+ '@rollup/rollup-win32-x64-gnu@4.66.0':
+ optional: true
+
+ '@rollup/rollup-win32-x64-msvc@4.66.0':
+ optional: true
+
+ '@tailwindcss/node@4.3.3':
+ dependencies:
+ '@jridgewell/remapping': 2.3.5
+ enhanced-resolve: 5.26.0
+ jiti: 2.7.0
+ lightningcss: 1.32.0
+ magic-string: 0.30.21
+ source-map-js: 1.2.2
+ tailwindcss: 4.3.3
+
+ '@tailwindcss/oxide-android-arm64@4.3.3':
+ optional: true
+
+ '@tailwindcss/oxide-darwin-arm64@4.3.3':
+ optional: true
+
+ '@tailwindcss/oxide-darwin-x64@4.3.3':
+ optional: true
+
+ '@tailwindcss/oxide-freebsd-x64@4.3.3':
+ optional: true
+
+ '@tailwindcss/oxide-linux-arm-gnueabihf@4.3.3':
+ optional: true
+
+ '@tailwindcss/oxide-linux-arm64-gnu@4.3.3':
+ optional: true
+
+ '@tailwindcss/oxide-linux-arm64-musl@4.3.3':
+ optional: true
+
+ '@tailwindcss/oxide-linux-x64-gnu@4.3.3':
+ optional: true
+
+ '@tailwindcss/oxide-linux-x64-musl@4.3.3':
+ optional: true
+
+ '@tailwindcss/oxide-wasm32-wasi@4.3.3':
+ optional: true
+
+ '@tailwindcss/oxide-win32-arm64-msvc@4.3.3':
+ optional: true
+
+ '@tailwindcss/oxide-win32-x64-msvc@4.3.3':
+ optional: true
+
+ '@tailwindcss/oxide@4.3.3':
+ optionalDependencies:
+ '@tailwindcss/oxide-android-arm64': 4.3.3
+ '@tailwindcss/oxide-darwin-arm64': 4.3.3
+ '@tailwindcss/oxide-darwin-x64': 4.3.3
+ '@tailwindcss/oxide-freebsd-x64': 4.3.3
+ '@tailwindcss/oxide-linux-arm-gnueabihf': 4.3.3
+ '@tailwindcss/oxide-linux-arm64-gnu': 4.3.3
+ '@tailwindcss/oxide-linux-arm64-musl': 4.3.3
+ '@tailwindcss/oxide-linux-x64-gnu': 4.3.3
+ '@tailwindcss/oxide-linux-x64-musl': 4.3.3
+ '@tailwindcss/oxide-wasm32-wasi': 4.3.3
+ '@tailwindcss/oxide-win32-arm64-msvc': 4.3.3
+ '@tailwindcss/oxide-win32-x64-msvc': 4.3.3
+
+ '@tailwindcss/vite@4.3.3(vite@6.4.5(jiti@2.7.0)(lightningcss@1.32.0))':
+ dependencies:
+ '@tailwindcss/node': 4.3.3
+ '@tailwindcss/oxide': 4.3.3
+ tailwindcss: 4.3.3
+ vite: 6.4.5(jiti@2.7.0)(lightningcss@1.32.0)
+
+ '@types/babel__core@7.20.5':
+ dependencies:
+ '@babel/parser': 7.29.11
+ '@babel/types': 7.29.11
+ '@types/babel__generator': 7.27.0
+ '@types/babel__template': 7.4.4
+ '@types/babel__traverse': 7.28.0
+
+ '@types/babel__generator@7.27.0':
+ dependencies:
+ '@babel/types': 7.29.11
+
+ '@types/babel__template@7.4.4':
+ dependencies:
+ '@babel/parser': 7.29.11
+ '@babel/types': 7.29.11
+
+ '@types/babel__traverse@7.28.0':
+ dependencies:
+ '@babel/types': 7.29.11
+
+ '@types/estree@1.0.9': {}
+
+ '@vitejs/plugin-react@4.7.0(vite@6.4.5(jiti@2.7.0)(lightningcss@1.32.0))':
+ dependencies:
+ '@babel/core': 7.29.7
+ '@babel/plugin-transform-react-jsx-self': 7.29.7(@babel/core@7.29.7)
+ '@babel/plugin-transform-react-jsx-source': 7.29.7(@babel/core@7.29.7)
+ '@rolldown/pluginutils': 1.0.0-beta.27
+ '@types/babel__core': 7.20.5
+ react-refresh: 0.17.0
+ vite: 6.4.5(jiti@2.7.0)(lightningcss@1.32.0)
+ transitivePeerDependencies:
+ - supports-color
+
+ baseline-browser-mapping@2.11.34: {}
+
+ browserslist@4.29.5:
+ dependencies:
+ baseline-browser-mapping: 2.11.34
+ caniuse-lite: 1.0.30001820
+ electron-to-chromium: 1.5.467
+ node-releases: 2.0.60
+ update-browserslist-db: 1.3.4(browserslist@4.29.5)
+
+ caniuse-lite@1.0.30001820: {}
+
+ clsx@2.1.1: {}
+
+ convert-source-map@2.0.0: {}
+
+ debug@4.4.3:
+ dependencies:
+ ms: 2.1.3
+
+ detect-libc@2.1.2: {}
+
+ electron-to-chromium@1.5.467: {}
+
+ enhanced-resolve@5.26.0:
+ dependencies:
+ graceful-fs: 4.2.11
+ tapable: 2.3.3
+
+ esbuild@0.25.12:
+ optionalDependencies:
+ '@esbuild/aix-ppc64': 0.25.12
+ '@esbuild/android-arm': 0.25.12
+ '@esbuild/android-arm64': 0.25.12
+ '@esbuild/android-x64': 0.25.12
+ '@esbuild/darwin-arm64': 0.25.12
+ '@esbuild/darwin-x64': 0.25.12
+ '@esbuild/freebsd-arm64': 0.25.12
+ '@esbuild/freebsd-x64': 0.25.12
+ '@esbuild/linux-arm': 0.25.12
+ '@esbuild/linux-arm64': 0.25.12
+ '@esbuild/linux-ia32': 0.25.12
+ '@esbuild/linux-loong64': 0.25.12
+ '@esbuild/linux-mips64el': 0.25.12
+ '@esbuild/linux-ppc64': 0.25.12
+ '@esbuild/linux-riscv64': 0.25.12
+ '@esbuild/linux-s390x': 0.25.12
+ '@esbuild/linux-x64': 0.25.12
+ '@esbuild/netbsd-arm64': 0.25.12
+ '@esbuild/netbsd-x64': 0.25.12
+ '@esbuild/openbsd-arm64': 0.25.12
+ '@esbuild/openbsd-x64': 0.25.12
+ '@esbuild/openharmony-arm64': 0.25.12
+ '@esbuild/sunos-x64': 0.25.12
+ '@esbuild/win32-arm64': 0.25.12
+ '@esbuild/win32-ia32': 0.25.12
+ '@esbuild/win32-x64': 0.25.12
+
+ escalade@3.2.0: {}
+
+ fdir@6.5.0(picomatch@4.0.7):
+ optionalDependencies:
+ picomatch: 4.0.7
+
+ fsevents@2.3.3:
+ optional: true
+
+ gensync@1.0.0-beta.2: {}
+
+ graceful-fs@4.2.11: {}
+
+ jiti@2.7.0: {}
+
+ js-tokens@4.0.0: {}
+
+ jsesc@3.1.0: {}
+
+ json5@2.2.3: {}
+
+ lightningcss-android-arm64@1.32.0:
+ optional: true
+
+ lightningcss-darwin-arm64@1.32.0:
+ optional: true
+
+ lightningcss-darwin-x64@1.32.0:
+ optional: true
+
+ lightningcss-freebsd-x64@1.32.0:
+ optional: true
+
+ lightningcss-linux-arm-gnueabihf@1.32.0:
+ optional: true
+
+ lightningcss-linux-arm64-gnu@1.32.0:
+ optional: true
+
+ lightningcss-linux-arm64-musl@1.32.0:
+ optional: true
+
+ lightningcss-linux-x64-gnu@1.32.0:
+ optional: true
+
+ lightningcss-linux-x64-musl@1.32.0:
+ optional: true
+
+ lightningcss-win32-arm64-msvc@1.32.0:
+ optional: true
+
+ lightningcss-win32-x64-msvc@1.32.0:
+ optional: true
+
+ lightningcss@1.32.0:
+ dependencies:
+ detect-libc: 2.1.2
+ optionalDependencies:
+ lightningcss-android-arm64: 1.32.0
+ lightningcss-darwin-arm64: 1.32.0
+ lightningcss-darwin-x64: 1.32.0
+ lightningcss-freebsd-x64: 1.32.0
+ lightningcss-linux-arm-gnueabihf: 1.32.0
+ lightningcss-linux-arm64-gnu: 1.32.0
+ lightningcss-linux-arm64-musl: 1.32.0
+ lightningcss-linux-x64-gnu: 1.32.0
+ lightningcss-linux-x64-musl: 1.32.0
+ lightningcss-win32-arm64-msvc: 1.32.0
+ lightningcss-win32-x64-msvc: 1.32.0
+
+ lru-cache@5.1.1:
+ dependencies:
+ yallist: 3.1.1
+
+ lucide-react@1.64.0(react@19.3.1):
+ dependencies:
+ react: 19.3.1
+
+ magic-string@0.30.21:
+ dependencies:
+ '@jridgewell/sourcemap-codec': 1.6.0
+
+ ms@2.1.3: {}
+
+ nanoid@3.3.20: {}
+
+ node-releases@2.0.60: {}
+
+ picocolors@1.1.1: {}
+
+ picomatch@4.0.7: {}
+
+ postcss@8.5.32:
+ dependencies:
+ nanoid: 3.3.20
+ picocolors: 1.1.1
+ source-map-js: 1.2.2
+
+ react-dom@19.3.1(react@19.3.1):
+ dependencies:
+ react: 19.3.1
+ scheduler: 0.28.0
+
+ react-refresh@0.17.0: {}
+
+ react@19.3.1: {}
+
+ rollup@4.66.0:
+ dependencies:
+ '@types/estree': 1.0.9
+ optionalDependencies:
+ '@napi-rs/lzma-linux-x64-gnu': 1.5.1
+ '@rollup/rollup-android-arm-eabi': 4.66.0
+ '@rollup/rollup-android-arm64': 4.66.0
+ '@rollup/rollup-darwin-arm64': 4.66.0
+ '@rollup/rollup-darwin-x64': 4.66.0
+ '@rollup/rollup-freebsd-arm64': 4.66.0
+ '@rollup/rollup-freebsd-x64': 4.66.0
+ '@rollup/rollup-linux-arm-gnueabihf': 4.66.0
+ '@rollup/rollup-linux-arm-musleabihf': 4.66.0
+ '@rollup/rollup-linux-arm64-gnu': 4.66.0
+ '@rollup/rollup-linux-arm64-musl': 4.66.0
+ '@rollup/rollup-linux-loong64-gnu': 4.66.0
+ '@rollup/rollup-linux-loong64-musl': 4.66.0
+ '@rollup/rollup-linux-ppc64-gnu': 4.66.0
+ '@rollup/rollup-linux-ppc64-musl': 4.66.0
+ '@rollup/rollup-linux-riscv64-gnu': 4.66.0
+ '@rollup/rollup-linux-riscv64-musl': 4.66.0
+ '@rollup/rollup-linux-s390x-gnu': 4.66.0
+ '@rollup/rollup-linux-x64-gnu': 4.66.0
+ '@rollup/rollup-linux-x64-musl': 4.66.0
+ '@rollup/rollup-openbsd-x64': 4.66.0
+ '@rollup/rollup-openharmony-arm64': 4.66.0
+ '@rollup/rollup-win32-arm64-msvc': 4.66.0
+ '@rollup/rollup-win32-ia32-msvc': 4.66.0
+ '@rollup/rollup-win32-x64-gnu': 4.66.0
+ '@rollup/rollup-win32-x64-msvc': 4.66.0
+ fsevents: 2.3.3
+
+ scheduler@0.28.0: {}
+
+ semver@6.3.1: {}
+
+ source-map-js@1.2.2: {}
+
+ tailwind-merge@3.7.0: {}
+
+ tailwindcss@4.3.3: {}
+
+ tapable@2.3.3: {}
+
+ tinyglobby@0.2.17:
+ dependencies:
+ fdir: 6.5.0(picomatch@4.0.7)
+ picomatch: 4.0.7
+
+ update-browserslist-db@1.3.4(browserslist@4.29.5):
+ dependencies:
+ browserslist: 4.29.5
+ escalade: 3.2.0
+ picocolors: 1.1.1
+
+ vite-plugin-singlefile@2.3.4(rollup@4.66.0)(vite@6.4.5(jiti@2.7.0)(lightningcss@1.32.0)):
+ dependencies:
+ picomatch: 4.0.7
+ vite: 6.4.5(jiti@2.7.0)(lightningcss@1.32.0)
+ optionalDependencies:
+ rollup: 4.66.0
+
+ vite@6.4.5(jiti@2.7.0)(lightningcss@1.32.0):
+ dependencies:
+ esbuild: 0.25.12
+ fdir: 6.5.0(picomatch@4.0.7)
+ picomatch: 4.0.7
+ postcss: 8.5.32
+ rollup: 4.66.0
+ tinyglobby: 0.2.17
+ optionalDependencies:
+ fsevents: 2.3.3
+ jiti: 2.7.0
+ lightningcss: 1.32.0
+
+ yallist@3.1.1: {}
diff --git a/h3-console/frontend/pnpm-workspace.yaml b/h3-console/frontend/pnpm-workspace.yaml
new file mode 100644
index 0000000..00f6fc4
--- /dev/null
+++ b/h3-console/frontend/pnpm-workspace.yaml
@@ -0,0 +1,2 @@
+allowBuilds:
+ esbuild: set this to true or false
diff --git a/h3-console/frontend/src/App.jsx b/h3-console/frontend/src/App.jsx
new file mode 100644
index 0000000..0459322
--- /dev/null
+++ b/h3-console/frontend/src/App.jsx
@@ -0,0 +1,116 @@
+import React, { useState, useEffect } from 'react';
+import Sidebar from './components/Sidebar';
+import CreateForm from './components/CreateForm';
+import TaskDetail from './components/TaskDetail';
+import { ToastProvider, useToast } from './components/Toast';
+import { fetchBackends } from './api';
+import { getH3Base, getToken, setToken } from './utils';
+
+function tidFromPath() {
+ if (typeof window === 'undefined') return null;
+ let p = window.location.pathname;
+ const base = getH3Base();
+ if (base && p.startsWith(base)) p = p.slice(base.length);
+ const m = p.match(/^\/task\.html\/(.+)$/);
+ return m ? decodeURIComponent(m[1]) : null;
+}
+
+function MainApp() {
+ const { addToast } = useToast();
+ const [selectedId, setSelectedId] = useState(() => tidFromPath());
+ const [initialFormValues, setInitialFormValues] = useState(null);
+ const [backendInfo, setBackendInfo] = useState(null);
+
+ // 路由同步
+ useEffect(() => {
+ const handlePopState = () => {
+ const tid = tidFromPath();
+ setSelectedId(tid);
+ };
+ window.addEventListener('popstate', handlePopState);
+ return () => window.removeEventListener('popstate', handlePopState);
+ }, []);
+
+ // 定时刷新后端状态
+ const loadBackends = async () => {
+ const info = await fetchBackends();
+ if (info) setBackendInfo(info);
+ };
+
+ useEffect(() => {
+ loadBackends();
+ const timer = setInterval(loadBackends, 6000);
+ return () => clearInterval(timer);
+ }, []);
+
+ const handleSelectTask = (id) => {
+ setSelectedId(id);
+ const base = getH3Base();
+ window.history.pushState(null, '', `${base}/task.html/${encodeURIComponent(id)}`);
+ };
+
+ const handleCreateTask = () => {
+ setSelectedId(null);
+ setInitialFormValues(null);
+ const base = getH3Base();
+ window.history.pushState(null, '', `${base}/newtask.html`);
+ };
+
+ const handleRerunTask = (job) => {
+ setSelectedId(null);
+ setInitialFormValues(job.req || {});
+ const base = getH3Base();
+ window.history.pushState(null, '', `${base}/newtask.html`);
+ addToast('已将历史任务参数灌入表单,可直接微调或提交', 'info');
+ };
+
+ const handleManageToken = () => {
+ const cur = getToken();
+ const next = window.prompt('设置 H3 访问令牌 (Access Token),留空清除:', cur);
+ if (next !== null) {
+ setToken(next);
+ addToast(next ? '令牌已保存' : '令牌已清除', 'info');
+ }
+ };
+
+ return (
+
+ {/* 左侧任务栏 (包含系统配置入口) */}
+
+
+ {/* 右侧主工作区 (纯净流式布局,绝不在右上角悬浮危险遮挡元素) */}
+
+
+ {selectedId ? (
+
+ ) : (
+
+ )}
+
+
+
+ );
+}
+
+export default function App() {
+ return (
+
+
+
+ );
+}
diff --git a/h3-console/frontend/src/api.js b/h3-console/frontend/src/api.js
new file mode 100644
index 0000000..a3bca81
--- /dev/null
+++ b/h3-console/frontend/src/api.js
@@ -0,0 +1,113 @@
+import { getH3Base, getToken, setToken } from './utils';
+
+export async function apiFetch(path, options = {}) {
+ const base = getH3Base();
+ const url = path.startsWith('http') ? path : `${base}${path}`;
+ const token = getToken();
+
+ const headers = { ...(options.headers || {}) };
+ if (token) {
+ headers['Authorization'] = `Bearer ${token}`;
+ }
+
+ const res = await fetch(url, { ...options, headers });
+ if (res.status === 401) {
+ setToken('');
+ const newToken = window.prompt('H3 访问令牌 (Token) 已过期或无效,请输入新令牌:');
+ if (newToken) {
+ setToken(newToken);
+ headers['Authorization'] = `Bearer ${newToken}`;
+ return fetch(url, { ...options, headers });
+ }
+ }
+ return res;
+}
+
+export async function fetchJobs({ page = 1, limit = 20, q = '', status = '', kind = '', sort = 'desc', trash = false } = {}) {
+ const params = new URLSearchParams();
+ if (trash) {
+ params.set('trash', 'true');
+ params.set('limit', '50');
+ } else {
+ params.set('page', String(page));
+ params.set('limit', String(limit));
+ if (q) params.set('q', q);
+ if (status) params.set('status', status);
+ if (kind) params.set('kind', kind);
+ if (sort) params.set('sort', sort);
+ }
+
+ const res = await apiFetch(`/v1/videos?${params.toString()}`);
+ if (!res.ok) throw new Error(await res.text());
+ return res.json();
+}
+
+export async function fetchJobDetail(id) {
+ const res = await apiFetch(`/v1/videos/${id}`);
+ if (!res.ok) throw new Error(await res.text());
+ return res.json();
+}
+
+export async function createJob(body) {
+ const res = await apiFetch('/v1/videos', {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify(body),
+ });
+ if (!res.ok) throw new Error(await res.text());
+ return res.json();
+}
+
+export async function uploadAsset(file) {
+ const fd = new FormData();
+ fd.append('file', file);
+ const res = await apiFetch('/v1/assets', {
+ method: 'POST',
+ body: fd,
+ });
+ if (!res.ok) throw new Error(await res.text());
+ return res.json();
+}
+
+export async function prioritizeJob(id) {
+ const res = await apiFetch(`/v1/videos/${id}/prioritize`, { method: 'POST' });
+ if (!res.ok) throw new Error(await res.text());
+ return res.json();
+}
+
+export async function deleteJob(id) {
+ const res = await apiFetch(`/v1/videos/${id}`, { method: 'DELETE' });
+ if (!res.ok) throw new Error(await res.text());
+ return res.json();
+}
+
+export async function restoreJob(id) {
+ const res = await apiFetch(`/v1/videos/${id}/restore`, { method: 'POST' });
+ if (!res.ok) throw new Error(await res.text());
+ return res.json();
+}
+
+export async function cancelJob(id) {
+ const res = await apiFetch(`/v1/videos/${id}/cancel`, { method: 'POST' });
+ if (!res.ok) throw new Error(await res.text());
+ return res.json();
+}
+
+export async function resetGpu() {
+ const res = await apiFetch('/v1/admin/reset-gpu', { method: 'POST' });
+ if (!res.ok) throw new Error(await res.text());
+ return res.json();
+}
+
+export async function fetchBackends() {
+ const res = await apiFetch('/v1/backends');
+ if (!res.ok) return null;
+ return res.json();
+}
+
+export async function fetchComfyHistory(id) {
+ const res = await apiFetch(`/v1/videos/${id}/comfy-history`);
+ if (!res.ok) throw new Error(await res.text());
+ return res.json();
+}
+
diff --git a/h3-console/frontend/src/components/CreateForm.jsx b/h3-console/frontend/src/components/CreateForm.jsx
new file mode 100644
index 0000000..53e2c33
--- /dev/null
+++ b/h3-console/frontend/src/components/CreateForm.jsx
@@ -0,0 +1,442 @@
+import React, { useState, useEffect } from 'react';
+import { Sparkles, Sliders, RefreshCw, Send, AlertCircle, ChevronDown, ChevronUp } from 'lucide-react';
+import MediaUploader from './MediaUploader';
+import { RESOLUTIONS, DURATIONS, STEPS_BY_TURBO, SEED_OPTIONS, DEFAULT_RES, DEFAULT_DUR } from '../constants';
+import { createJob } from '../api';
+import { useToast } from './Toast';
+
+export default function CreateForm({ initialValues, onCreated }) {
+ const { addToast } = useToast();
+ const [tab, setTab] = useState('text'); // 'text' | 'fl2va' | 'ref2va'
+ const [prompt, setPrompt] = useState('');
+ const [resValue, setResValue] = useState(DEFAULT_RES);
+ const [durValue, setDurValue] = useState(DEFAULT_DUR);
+ const [turbo, setTurbo] = useState(false);
+ const [steps, setSteps] = useState('20');
+ const [seed, setSeed] = useState('0');
+ const [cfgScale, setCfgScale] = useState('');
+ const [negativePrompt, setNegativePrompt] = useState('');
+ const [showAdvanced, setShowAdvanced] = useState(false);
+ const [submitting, setSubmitting] = useState(false);
+ const [err, setErr] = useState('');
+
+ // 媒体状态
+ const [firstFrame, setFirstFrame] = useState(null);
+ const [lastFrame, setLastFrame] = useState(null);
+ const [refImages, setRefImages] = useState([]);
+ const [refVideos, setRefVideos] = useState([]);
+ const [refAudios, setRefAudios] = useState([]);
+
+ // 如果从详情页点了“重跑”,注入初始参数
+ useEffect(() => {
+ if (!initialValues) return;
+ const req = initialValues;
+ if (req.first_frame_url || req.last_frame_url) {
+ setTab('fl2va');
+ setFirstFrame(req.first_frame_url || null);
+ setLastFrame(req.last_frame_url || null);
+ } else if ((req.reference_image_urls || []).length || (req.reference_video_urls || []).length || (req.reference_audio_urls || []).length) {
+ setTab('ref2va');
+ setRefImages(req.reference_image_urls || []);
+ setRefVideos(req.reference_video_urls || []);
+ setRefAudios(req.reference_audio_urls || []);
+ } else {
+ setTab('text');
+ }
+
+ setPrompt(req.prompt || '');
+ const matchedRes = RESOLUTIONS.find((r) => r.width === req.width && r.height === req.height);
+ if (matchedRes) setResValue(matchedRes.value);
+
+ const matchedDur = DURATIONS.find((d) => d.frames === req.frames);
+ if (matchedDur) setDurValue(matchedDur.value);
+
+ const isTurbo = req.turbo === false ? false : true;
+ setTurbo(isTurbo);
+ if (req.steps != null) setSteps(String(req.steps));
+ if (req.seed != null) setSeed(String(req.seed));
+ if (req.cfg_scale != null) setCfgScale(String(req.cfg_scale));
+ if (req.negative_prompt) setNegativePrompt(req.negative_prompt);
+ }, [initialValues]);
+
+ // 分辨率对应的时长合法性校验
+ const currentRes = RESOLUTIONS.find((r) => r.value === resValue) || RESOLUTIONS[0];
+ const allowedDurations = DURATIONS.filter((d) => d.frames <= currentRes.maxFrames);
+ const droppedDurations = DURATIONS.filter((d) => d.frames > currentRes.maxFrames);
+
+ useEffect(() => {
+ if (!allowedDurations.some((d) => d.value === durValue)) {
+ setDurValue(allowedDurations[0]?.value || DEFAULT_DUR);
+ }
+ }, [resValue]);
+
+ // Turbo 切换自动联动步数
+ const stepOptions = STEPS_BY_TURBO[turbo];
+ useEffect(() => {
+ if (!stepOptions.some((s) => s.value === steps)) {
+ setSteps(stepOptions[0].value);
+ }
+ }, [turbo]);
+
+ const handleClear = () => {
+ if (!confirm('确定清空当前表单所有内容?')) return;
+ setPrompt('');
+ setResValue(DEFAULT_RES);
+ setDurValue(DEFAULT_DUR);
+ setTurbo(false);
+ setSteps('20');
+ setSeed('0');
+ setCfgScale('');
+ setNegativePrompt('');
+ setFirstFrame(null);
+ setLastFrame(null);
+ setRefImages([]);
+ setRefVideos([]);
+ setRefAudios([]);
+ setErr('');
+ addToast('已恢复默认配置', 'info');
+ };
+
+ const handleSubmit = async (e) => {
+ e.preventDefault();
+ setErr('');
+
+ if (!prompt.trim()) {
+ setErr('请填写 Prompt 提示词');
+ return;
+ }
+
+ const durObj = DURATIONS.find((d) => d.value === durValue);
+ const body = {
+ prompt: prompt.trim(),
+ width: currentRes.width,
+ height: currentRes.height,
+ frames: durObj ? durObj.frames : 243,
+ turbo,
+ steps: parseInt(steps, 10),
+ seed: seed === 'random' ? Math.floor(Math.random() * 2 ** 31) : parseInt(seed, 10),
+ };
+
+ if (cfgScale.trim() !== '') body.cfg_scale = parseFloat(cfgScale);
+ if (negativePrompt.trim() !== '') body.negative_prompt = negativePrompt.trim();
+
+ if (tab === 'fl2va') {
+ if (!firstFrame) {
+ setErr('首尾帧模式下必须上传【首帧图】');
+ return;
+ }
+ body.first_frame_url = firstFrame;
+ if (lastFrame) body.last_frame_url = lastFrame;
+ } else if (tab === 'ref2va') {
+ if (refImages.length === 0 && refVideos.length === 0 && refAudios.length === 0) {
+ setErr('参考模式下至少需要上传 1 张参考图、1 段参考视频或 1 个音频');
+ return;
+ }
+ body.reference_image_urls = refImages;
+ body.reference_video_urls = refVideos;
+ body.reference_audio_urls = refAudios;
+ }
+
+ setSubmitting(true);
+ try {
+ const job = await createJob(body);
+ addToast(`任务提交成功 (#${job.id.substring(0, 8)})`, 'success');
+ onCreated(job.id);
+ } catch (e) {
+ setErr('提交失败: ' + e.message);
+ addToast('提交失败: ' + e.message, 'error');
+ } finally {
+ setSubmitting(false);
+ }
+ };
+
+ return (
+
+ {/* 顶部标签切换 */}
+
+ {[
+ { key: 'text', label: '文生视频', desc: 'Text to Video' },
+ { key: 'fl2va', label: '首尾帧生视频', desc: 'FL2VA' },
+ { key: 'ref2va', label: '多参考图生视频', desc: 'Ref2VA' },
+ ].map((t) => (
+
+ ))}
+
+
+
+
+ );
+}
diff --git a/h3-console/frontend/src/components/JobCard.jsx b/h3-console/frontend/src/components/JobCard.jsx
new file mode 100644
index 0000000..dfe5399
--- /dev/null
+++ b/h3-console/frontend/src/components/JobCard.jsx
@@ -0,0 +1,213 @@
+import React, { useState, useRef } from 'react';
+import { Play, RotateCcw, ArrowUpToLine, Trash2, MoreVertical, Clock } from 'lucide-react';
+import { formatTimeAgo, thumbUrl, h3FileUrl, getH3Base, getJobKind } from '../utils';
+
+export default function JobCard({ job, isSelected, onSelect, onRerun, onPrioritize, onDelete }) {
+ const [isHovered, setIsHovered] = useState(false);
+ const [showMenu, setShowMenu] = useState(false);
+ const videoRef = useRef(null);
+
+ const req = job.req || {};
+ const kind = getJobKind(req);
+ const isDone = job.status === 'done';
+ const isRunning = job.status === 'running';
+ const isQueued = job.status === 'queued';
+ const isFailed = job.status === 'failed';
+ const isCancelled = job.status === 'cancelled';
+
+ // 进度计算
+ const curStep = job.step || 0;
+ const totalSteps = job.total_steps || req.steps || 20;
+ const progressPct = totalSteps > 0 ? Math.min(100, Math.max(curStep > 0 ? 5 : 2, Math.round((curStep / totalSteps) * 100))) : 0;
+
+ // 悬停时自动微播放
+ const handleMouseEnter = () => {
+ setIsHovered(true);
+ if (isDone && videoRef.current) {
+ videoRef.current.currentTime = 0;
+ videoRef.current.play().catch(() => {});
+ }
+ };
+
+ const handleMouseLeave = () => {
+ setIsHovered(false);
+ setShowMenu(false);
+ if (isDone && videoRef.current) {
+ videoRef.current.pause();
+ }
+ };
+
+ // 缩略图地址
+ const videoFileUrl = isDone ? h3FileUrl(`${getH3Base()}/v1/videos/${job.id}/file`) : null;
+ const coverImg = req.first_frame_url || (req.reference_image_urls || [])[0];
+
+ return (
+ onSelect(job.id)}
+ onMouseEnter={handleMouseEnter}
+ onMouseLeave={handleMouseLeave}
+ className={`group relative p-3 rounded-2xl border transition-all duration-200 cursor-pointer flex flex-col gap-2 ${
+ isSelected
+ ? 'bg-emerald-50/80 border-emerald-500 ring-1 ring-emerald-500/40 shadow-xs'
+ : 'bg-white border-slate-200/90 hover:bg-slate-50/80 hover:border-slate-300 shadow-xs'
+ }`}
+ >
+
+ {/* 缩略图容器 (自适应比例) */}
+
+ {isDone && videoFileUrl ? (
+
+ ) : coverImg ? (
+
})
+ ) : (
+
+ {kind.label}
+ {job.partition || 'H3'}
+
+ )}
+
+ {/* 状态悬浮微标 */}
+ {isDone && (
+
+ )}
+ {isRunning && (
+
+
+ {progressPct}%
+
+
+ )}
+ {isQueued && (
+
+ {job.queue_position ? `#${job.queue_position}` : '排队中'}
+
+ )}
+
+
+ {/* 右侧核心信息 */}
+
+
+ {/* 顶栏:短 ID 与状态胶囊 */}
+
+
+ #{job.id.substring(0, 8)}
+
+
+ {/* 状态 Pill */}
+
+ {isDone && (
+
+ 已完成
+
+ )}
+ {isRunning && (
+
+
+ 采样 {curStep}/{totalSteps}步
+
+ )}
+ {isQueued && (
+
+ 排队中
+
+ )}
+ {isFailed && (
+
+ 失败
+
+ )}
+ {isCancelled && (
+
+ 已取消
+
+ )}
+
+
+
+ {/* Prompt 摘要 */}
+
+ {req.prompt || '(无 prompt 描述)'}
+
+
+
+ {/* 底栏:规格、时间与快捷菜单 */}
+
+
+
+ {kind.label} · {req.width ? `${req.width}p` : ''}
+
+
+
+ {formatTimeAgo(job.created_at)}
+
+
+
+ {/* 三点操作菜单 */}
+
e.stopPropagation()}>
+
+
+ {showMenu && (
+
+
+ {isQueued && (
+
+ )}
+
+
+
+ )}
+
+
+
+
+
+ {/* 处于运行中时,卡片底部直接展现清晰的横向进度条 */}
+ {isRunning && (
+
+ )}
+
+ );
+}
diff --git a/h3-console/frontend/src/components/MediaUploader.jsx b/h3-console/frontend/src/components/MediaUploader.jsx
new file mode 100644
index 0000000..fde07e7
--- /dev/null
+++ b/h3-console/frontend/src/components/MediaUploader.jsx
@@ -0,0 +1,201 @@
+import React, { useState, useRef } from 'react';
+import { Upload, Link as LinkIcon, X, Film, Music, Image as ImageIcon, ExternalLink } from 'lucide-react';
+import { uploadAsset } from '../api';
+import { filenameFromUrl, thumbUrl, localAsset } from '../utils';
+
+export default function MediaUploader({
+ label,
+ accept = 'image/*',
+ type = 'image', // 'image' | 'video' | 'audio'
+ multiple = false,
+ maxCount = 99,
+ value, // string | string[]
+ onChange,
+ hint,
+}) {
+ const [urlInput, setUrlInput] = useState('');
+ const [isUploading, setIsUploading] = useState(false);
+ const [isDragOver, setIsDragOver] = useState(false);
+ const fileInputRef = useRef(null);
+
+ const items = multiple ? (Array.isArray(value) ? value : []) : (value ? [value] : []);
+
+ const handleFiles = async (files) => {
+ if (!files || files.length === 0) return;
+ setIsUploading(true);
+ try {
+ const newUrls = [];
+ for (const file of Array.from(files)) {
+ if (items.length + newUrls.length >= maxCount) break;
+ const res = await uploadAsset(file);
+ if (res.url) newUrls.push(res.url);
+ }
+ if (multiple) {
+ onChange([...items, ...newUrls].slice(0, maxCount));
+ } else if (newUrls.length > 0) {
+ onChange(newUrls[0]);
+ }
+ } catch (e) {
+ alert('上传失败: ' + e.message);
+ } finally {
+ setIsUploading(false);
+ if (fileInputRef.current) fileInputRef.current.value = '';
+ }
+ };
+
+ const handleAddUrl = (e) => {
+ if (e.key === 'Enter') {
+ e.preventDefault();
+ const u = urlInput.trim();
+ if (!u) return;
+ if (multiple) {
+ if (items.length >= maxCount) {
+ alert(`最多只允许添加 ${maxCount} 个`);
+ return;
+ }
+ onChange([...items, u]);
+ } else {
+ onChange(u);
+ }
+ setUrlInput('');
+ }
+ };
+
+ const handleRemove = (index) => {
+ if (multiple) {
+ const next = items.filter((_, i) => i !== index);
+ onChange(next);
+ } else {
+ onChange(null);
+ }
+ };
+
+ // 支持键盘粘贴截图
+ const handlePaste = (e) => {
+ const clipboardFiles = e.clipboardData?.files;
+ if (clipboardFiles && clipboardFiles.length > 0) {
+ e.preventDefault();
+ handleFiles(clipboardFiles);
+ }
+ };
+
+ return (
+
+
+
+ {hint && {hint}}
+
+
+ {/* 拖拽上传与输入区 */}
+ {(items.length < maxCount || !multiple) && (
+
+ )}
+
+ {/* 媒体缩略预览网格 */}
+ {items.length > 0 && (
+
+ {items.map((u, idx) => {
+ const rawName = filenameFromUrl(u);
+ return (
+
+ {/* 媒体呈现 */}
+
+ {type === 'image' && (
+
})
+ )}
+ {type === 'video' && (
+
+ )}
+ {type === 'audio' && (
+
+ )}
+
+ {/* 悬停快速操作 */}
+
+
+
+
+
+
+
+
+ {/* 底部文件名 */}
+
+ {rawName || '媒体文件'}
+
+
+ );
+ })}
+
+ )}
+
+ );
+}
+
diff --git a/h3-console/frontend/src/components/Modal.jsx b/h3-console/frontend/src/components/Modal.jsx
new file mode 100644
index 0000000..80589e1
--- /dev/null
+++ b/h3-console/frontend/src/components/Modal.jsx
@@ -0,0 +1,48 @@
+import React from 'react';
+import { AlertTriangle, X } from 'lucide-react';
+
+export default function Modal({ isOpen, title, content, confirmText = '确定', cancelText = '取消', isDanger = false, onConfirm, onCancel }) {
+ if (!isOpen) return null;
+
+ return (
+
+
+
+
+
+
+
+
+
+
+ );
+}
+
diff --git a/h3-console/frontend/src/components/Sidebar.jsx b/h3-console/frontend/src/components/Sidebar.jsx
new file mode 100644
index 0000000..e070a17
--- /dev/null
+++ b/h3-console/frontend/src/components/Sidebar.jsx
@@ -0,0 +1,344 @@
+import React, { useState, useEffect } from 'react';
+import {
+ Plus, Search, ArrowUpDown, Trash2, KeyRound,
+ ChevronLeft, ChevronRight, Server, RefreshCw
+} from 'lucide-react';
+import JobCard from './JobCard';
+import { fetchJobs, prioritizeJob, deleteJob, restoreJob, resetGpu } from '../api';
+import { getToken, getH3Base } from '../utils';
+import { useToast } from './Toast';
+import Modal from './Modal';
+
+export default function Sidebar({
+ selectedId,
+ onSelectTask,
+ onCreateTask,
+ onRerunTask,
+ backendInfo,
+ onRefreshBackends,
+ onManageToken,
+}) {
+ const { addToast } = useToast();
+ const [jobs, setJobs] = useState([]);
+ const [totalJobs, setTotalJobs] = useState(0);
+ const [page, setPage] = useState(1);
+ const [searchQ, setSearchQ] = useState('');
+ const [statusFilter, setStatusFilter] = useState('');
+ const [kindFilter, setKindFilter] = useState('');
+ const [sortOrder, setSortOrder] = useState('desc');
+ const [viewingTrash, setViewingTrash] = useState(false);
+ const [trashJobs, setTrashJobs] = useState([]);
+ const [loading, setLoading] = useState(false);
+
+ // 模态弹窗状态
+ const [confirmModal, setConfirmModal] = useState({ isOpen: false, title: '', content: '', isDanger: false, onConfirm: null });
+
+ // 刷新任务列表
+ const loadJobs = async (targetPage = page) => {
+ setLoading(true);
+ try {
+ if (viewingTrash) {
+ const data = await fetchJobs({ trash: true });
+ setTrashJobs(data.jobs || []);
+ } else {
+ const data = await fetchJobs({
+ page: targetPage,
+ limit: 20,
+ q: searchQ.trim(),
+ status: statusFilter,
+ kind: kindFilter,
+ sort: sortOrder,
+ });
+ setJobs(data.jobs || []);
+ setTotalJobs(data.total || 0);
+ }
+ } catch (e) {
+ console.error(e);
+ } finally {
+ setLoading(false);
+ }
+ };
+
+ useEffect(() => {
+ loadJobs(1);
+ setPage(1);
+ }, [searchQ, statusFilter, kindFilter, sortOrder, viewingTrash]);
+
+ // 定时每 5 秒轮询主列表
+ useEffect(() => {
+ const timer = setInterval(() => {
+ if (!viewingTrash) loadJobs(page);
+ }, 5000);
+ return () => clearInterval(timer);
+ }, [page, searchQ, statusFilter, kindFilter, sortOrder, viewingTrash]);
+
+ const handlePrioritize = async (id) => {
+ try {
+ const res = await prioritizeJob(id);
+ addToast(`任务 #${id.substring(0, 8)} 已插队到第 ${res.queue_position} 位`, 'success');
+ loadJobs(page);
+ } catch (e) {
+ addToast('插队失败: ' + e.message, 'error');
+ }
+ };
+
+ const handleDelete = async (id) => {
+ try {
+ await deleteJob(id);
+ addToast('已将任务移入回收站', 'info');
+ if (selectedId === id) onCreateTask();
+ loadJobs(page);
+ } catch (e) {
+ addToast('删除失败: ' + e.message, 'error');
+ }
+ };
+
+ const handleRestore = async (id) => {
+ try {
+ await restoreJob(id);
+ addToast('任务已从回收站恢复', 'success');
+ loadJobs();
+ } catch (e) {
+ addToast('恢复失败: ' + e.message, 'error');
+ }
+ };
+
+ const handleResetGpu = () => {
+ setConfirmModal({
+ isOpen: true,
+ title: '重置整个服务的 GPU 显存',
+ content: '确定要触发硬重启?\n这会打断当前正在执行的任务(如果有),并重启后端的 Ray 与推理进程以回收显存泄露。\n大约需要耗时 15-20 秒,重置完成前无法提交新任务。',
+ isDanger: true,
+ onConfirm: async () => {
+ setConfirmModal((prev) => ({ ...prev, isOpen: false }));
+ try {
+ await resetGpu();
+ addToast('已触发 GPU 重启,正在回收显存…', 'info');
+ if (onRefreshBackends) onRefreshBackends();
+ } catch (e) {
+ addToast('重置失败: ' + e.message, 'error');
+ }
+ },
+ });
+ };
+
+ const totalPages = Math.max(1, Math.ceil(totalJobs / 20));
+
+ return (
+
+ );
+}
diff --git a/h3-console/frontend/src/components/TaskDetail.jsx b/h3-console/frontend/src/components/TaskDetail.jsx
new file mode 100644
index 0000000..a9117e7
--- /dev/null
+++ b/h3-console/frontend/src/components/TaskDetail.jsx
@@ -0,0 +1,483 @@
+import React, { useState, useEffect } from 'react';
+import {
+ ArrowLeft, Download, RotateCcw, ArrowUpToLine, StopCircle, XCircle,
+ Copy, Check, Code, Cpu, Clock, Layers, ShieldAlert, Sparkles, Activity
+} from 'lucide-react';
+import { fetchJobDetail, prioritizeJob, cancelJob, fetchComfyHistory } from '../api';
+import { formatDateTime, formatBytes, thumbUrl, localAsset, h3FileUrl, getH3Base, getJobKind } from '../utils';
+import { useToast } from './Toast';
+import Modal from './Modal';
+
+export default function TaskDetail({ taskId, onBack, onRerun }) {
+ const { addToast } = useToast();
+ const [job, setJob] = useState(null);
+ const [loading, setLoading] = useState(true);
+ const [copiedPrompt, setCopiedPrompt] = useState(false);
+ const [comfyHistory, setComfyHistory] = useState(null);
+ const [loadingHistory, setLoadingHistory] = useState(false);
+ const [showHistory, setShowHistory] = useState(false);
+ const [showRawReq, setShowRawReq] = useState(false);
+
+ // 模态弹窗状态
+ const [confirmModal, setConfirmModal] = useState({ isOpen: false, title: '', content: '', isDanger: false, onConfirm: null });
+
+ const loadDetail = async () => {
+ try {
+ const data = await fetchJobDetail(taskId);
+ setJob(data);
+ } catch (e) {
+ console.error(e);
+ } finally {
+ setLoading(false);
+ }
+ };
+
+ useEffect(() => {
+ loadDetail();
+ // 轮询:排队或运行中每 2.5 秒刷新一次
+ const timer = setInterval(() => {
+ if (job && (job.status === 'queued' || job.status === 'running')) {
+ loadDetail();
+ }
+ }, 2500);
+ return () => clearInterval(timer);
+ }, [taskId, job?.status]);
+
+ if (loading && !job) {
+ return (
+
+ );
+ }
+
+ if (!job) {
+ return (
+
+
未找到该任务或已被彻底删除
+
+
+ );
+ }
+
+ const req = job.req || {};
+ const isDone = job.status === 'done';
+ const isRunning = job.status === 'running';
+ const isQueued = job.status === 'queued';
+ const isFailed = job.status === 'failed';
+ const isCancelled = job.status === 'cancelled';
+
+ // 进度精细计算
+ const curStep = job.step || 0;
+ const totalSteps = job.total_steps || req.steps || 20;
+ const progressPercent = totalSteps > 0
+ ? Math.min(100, Math.max(curStep > 0 ? 5 : 2, Math.round((curStep / totalSteps) * 100)))
+ : 0;
+
+ // 预估剩余时间 (秒)
+ const elapsed = job.elapsed_seconds || 0;
+ let estimatedRemaining = null;
+ if (curStep > 0 && totalSteps > curStep) {
+ const secPerStep = elapsed / curStep;
+ const remainSec = Math.round(secPerStep * (totalSteps - curStep));
+ estimatedRemaining = remainSec > 60 ? `约 ${Math.round(remainSec / 60)} 分钟` : `约 ${remainSec} 秒`;
+ }
+
+ const videoSrc = isDone ? h3FileUrl(`${getH3Base()}/v1/videos/${job.id}/file`) : null;
+
+ const handleCopyPrompt = () => {
+ if (!req.prompt) return;
+ navigator.clipboard.writeText(req.prompt);
+ setCopiedPrompt(true);
+ addToast('已复制 Prompt 提示词到剪贴板', 'success');
+ setTimeout(() => setCopiedPrompt(false), 2000);
+ };
+
+ const handlePrioritize = async () => {
+ try {
+ const res = await prioritizeJob(job.id);
+ addToast(`已成功插队至第 ${res.queue_position} 位`, 'success');
+ loadDetail();
+ } catch (e) {
+ addToast('插队失败: ' + e.message, 'error');
+ }
+ };
+
+ const handleCancel = (running) => {
+ setConfirmModal({
+ isOpen: true,
+ title: running ? '停止正在运行的任务' : '取消排队任务',
+ content: running
+ ? '确定停止当前正在运行的任务?\n这会硬重启后台 Raylight 服务以释放 GPU 显存(约耗时 15-20 秒),重启完成前将暂时无法提交新任务。'
+ : '确定取消这条排队中的任务?',
+ isDanger: running,
+ onConfirm: async () => {
+ setConfirmModal((prev) => ({ ...prev, isOpen: false }));
+ try {
+ await cancelJob(job.id);
+ addToast(running ? '已发出停止与 GPU 重置指令' : '已取消任务', 'info');
+ loadDetail();
+ } catch (e) {
+ addToast('操作失败: ' + e.message, 'error');
+ }
+ },
+ });
+ };
+
+ const handleToggleHistory = async () => {
+ if (!showHistory && !comfyHistory) {
+ setLoadingHistory(true);
+ try {
+ const hist = await fetchComfyHistory(job.id);
+ setComfyHistory(hist);
+ } catch (e) {
+ addToast('获取 ComfyUI History 失败: ' + e.message, 'error');
+ } finally {
+ setLoadingHistory(false);
+ }
+ }
+ setShowHistory(!showHistory);
+ };
+
+ return (
+
+ {/* 顶栏导航与快捷动作 */}
+
+
+
+
+
+
#{job.id}
+ {/* 状态标识 */}
+ {isDone && 已完成}
+ {isRunning && (
+
+
+ 正在采样 · {progressPercent}%
+
+ )}
+ {isQueued && (
+
+ 排队中 {job.queue_position ? `(第 ${job.queue_position} 位)` : ''}
+
+ )}
+ {isFailed && 执行失败}
+ {isCancelled && 已取消}
+
+
创建于 {formatDateTime(job.created_at)}
+
+
+
+ {/* 顶部操作条 */}
+
+
+
+ {isQueued && (
+ <>
+
+
+ >
+ )}
+
+ {isRunning && (
+
+ )}
+
+
+
+ {/* 核心成果展示区 / 实时可视化进度看板 */}
+
+ {isDone && videoSrc ? (
+
+ ) : isRunning ? (
+ /* 🔥 极度直观的全新多维进度看板 */
+
+
+
+ {/* 大数字百分比 */}
+
+
+ {progressPercent}
+ %
+
+
+ 正在执行第 {curStep} 步 / 共 {totalSteps} 步
+
+
+
+ {/* 真实横向流光进度条 */}
+
+
+
+ {/* 进度条两端时间指示 */}
+
+
+
+ 已耗时: {elapsed} 秒
+
+ {estimatedRemaining && (
+
+ 预计还需: {estimatedRemaining}
+
+ )}
+
+
+
+ {/* 4 阶段步骤状态指示链 */}
+
+
+ ) : isQueued ? (
+
+
+
+
正在等待空闲的 {job.partition || 'GPU'} 后端…
+
当前排队第 {job.queue_position || 1} 位,空闲节点就绪后将自动调度启动
+
+
+ ) : (
+
+
+
{isFailed ? '生成失败' : '任务已取消'}
+
+ {job.error || '无详细错误日志'}
+
+
+ )}
+
+
+ {/* 仪表盘指标卡片网格 */}
+
+
+
+
+ 生成耗时
+
+
+ {job.started_at && job.finished_at ? `${(job.finished_at - job.started_at).toFixed(1)} 秒` : isRunning ? `${job.elapsed_seconds || 0} 秒 (进行中)` : '—'}
+
+
+
+
+
+
+ 峰值显存
+
+
+ {job.peak_vram_gb ? `${job.peak_vram_gb} GB` : '—'}
+
+
+
+
+
+
+ 分配后端卡
+
+
+ {job.backend || '自动弹性调度'}
+
+
+
+
+
+
+ 采样规格
+
+
+ {req.turbo === false ? '常规' : 'Turbo'} · {req.steps ?? '?'}步 · seed={req.seed ?? 0}
+
+
+
+
+ {/* Prompt 提示词卡片 */}
+
+
+ Prompt 提示词
+
+
+
+ {req.prompt || '(无提示词)'}
+
+ {req.negative_prompt && (
+
+ 负面提示词:
+ {req.negative_prompt}
+
+ )}
+
+
+ {/* 输入素材回显 (首尾帧 / 参考媒体) */}
+ {(req.first_frame_url || req.last_frame_url || (req.reference_image_urls || []).length > 0 || (req.reference_video_urls || []).length > 0 || (req.reference_audio_urls || []).length > 0) && (
+
+
输入素材与参考源
+
+ {req.first_frame_url && (
+
+
})
+
首帧
+
+ )}
+ {req.last_frame_url && (
+
+
})
+
尾帧
+
+ )}
+ {(req.reference_image_urls || []).map((u, i) => (
+
+
})
+
参考图 {i+1}
+
+ ))}
+ {(req.reference_video_urls || []).map((u, i) => (
+
+ ))}
+
+
+ )}
+
+ {/* 底部调试折叠抽屉 */}
+
+ {/* 原始请求 JSON */}
+
+
+ {showRawReq && (
+
+ {JSON.stringify(req, null, 2)}
+
+ )}
+
+
+ {/* ComfyUI History */}
+ {job.comfy_prompt_id && (
+
+
+ {showHistory && (
+
+ {loadingHistory ? '正在拉取 ComfyUI History…' : comfyHistory ? JSON.stringify(comfyHistory, null, 2) : '无可用数据'}
+
+ )}
+
+ )}
+
+
+ {/* 模态弹窗 */}
+
setConfirmModal((prev) => ({ ...prev, isOpen: false }))}
+ />
+
+ );
+}
diff --git a/h3-console/frontend/src/components/Toast.jsx b/h3-console/frontend/src/components/Toast.jsx
new file mode 100644
index 0000000..78102ff
--- /dev/null
+++ b/h3-console/frontend/src/components/Toast.jsx
@@ -0,0 +1,60 @@
+import React, { createContext, useContext, useState, useCallback } from 'react';
+import { CheckCircle2, AlertCircle, Info, X } from 'lucide-react';
+
+const ToastContext = createContext(null);
+
+export function ToastProvider({ children }) {
+ const [toasts, setToasts] = useState([]);
+
+ const addToast = useCallback((msg, type = 'info', duration = 3000) => {
+ const id = Date.now() + Math.random().toString(36).substring(2, 6);
+ setToasts((prev) => [...prev, { id, msg, type }]);
+ if (duration > 0) {
+ setTimeout(() => {
+ setToasts((prev) => prev.filter((t) => t.id !== id));
+ }, duration);
+ }
+ }, []);
+
+ const removeToast = useCallback((id) => {
+ setToasts((prev) => prev.filter((t) => t.id !== id));
+ }, []);
+
+ return (
+
+ {children}
+
+ {toasts.map((t) => (
+
+ {t.type === 'success' &&
}
+ {t.type === 'error' &&
}
+ {t.type === 'info' &&
}
+
{t.msg}
+
+
+ ))}
+
+
+ );
+}
+
+export function useToast() {
+ const ctx = useContext(ToastContext);
+ if (!ctx) throw new Error('useToast must be used within ToastProvider');
+ return ctx;
+}
+
diff --git a/h3-console/frontend/src/constants.js b/h3-console/frontend/src/constants.js
new file mode 100644
index 0000000..f9cd57e
--- /dev/null
+++ b/h3-console/frontend/src/constants.js
@@ -0,0 +1,45 @@
+export const RESOLUTIONS = [
+ { value: '1344x768', width: 1344, height: 768, label: '1344×768 (横屏 · 官方默认)', maxFrames: 412, isLandscape: true, aspect: '16:9' },
+ { value: '768x1344', width: 768, height: 1344, label: '768×1344 (竖屏 · 官方默认)', maxFrames: 412, isLandscape: false, aspect: '9:16' },
+ { value: '832x480', width: 832, height: 480, label: '832×480 (横屏 · 标准)', maxFrames: 1125, isLandscape: true, aspect: '16:9' },
+ { value: '480x832', width: 480, height: 832, label: '480×832 (竖屏 · 推荐短剧)', maxFrames: 1125, isLandscape: false, aspect: '9:16' },
+ { value: '1024x576', width: 1024, height: 576, label: '1024×576 (横屏 · 高清)', maxFrames: 743, isLandscape: true, aspect: '16:9' },
+ { value: '576x1024', width: 576, height: 1024, label: '576×1024 (竖屏 · 高清)', maxFrames: 743, isLandscape: false, aspect: '9:16' },
+ { value: '448x256', width: 448, height: 256, label: '448×256 (横屏 · 超长)', maxFrames: 1200, isLandscape: true, aspect: '16:9' },
+ { value: '256x448', width: 256, height: 448, label: '256×448 (竖屏 · 超长)', maxFrames: 1200, isLandscape: false, aspect: '9:16' },
+];
+
+export const DEFAULT_RES = '480x832';
+export const DEFAULT_DUR = '243'; // 10s @ 24fps
+
+export const DURATIONS = [
+ { value: '124', frames: 124, label: '5秒 (124帧)' },
+ { value: '243', frames: 243, label: '10秒 (243帧 · 默认)' },
+ { value: '362', frames: 362, label: '15秒 (362帧)' },
+ { value: '412', frames: 412, label: '17秒 (412帧 · 1344上限)' },
+ { value: '600', frames: 600, label: '25秒 (600帧)' },
+ { value: '743', frames: 743, label: '31秒 (743帧 · 1024上限)' },
+ { value: '900', frames: 900, label: '37.5秒 (900帧)' },
+ { value: '1125', frames: 1125, label: '47秒 (1125帧 · 832上限)' },
+ { value: '1200', frames: 1200, label: '50秒 (1200帧 · 256画质上限)' },
+];
+
+export const STEPS_BY_TURBO = {
+ true: [
+ { value: '4', label: '4 步 (极速推荐 · LoRA 蒸馏)' },
+ { value: '8', label: '8 步 (上限)' },
+ ],
+ false: [
+ { value: '20', label: '20 步 (平衡标准)' },
+ { value: '50', label: '50 步 (全质量 · 约9.6倍耗时)' },
+ ],
+};
+
+export const SEED_OPTIONS = [
+ { value: '0', label: '0 (默认固定)' },
+ { value: '42', label: '42' },
+ { value: '123', label: '123' },
+ { value: '2024', label: '2024' },
+ { value: 'random', label: '🎲 随机 Seed' },
+];
+
diff --git a/h3-console/frontend/src/index.css b/h3-console/frontend/src/index.css
new file mode 100644
index 0000000..f1d3537
--- /dev/null
+++ b/h3-console/frontend/src/index.css
@@ -0,0 +1,51 @@
+@import "tailwindcss";
+
+:root {
+ /* 全局浅色主题变量 - 干净清爽 */
+ --bg-app: #f8fafc; /* 页面底色 */
+ --bg-surface: #ffffff; /* 卡片/侧边栏底色 */
+ --bg-subtle: #f1f5f9; /* 输入框/微浅底色 */
+ --border-color: #e2e8f0; /* 边框色 */
+ --text-main: #0f172a; /* 主标题/正文字色 */
+ --text-muted: #64748b; /* 辅助文字色 */
+ --primary: #059669; /* 品牌翡翠绿 */
+}
+
+.dark {
+ /* 备用暗色主题变量 */
+ --bg-app: #090a0f;
+ --bg-surface: #13151c;
+ --bg-subtle: #1c1f2b;
+ --border-color: #272c3d;
+ --text-main: #f8fafc;
+ --text-muted: #94a3b8;
+ --primary: #10b981;
+}
+
+body {
+ background-color: var(--bg-app);
+ color: var(--text-main);
+ font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "PingFang SC", sans-serif;
+ margin: 0;
+ padding: 0;
+ overflow: hidden;
+ height: 100vh;
+}
+
+/* 统一控制所有卡片、边框、输入框 */
+.theme-surface {
+ background-color: var(--bg-surface);
+ border-color: var(--border-color);
+ color: var(--text-main);
+}
+.theme-subtle {
+ background-color: var(--bg-subtle);
+ border-color: var(--border-color);
+ color: var(--text-main);
+}
+.theme-border {
+ border-color: var(--border-color);
+}
+.theme-text-muted {
+ color: var(--text-muted);
+}
diff --git a/h3-console/frontend/src/main.jsx b/h3-console/frontend/src/main.jsx
new file mode 100644
index 0000000..6d961d2
--- /dev/null
+++ b/h3-console/frontend/src/main.jsx
@@ -0,0 +1,11 @@
+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/h3-console/frontend/src/utils.js b/h3-console/frontend/src/utils.js
new file mode 100644
index 0000000..4aed4de
--- /dev/null
+++ b/h3-console/frontend/src/utils.js
@@ -0,0 +1,91 @@
+export function getH3Base() {
+ if (typeof window !== 'undefined' && window.__H3_BASE__ !== undefined) {
+ return window.__H3_BASE__;
+ }
+ return '';
+}
+
+export function getToken() {
+ if (typeof window === 'undefined') return '';
+ return localStorage.getItem('h3_console_token') || '';
+}
+
+export function setToken(token) {
+ if (typeof window === 'undefined') return;
+ if (!token) {
+ localStorage.removeItem('h3_console_token');
+ } else {
+ localStorage.setItem('h3_console_token', token.trim());
+ }
+}
+
+export function formatTimeAgo(ts) {
+ if (!ts) return '—';
+ const now = Date.now() / 1000;
+ const diff = Math.max(0, Math.round(now - ts));
+ if (diff < 60) return `${diff}秒前`;
+ if (diff < 3600) return `${Math.round(diff / 60)}分钟前`;
+ if (diff < 86400) return `${Math.round(diff / 3600)}小时前`;
+ return `${Math.round(diff / 86400)}天前`;
+}
+
+export function formatDateTime(ts) {
+ if (!ts) return '—';
+ return new Date(ts * 1000).toLocaleString('zh-CN', {
+ hour12: false,
+ year: 'numeric',
+ month: '2-digit',
+ day: '2-digit',
+ hour: '2-digit',
+ minute: '2-digit',
+ second: '2-digit',
+ });
+}
+
+export function formatBytes(bytes) {
+ if (!bytes || bytes <= 0) return '0 B';
+ const units = ['B', 'KB', 'MB', 'GB'];
+ const i = Math.floor(Math.log(bytes) / Math.log(1024));
+ return `${(bytes / Math.pow(1024, i)).toFixed(1)} ${units[i]}`;
+}
+
+export function filenameFromUrl(u) {
+ if (!u) return '';
+ try {
+ const base = decodeURIComponent(u.split('/').pop().split('?')[0]);
+ const m = base.match(/^[0-9a-f]{16}__(.+)$/);
+ return m ? m[1] : base;
+ } catch (e) {
+ return u;
+ }
+}
+
+// 上传返回的 URL 是写死内网 IP 的绝对地址(H3_CONSOLE_PUBLIC_BASE,raylight 要拉取,存库的值不能改),
+// 别的网络打开页面时浏览器连不上。展示时把本机上传的文件({16位hex}__ 前缀)改成当前页面同源路径;
+// 外部图床 URL 原样返回。绝不改 req 里存的 URL。
+export function localAsset(u) {
+ const m = u && u.match(/\/assets\/([0-9a-f]{16}__[^/?#]*)$/);
+ return m ? getH3Base() + '/assets/' + m[1] : u;
+}
+
+export function thumbUrl(u) {
+ if (!u) return u;
+ u = localAsset(u);
+ const i = u.indexOf('/assets/');
+ if (i === -1) return u;
+ return u.slice(0, i) + '/thumbs/' + u.slice(i + '/assets/'.length);
+}
+
+export function h3FileUrl(path) {
+ if (!path) return '';
+ const token = getToken();
+ return token ? `${path}${path.includes('?') ? '&' : '?'}token=${encodeURIComponent(token)}` : path;
+}
+
+export function getJobKind(req = {}) {
+ if (req.first_frame_url || req.last_frame_url) return { key: 'fl2va', label: '首尾帧' };
+ if ((req.reference_image_urls || []).length || (req.reference_video_urls || []).length || (req.reference_audio_urls || []).length) {
+ return { key: 'ref2va', label: '参考图' };
+ }
+ return { key: 'text', label: '文生' };
+}
diff --git a/h3-console/frontend/vite.config.js b/h3-console/frontend/vite.config.js
new file mode 100644
index 0000000..6b23bac
--- /dev/null
+++ b/h3-console/frontend/vite.config.js
@@ -0,0 +1,19 @@
+import { defineConfig } from 'vite'
+import react from '@vitejs/plugin-react'
+import tailwindcss from '@tailwindcss/vite'
+import { viteSingleFile } from 'vite-plugin-singlefile'
+
+export default defineConfig({
+ plugins: [
+ react(),
+ tailwindcss(),
+ viteSingleFile(),
+ ],
+ build: {
+ target: 'esnext',
+ assetsInlineLimit: 100000000,
+ chunkSizeWarningLimit: 100000000,
+ cssCodeSplit: false,
+ },
+})
+
diff --git a/h3-console/server.py b/h3-console/server.py
index 392664e..0d14ebd 100644
--- a/h3-console/server.py
+++ b/h3-console/server.py
@@ -17,7 +17,7 @@ from typing import Optional
import hmac
import httpx
from fastapi import APIRouter, Depends, FastAPI, HTTPException, UploadFile, File, Request
-from fastapi.responses import StreamingResponse, FileResponse, HTMLResponse
+from fastapi.responses import StreamingResponse, FileResponse, HTMLResponse, Response
from fastapi.staticfiles import StaticFiles
from PIL import Image
@@ -390,6 +390,17 @@ async def _discover_from_gateway():
)
+@api.post("/v1/videos/validate")
+async def validate_video_request(payload: dict):
+ try:
+ response = await client.post(f"{GATEWAY_URL}/v1/videos/validate", json=payload, timeout=180.0)
+ except httpx.HTTPError:
+ raise HTTPException(503, "generation validator unavailable")
+ if response.status_code != 200:
+ raise HTTPException(response.status_code, response.json().get("detail", "request validation failed"))
+ return response.json()
+
+
@api.post("/v1/videos", status_code=202)
async def create_video(req: dict):
_check_not_resetting()
@@ -752,8 +763,10 @@ async def models():
# 不用两边手动同步一份数字。
try:
resp = await client.get(f"{GATEWAY_URL}/v1/resolutions")
- resolutions = resp.json()["resolutions"] if resp.status_code == 200 else []
+ capabilities = resp.json() if resp.status_code == 200 else {}
+ resolutions = capabilities.get("resolutions", [])
except httpx.HTTPError:
+ capabilities = {}
resolutions = []
presets = [
{"label": f"{r['width']}×{r['height']},最多 {r['max_frames']} 帧(≈{r['max_frames']/24:.1f}s@24fps)",
@@ -768,6 +781,8 @@ async def models():
"desc": "文生视频 / 多参考图生成"},
},
"presets": presets,
+ "request_schema": capabilities.get("request_schema"),
+ "request_validation": capabilities.get("request_validation"),
}
@@ -916,6 +931,22 @@ async def index_page():
return render_index()
+# 给接入方看的文档:API.md 是完整接口说明,skill.md 是可直接喂给 agent 的精简技能。
+# 不校验 token(跟页面一样),内容里不含 token;每次请求现读文件,改文档不用重启。
+def _md(name: str) -> Response:
+ return Response((BASE / name).read_text(encoding="utf-8"), media_type="text/markdown; charset=utf-8")
+
+
+@router.get("/docs/api.md")
+async def doc_api():
+ return _md("API.md")
+
+
+@router.get("/skill.md")
+async def doc_skill():
+ return _md("skill.md")
+
+
@router.get("/task.html/{tid}")
async def task_page(tid: str):
return render_index()
diff --git a/h3-console/skill.md b/h3-console/skill.md
new file mode 100644
index 0000000..7cbe0e2
--- /dev/null
+++ b/h3-console/skill.md
@@ -0,0 +1,52 @@
+---
+name: h3-video
+description: 用 MiniMax H3 生成视频(文生 / 首尾帧 / 参考图),通过 H3 Console 的 HTTP API 提交任务并取回结果。Use when:用户要生成视频、把想法/参考图写成 H3 提示词并提交。
+---
+
+# H3 视频生成
+
+完整接口文档:同域名下的 `/docs/api.md`(公网:`http://qwen.wudaima.com/console/docs/api.md`)。
+**只打 console 的 `/v1/*`,不要直连底层 raylight 端口。**
+
+- 公网 Base URL:`http://qwen.wudaima.com/console`(所有路径带 `/console` 前缀)
+- 局域网 Base URL:`http://192.168.110.67:8080`
+- 鉴权:`Authorization: Bearer `,token 向服务器管理员索取,**不要写进提示词或日志**。
+
+## 流程
+
+1. **定模式**(按传的字段自动判断):只有文字 → 文生;有首/尾帧图 → fl2va(`first_frame_url`/`last_frame_url`);有人物/场景/风格参考图、参考视频、参考音频 → ref2va(`reference_*_urls`)。
+2. **写提示词**,见下节。
+3. **给用户看草稿并等确认**。一条 1344×768、10–15s 的视频要跑 30–55 分钟,别未经确认就提交。
+4. 有本地素材先上传:`curl -H "Authorization: Bearer $T" -F file=@a.jpg $BASE/v1/assets` → 返回 `url`。
+5. 提交:
+ ```bash
+ curl -X POST $BASE/v1/videos -H "Authorization: Bearer $T" -H "Content-Type: application/json" \
+ -d '{"prompt":"…","width":1344,"height":768,"frames":300,"reference_image_urls":[""]}'
+ ```
+ **永远返回 HTTP 202,必须检查响应体 `status`**:`"failed"` 表示校验没过,原因在 `error`。
+6. 轮询 `GET $BASE/v1/videos/{id}`,状态 `queued → running → done/failed/cancelled`;完成后 `GET $BASE/v1/videos/{id}/file` 取 mp4。
+ 给用户的详情页链接:`$BASE/task.html/{id}`。
+
+## 参数要点
+
+- `frames` 为 24fps 帧数,秒数 = frames / 24。默认建议 1344×768、300 帧(≈12.5s)。
+- 各分辨率帧数上限以 `GET /v1/models` 为准(1344×768 目前 412 帧)。超限直接判失败。
+- 4 张卡是一个集群,**同一时刻只跑 1 条任务**,其余排队;多提交不会更快。
+- `turbo` / `cfg_scale` / `negative_prompt` 当前不生效。
+
+## 提示词写法
+
+```
+完整提示词 = 参考素材说明 + 核心创意 + 画面过程说明
+```
+
+- **参考素材说明**(没素材就跳过):用 `@图片1` / `@视频1` / `@音频1` 编号并写清用途(人物参考、场景参考、风格参考、首帧……),编号顺序与实际传入顺序一致。
+- **核心创意**:一句话写清主体 + 地点 + 事件 + 风格 + 运镜。运镜写具体(如 `truck left + pan right`),不要自相矛盾(有推近就别写"固定机位")。
+- **画面过程**:按时间轴/shot 分段,每段写景别、内容、运镜、动作、台词、声音。
+- **时长一致**:提示词里的总时长和分镜时间戳必须与提交的 `frames / 24` 一致。
+- **每个 shot 都带声音**:画内环境声/拟音 + 配乐,不要把声音全堆在结尾一句。
+- **材质与微动作**:画面里可见的主要元素都给材质词;静态 shot 也要有光影/蒸汽/织物等细微动态。
+- **画面里要出现的文字/Logo** 必须逐字写出。
+- 不需要人物说话时,加"人物保持安静、嘴巴自然闭合",避免模型自动念白。
+- 比喻改成具体可视描述;不要写"不要字幕"(负向词对字幕无效甚至反效果);避免"亮度分区"一类词,只描述具体光源照到哪里。
+- 想要角色一致,务必传人物参考图。
diff --git a/h3-console/static/index.html b/h3-console/static/index.html
index 87a2752..c4522a0 100644
--- a/h3-console/static/index.html
+++ b/h3-console/static/index.html
@@ -1,1273 +1,312 @@
-
-
-H3 Console
-
-
-
-
-
-
-
-
-
-
+
+
+
+ H3 Console - AI Video Studio
+
+
+
+
+
+
+