Compare commits

..

11 Commits

Author SHA1 Message Date
ludan 324ebf5ce5 feat: 优化首页入口按钮质感,提升商业产品精致度
- 按钮背景改为微渐变+毛玻璃效果(backdrop-filter)
- 边框改为半透明白色,圆角从8px升级到12px
- 增加内高光+外层深度阴影提升层次感
- 字间距、字重大小幅调整,更精致克制
- hover态增加accent光晕+图标变绿+放大效果
- 主按钮增加渐变绿底+内高光+绿色辉光阴影
- 增加按压态scale(0.97)反馈
- 主按钮图标hover放大1.12倍
2026-06-02 19:09:00 +08:00
ludan 9ababfda46 fix: 修复剧本评分文本框随内容无限下拉导致按钮被挤出视口
- textarea 增加 max-height 限制高度不随内容增长
- textarea 增加 overflow-y: auto 启用内部滚动
- text-shell 同步增加 max-height 约束
2026-06-02 18:18:00 +08:00
stringadmin 94080f30f7 Merge pull request 'feat: 升级剧本评分系统为 DeepSeek V4 多维评分体系' (#4) from feat/script-eval-deepseek-v4 into master
Reviewed-on: #4
2026-06-02 09:03:11 +00:00
stringadmin 7d446dfc5f Merge branch 'master' into feat/script-eval-deepseek-v4 2026-06-02 09:02:46 +00:00
stringadmin d71437b09c Merge pull request 'Fix/ecommerce 502 bug' (#3) from fix/ecommerce-502-bug into master
Reviewed-on: #3
2026-06-02 09:01:38 +00:00
stringadmin f1bfbf8608 Merge branch 'master' into fix/ecommerce-502-bug 2026-06-02 09:01:31 +00:00
stringadmin 94c1453c9b fix: upload-binary Content-Type fix, 429/timeout retry, 120s timeout - Remove Content-Type: application/json from uploadAssetBinary FormData request - Add retryOnTransient for 429 + timeout + signal timed out errors - Increase AI chat timeout from 60s to 120s per call - Apply retry logic to both chat() and visionChat() 2026-06-02 16:58:59 +08:00
stringadmin 9504f8ee87 fix(ecommerce): replace base64 upload with binary blob in video service
runVideoPlan was passing blob URLs as "dataUrl" to uploadAssetWithProgress,
which sent them to /api/oss/upload (base64 path). Blob URLs don't match
DATA_URL_PATTERN regex, causing corrupt 44-byte files on OSS.

Now uses uploadAssetBinary (FormData multipart) via /api/oss/upload-binary,
fetching blob → uploading binary directly, same as EcommercePage path.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-02 16:16:09 +08:00
ludan d1b5d64bc8 feat: 升级剧本评分系统为 DeepSeek V4 多维评分体系
变更内容:
- scriptEvalClient: 替换系统提示词为 DeepSeek V4 完整版(六维评分细则、
  评分类别、评分铁律、等级标准),维度满分调整为 hook 20/plot 20/
  character 18/dialogue 15/visual 15/content 12,总分算法改为直接累加,
  模型使用 qwen3.7-max,增加请求链路 console 日志
- ScriptTokensPage: 同步维度满分和描述(角色塑造 18、内容深度 12),
  增加页面挂载和评测流程的 console 日志
- vite.config: esbuild.drop 移除 "console",保留 "debugger",
  确保开发环境 console 日志正常输出
2026-06-02 16:04:26 +08:00
stringadmin 44c748b0dc feat(ecommerce): use FormData binary upload instead of base64 dataUrl
- Add uploadAssetBinary method to aiGenerationClient (FormData + busboy)
- Replace base64 dataUrl upload in uploadProductImages with direct blob upload
  via /oss/upload-binary multipart endpoint
- This eliminates the DATA_URL_PATTERN regex parsing bug that produced
  44-byte corrupt files on OSS, causing DashScope "image format illegal" errors

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-02 16:03:50 +08:00
stringadmin 160552b45e fix(ecommerce): 502 bug - vision model upgrade + MIME normalization + fallback
- Upgrade VISION_MODEL to qwen3.7-plus (latest, confirmed working with image_url)
- Add VISION_FALLBACK_MODEL = qwen-vl-plus for retry on "image format" errors
- Normalize upload MIME types: unsupported formats (HEIC/AVIF) fall back to image/png
  to prevent server saving as .bin which DashScope can't read
- Server-side: add image/avif, image/heic, image/heif to MIME_EXTENSIONS

Root cause: DashScope returned "image format is illegal" when uploaded images
had unrecognized MIME types → saved as .bin → DashScope couldn't decode.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-02 15:20:23 +08:00
24 changed files with 760 additions and 3615 deletions
+169 -158
View File
@@ -8,20 +8,20 @@
"name": "omniai-web-preview",
"version": "0.1.0",
"dependencies": {
"@ant-design/icons": "5.3.0",
"@xyflow/react": "12.10.2",
"react": "18.2.0",
"react-dom": "18.2.0",
"zustand": "5.0.13"
"@ant-design/icons": "^5.3.0",
"@xyflow/react": "^12.10.2",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"zustand": "^5.0.13"
},
"devDependencies": {
"@types/react": "18.2.0",
"@types/react-dom": "18.2.0",
"@vitejs/plugin-react": "4.2.1",
"playwright": "1.60.0",
"sharp": "0.34.5",
"typescript": "5.3.3",
"vite": "5.1.0",
"@types/react": "^18.2.0",
"@types/react-dom": "^18.2.0",
"@vitejs/plugin-react": "^4.2.1",
"playwright": "^1.60.0",
"sharp": "^0.34.5",
"typescript": "^5.3.3",
"vite": "^5.1.0",
"vite-plugin-compression2": "2.5.3"
}
},
@@ -47,14 +47,14 @@
}
},
"node_modules/@ant-design/icons": {
"version": "5.3.0",
"resolved": "https://registry.npmjs.org/@ant-design/icons/-/icons-5.3.0.tgz",
"integrity": "sha512-69FgBsIkeCjw72ZU3fJpqjhmLCPrzKGEllbrAZK7MUdt1BrKsyG6A8YDCBPKea27UQ0tRXi33PcjR4tp/tEXMg==",
"version": "5.6.1",
"resolved": "https://registry.npmmirror.com/@ant-design/icons/-/icons-5.6.1.tgz",
"integrity": "sha512-0/xS39c91WjPAZOWsvi1//zjx6kAp4kxWwctR6kuU6p133w8RU0D2dSCvZC19uQyharg/sAvYxGYWl01BbZZfg==",
"license": "MIT",
"dependencies": {
"@ant-design/colors": "^7.0.0",
"@ant-design/icons-svg": "^4.4.0",
"@babel/runtime": "^7.11.2",
"@babel/runtime": "^7.24.8",
"classnames": "^2.2.6",
"rc-util": "^5.31.1"
},
@@ -103,6 +103,7 @@
"integrity": "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"@babel/code-frame": "^7.29.0",
"@babel/generator": "^7.29.0",
@@ -375,9 +376,9 @@
}
},
"node_modules/@esbuild/aix-ppc64": {
"version": "0.19.12",
"resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.19.12.tgz",
"integrity": "sha512-bmoCYyWdEL3wDQIVbcyzRyeKLgk2WtWLTWz1ZIAZF/EGbNOwSA6ew3PftJ1PqMiOOGu0OyFMzG53L0zqIpPeNA==",
"version": "0.21.5",
"resolved": "https://registry.npmmirror.com/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz",
"integrity": "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==",
"cpu": [
"ppc64"
],
@@ -392,9 +393,9 @@
}
},
"node_modules/@esbuild/android-arm": {
"version": "0.19.12",
"resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.19.12.tgz",
"integrity": "sha512-qg/Lj1mu3CdQlDEEiWrlC4eaPZ1KztwGJ9B6J+/6G+/4ewxJg7gqj8eVYWvao1bXrqGiW2rsBZFSX3q2lcW05w==",
"version": "0.21.5",
"resolved": "https://registry.npmmirror.com/@esbuild/android-arm/-/android-arm-0.21.5.tgz",
"integrity": "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==",
"cpu": [
"arm"
],
@@ -409,9 +410,9 @@
}
},
"node_modules/@esbuild/android-arm64": {
"version": "0.19.12",
"resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.19.12.tgz",
"integrity": "sha512-P0UVNGIienjZv3f5zq0DP3Nt2IE/3plFzuaS96vihvD0Hd6H/q4WXUGpCxD/E8YrSXfNyRPbpTq+T8ZQioSuPA==",
"version": "0.21.5",
"resolved": "https://registry.npmmirror.com/@esbuild/android-arm64/-/android-arm64-0.21.5.tgz",
"integrity": "sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==",
"cpu": [
"arm64"
],
@@ -426,9 +427,9 @@
}
},
"node_modules/@esbuild/android-x64": {
"version": "0.19.12",
"resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.19.12.tgz",
"integrity": "sha512-3k7ZoUW6Q6YqhdhIaq/WZ7HwBpnFBlW905Fa4s4qWJyiNOgT1dOqDiVAQFwBH7gBRZr17gLrlFCRzF6jFh7Kew==",
"version": "0.21.5",
"resolved": "https://registry.npmmirror.com/@esbuild/android-x64/-/android-x64-0.21.5.tgz",
"integrity": "sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==",
"cpu": [
"x64"
],
@@ -443,9 +444,9 @@
}
},
"node_modules/@esbuild/darwin-arm64": {
"version": "0.19.12",
"resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.19.12.tgz",
"integrity": "sha512-B6IeSgZgtEzGC42jsI+YYu9Z3HKRxp8ZT3cqhvliEHovq8HSX2YX8lNocDn79gCKJXOSaEot9MVYky7AKjCs8g==",
"version": "0.21.5",
"resolved": "https://registry.npmmirror.com/@esbuild/darwin-arm64/-/darwin-arm64-0.21.5.tgz",
"integrity": "sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==",
"cpu": [
"arm64"
],
@@ -460,9 +461,9 @@
}
},
"node_modules/@esbuild/darwin-x64": {
"version": "0.19.12",
"resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.19.12.tgz",
"integrity": "sha512-hKoVkKzFiToTgn+41qGhsUJXFlIjxI/jSYeZf3ugemDYZldIXIxhvwN6erJGlX4t5h417iFuheZ7l+YVn05N3A==",
"version": "0.21.5",
"resolved": "https://registry.npmmirror.com/@esbuild/darwin-x64/-/darwin-x64-0.21.5.tgz",
"integrity": "sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==",
"cpu": [
"x64"
],
@@ -477,9 +478,9 @@
}
},
"node_modules/@esbuild/freebsd-arm64": {
"version": "0.19.12",
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.19.12.tgz",
"integrity": "sha512-4aRvFIXmwAcDBw9AueDQ2YnGmz5L6obe5kmPT8Vd+/+x/JMVKCgdcRwH6APrbpNXsPz+K653Qg8HB/oXvXVukA==",
"version": "0.21.5",
"resolved": "https://registry.npmmirror.com/@esbuild/freebsd-arm64/-/freebsd-arm64-0.21.5.tgz",
"integrity": "sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==",
"cpu": [
"arm64"
],
@@ -494,9 +495,9 @@
}
},
"node_modules/@esbuild/freebsd-x64": {
"version": "0.19.12",
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.19.12.tgz",
"integrity": "sha512-EYoXZ4d8xtBoVN7CEwWY2IN4ho76xjYXqSXMNccFSx2lgqOG/1TBPW0yPx1bJZk94qu3tX0fycJeeQsKovA8gg==",
"version": "0.21.5",
"resolved": "https://registry.npmmirror.com/@esbuild/freebsd-x64/-/freebsd-x64-0.21.5.tgz",
"integrity": "sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==",
"cpu": [
"x64"
],
@@ -511,9 +512,9 @@
}
},
"node_modules/@esbuild/linux-arm": {
"version": "0.19.12",
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.19.12.tgz",
"integrity": "sha512-J5jPms//KhSNv+LO1S1TX1UWp1ucM6N6XuL6ITdKWElCu8wXP72l9MM0zDTzzeikVyqFE6U8YAV9/tFyj0ti+w==",
"version": "0.21.5",
"resolved": "https://registry.npmmirror.com/@esbuild/linux-arm/-/linux-arm-0.21.5.tgz",
"integrity": "sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==",
"cpu": [
"arm"
],
@@ -528,9 +529,9 @@
}
},
"node_modules/@esbuild/linux-arm64": {
"version": "0.19.12",
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.19.12.tgz",
"integrity": "sha512-EoTjyYyLuVPfdPLsGVVVC8a0p1BFFvtpQDB/YLEhaXyf/5bczaGeN15QkR+O4S5LeJ92Tqotve7i1jn35qwvdA==",
"version": "0.21.5",
"resolved": "https://registry.npmmirror.com/@esbuild/linux-arm64/-/linux-arm64-0.21.5.tgz",
"integrity": "sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==",
"cpu": [
"arm64"
],
@@ -545,9 +546,9 @@
}
},
"node_modules/@esbuild/linux-ia32": {
"version": "0.19.12",
"resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.19.12.tgz",
"integrity": "sha512-Thsa42rrP1+UIGaWz47uydHSBOgTUnwBwNq59khgIwktK6x60Hivfbux9iNR0eHCHzOLjLMLfUMLCypBkZXMHA==",
"version": "0.21.5",
"resolved": "https://registry.npmmirror.com/@esbuild/linux-ia32/-/linux-ia32-0.21.5.tgz",
"integrity": "sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==",
"cpu": [
"ia32"
],
@@ -562,9 +563,9 @@
}
},
"node_modules/@esbuild/linux-loong64": {
"version": "0.19.12",
"resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.19.12.tgz",
"integrity": "sha512-LiXdXA0s3IqRRjm6rV6XaWATScKAXjI4R4LoDlvO7+yQqFdlr1Bax62sRwkVvRIrwXxvtYEHHI4dm50jAXkuAA==",
"version": "0.21.5",
"resolved": "https://registry.npmmirror.com/@esbuild/linux-loong64/-/linux-loong64-0.21.5.tgz",
"integrity": "sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==",
"cpu": [
"loong64"
],
@@ -579,9 +580,9 @@
}
},
"node_modules/@esbuild/linux-mips64el": {
"version": "0.19.12",
"resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.19.12.tgz",
"integrity": "sha512-fEnAuj5VGTanfJ07ff0gOA6IPsvrVHLVb6Lyd1g2/ed67oU1eFzL0r9WL7ZzscD+/N6i3dWumGE1Un4f7Amf+w==",
"version": "0.21.5",
"resolved": "https://registry.npmmirror.com/@esbuild/linux-mips64el/-/linux-mips64el-0.21.5.tgz",
"integrity": "sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==",
"cpu": [
"mips64el"
],
@@ -596,9 +597,9 @@
}
},
"node_modules/@esbuild/linux-ppc64": {
"version": "0.19.12",
"resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.19.12.tgz",
"integrity": "sha512-nYJA2/QPimDQOh1rKWedNOe3Gfc8PabU7HT3iXWtNUbRzXS9+vgB0Fjaqr//XNbd82mCxHzik2qotuI89cfixg==",
"version": "0.21.5",
"resolved": "https://registry.npmmirror.com/@esbuild/linux-ppc64/-/linux-ppc64-0.21.5.tgz",
"integrity": "sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==",
"cpu": [
"ppc64"
],
@@ -613,9 +614,9 @@
}
},
"node_modules/@esbuild/linux-riscv64": {
"version": "0.19.12",
"resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.19.12.tgz",
"integrity": "sha512-2MueBrlPQCw5dVJJpQdUYgeqIzDQgw3QtiAHUC4RBz9FXPrskyyU3VI1hw7C0BSKB9OduwSJ79FTCqtGMWqJHg==",
"version": "0.21.5",
"resolved": "https://registry.npmmirror.com/@esbuild/linux-riscv64/-/linux-riscv64-0.21.5.tgz",
"integrity": "sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==",
"cpu": [
"riscv64"
],
@@ -630,9 +631,9 @@
}
},
"node_modules/@esbuild/linux-s390x": {
"version": "0.19.12",
"resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.19.12.tgz",
"integrity": "sha512-+Pil1Nv3Umes4m3AZKqA2anfhJiVmNCYkPchwFJNEJN5QxmTs1uzyy4TvmDrCRNT2ApwSari7ZIgrPeUx4UZDg==",
"version": "0.21.5",
"resolved": "https://registry.npmmirror.com/@esbuild/linux-s390x/-/linux-s390x-0.21.5.tgz",
"integrity": "sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==",
"cpu": [
"s390x"
],
@@ -647,9 +648,9 @@
}
},
"node_modules/@esbuild/linux-x64": {
"version": "0.19.12",
"resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.19.12.tgz",
"integrity": "sha512-B71g1QpxfwBvNrfyJdVDexenDIt1CiDN1TIXLbhOw0KhJzE78KIFGX6OJ9MrtC0oOqMWf+0xop4qEU8JrJTwCg==",
"version": "0.21.5",
"resolved": "https://registry.npmmirror.com/@esbuild/linux-x64/-/linux-x64-0.21.5.tgz",
"integrity": "sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==",
"cpu": [
"x64"
],
@@ -664,9 +665,9 @@
}
},
"node_modules/@esbuild/netbsd-x64": {
"version": "0.19.12",
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.19.12.tgz",
"integrity": "sha512-3ltjQ7n1owJgFbuC61Oj++XhtzmymoCihNFgT84UAmJnxJfm4sYCiSLTXZtE00VWYpPMYc+ZQmB6xbSdVh0JWA==",
"version": "0.21.5",
"resolved": "https://registry.npmmirror.com/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz",
"integrity": "sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==",
"cpu": [
"x64"
],
@@ -681,9 +682,9 @@
}
},
"node_modules/@esbuild/openbsd-x64": {
"version": "0.19.12",
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.19.12.tgz",
"integrity": "sha512-RbrfTB9SWsr0kWmb9srfF+L933uMDdu9BIzdA7os2t0TXhCRjrQyCeOt6wVxr79CKD4c+p+YhCj31HBkYcXebw==",
"version": "0.21.5",
"resolved": "https://registry.npmmirror.com/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz",
"integrity": "sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==",
"cpu": [
"x64"
],
@@ -698,9 +699,9 @@
}
},
"node_modules/@esbuild/sunos-x64": {
"version": "0.19.12",
"resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.19.12.tgz",
"integrity": "sha512-HKjJwRrW8uWtCQnQOz9qcU3mUZhTUQvi56Q8DPTLLB+DawoiQdjsYq+j+D3s9I8VFtDr+F9CjgXKKC4ss89IeA==",
"version": "0.21.5",
"resolved": "https://registry.npmmirror.com/@esbuild/sunos-x64/-/sunos-x64-0.21.5.tgz",
"integrity": "sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==",
"cpu": [
"x64"
],
@@ -715,9 +716,9 @@
}
},
"node_modules/@esbuild/win32-arm64": {
"version": "0.19.12",
"resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.19.12.tgz",
"integrity": "sha512-URgtR1dJnmGvX864pn1B2YUYNzjmXkuJOIqG2HdU62MVS4EHpU2946OZoTMnRUHklGtJdJZ33QfzdjGACXhn1A==",
"version": "0.21.5",
"resolved": "https://registry.npmmirror.com/@esbuild/win32-arm64/-/win32-arm64-0.21.5.tgz",
"integrity": "sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==",
"cpu": [
"arm64"
],
@@ -732,9 +733,9 @@
}
},
"node_modules/@esbuild/win32-ia32": {
"version": "0.19.12",
"resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.19.12.tgz",
"integrity": "sha512-+ZOE6pUkMOJfmxmBZElNOx72NKpIa/HFOMGzu8fqzQJ5kgf6aTGrcJaFsNiVMH4JKpMipyK+7k0n2UXN7a8YKQ==",
"version": "0.21.5",
"resolved": "https://registry.npmmirror.com/@esbuild/win32-ia32/-/win32-ia32-0.21.5.tgz",
"integrity": "sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==",
"cpu": [
"ia32"
],
@@ -749,9 +750,9 @@
}
},
"node_modules/@esbuild/win32-x64": {
"version": "0.19.12",
"resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.19.12.tgz",
"integrity": "sha512-T1QyPSDCyMXaO3pzBkF96E8xMkiRYbUEZADd29SyPGabqxMViNoii+NcK7eWJAEoU6RZyEm5lVSIjTmcdoB9HA==",
"version": "0.21.5",
"resolved": "https://registry.npmmirror.com/@esbuild/win32-x64/-/win32-x64-0.21.5.tgz",
"integrity": "sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==",
"cpu": [
"x64"
],
@@ -1305,6 +1306,13 @@
"@jridgewell/sourcemap-codec": "^1.4.14"
}
},
"node_modules/@rolldown/pluginutils": {
"version": "1.0.0-beta.27",
"resolved": "https://registry.npmmirror.com/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.27.tgz",
"integrity": "sha512-+d0F4MKMCbeVUJwG96uQ4SgAznZNSq93I3V+9NHA4OpvqG8mRCpGdKmK8l/dl02h2CCDHwW2FqilnTyDcAnqjA==",
"dev": true,
"license": "MIT"
},
"node_modules/@rollup/pluginutils": {
"version": "5.3.0",
"resolved": "https://registry.npmjs.org/@rollup/pluginutils/-/pluginutils-5.3.0.tgz",
@@ -1787,52 +1795,46 @@
"license": "MIT"
},
"node_modules/@types/react": {
"version": "18.2.0",
"resolved": "https://registry.npmjs.org/@types/react/-/react-18.2.0.tgz",
"integrity": "sha512-0FLj93y5USLHdnhIhABk83rm8XEGA7kH3cr+YUlvxoUGp1xNt/DINUMvqPxLyOQMzLmZe8i4RTHbvb8MC7NmrA==",
"version": "18.3.28",
"resolved": "https://registry.npmmirror.com/@types/react/-/react-18.3.28.tgz",
"integrity": "sha512-z9VXpC7MWrhfWipitjNdgCauoMLRdIILQsAEV+ZesIzBq/oUlxk0m3ApZuMFCXdnS4U7KrI+l3WRUEGQ8K1QKw==",
"devOptional": true,
"license": "MIT",
"peer": true,
"dependencies": {
"@types/prop-types": "*",
"@types/scheduler": "*",
"csstype": "^3.0.2"
"csstype": "^3.2.2"
}
},
"node_modules/@types/react-dom": {
"version": "18.2.0",
"resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.2.0.tgz",
"integrity": "sha512-8yQrvS6sMpSwIovhPOwfyNf2Wz6v/B62LFSVYQ85+Rq3tLsBIG7rP5geMxaijTUxSkrO6RzN/IRuIAADYQsleA==",
"version": "18.3.7",
"resolved": "https://registry.npmmirror.com/@types/react-dom/-/react-dom-18.3.7.tgz",
"integrity": "sha512-MEe3UeoENYVFXzoXEWsvcpg6ZvlrFNlOQ7EOsvhI3CfAXwzPfO8Qwuxd40nepsYKqyyVQnTdEfv68q91yLcKrQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"@types/react": "*"
"peerDependencies": {
"@types/react": "^18.0.0"
}
},
"node_modules/@types/scheduler": {
"version": "0.26.0",
"resolved": "https://registry.npmjs.org/@types/scheduler/-/scheduler-0.26.0.tgz",
"integrity": "sha512-WFHp9YUJQ6CKshqoC37iOlHnQSmxNc795UhB26CyBBttrN9svdIrUjl/NjnNmfcwtncN0h/0PPAFWv9ovP8mLA==",
"devOptional": true,
"license": "MIT"
},
"node_modules/@vitejs/plugin-react": {
"version": "4.2.1",
"resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-4.2.1.tgz",
"integrity": "sha512-oojO9IDc4nCUUi8qIR11KoQm0XFFLIwsRBwHRR4d/88IWghn1y6ckz/bJ8GHDCsYEJee8mDzqtJxh15/cisJNQ==",
"version": "4.7.0",
"resolved": "https://registry.npmmirror.com/@vitejs/plugin-react/-/plugin-react-4.7.0.tgz",
"integrity": "sha512-gUu9hwfWvvEDBBmgtAowQCojwZmJ5mcLn3aufeCsitijs3+f2NsrPtlAWIR6OPiqljl96GVCUbLe0HyqIpVaoA==",
"dev": true,
"license": "MIT",
"dependencies": {
"@babel/core": "^7.23.5",
"@babel/plugin-transform-react-jsx-self": "^7.23.3",
"@babel/plugin-transform-react-jsx-source": "^7.23.3",
"@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.14.0"
"react-refresh": "^0.17.0"
},
"engines": {
"node": "^14.18.0 || >=16.0.0"
},
"peerDependencies": {
"vite": "^4.2.0 || ^5.0.0"
"vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0"
}
},
"node_modules/@xyflow/react": {
@@ -1928,6 +1930,7 @@
}
],
"license": "MIT",
"peer": true,
"dependencies": {
"baseline-browser-mapping": "^2.10.12",
"caniuse-lite": "^1.0.30001782",
@@ -2046,6 +2049,7 @@
"resolved": "https://registry.npmmirror.com/d3-selection/-/d3-selection-3.0.0.tgz",
"integrity": "sha512-fmTRWbNMmsmWq6xJV8D19U/gw/bwrHfNXxrIN+HfZgnzqTHp9jOmKMhsTUjXOJnZOdZY9Q28y4yebKzqDKlxlQ==",
"license": "ISC",
"peer": true,
"engines": {
"node": ">=12"
}
@@ -2130,9 +2134,9 @@
"license": "ISC"
},
"node_modules/esbuild": {
"version": "0.19.12",
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.19.12.tgz",
"integrity": "sha512-aARqgq8roFBj054KvQr5f1sFu0D65G+miZRCuJyJ0G13Zwx7vRar5Zhn2tkQNzIXcBrNVsv/8stehpj+GAjgbg==",
"version": "0.21.5",
"resolved": "https://registry.npmmirror.com/esbuild/-/esbuild-0.21.5.tgz",
"integrity": "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==",
"dev": true,
"hasInstallScript": true,
"license": "MIT",
@@ -2143,29 +2147,29 @@
"node": ">=12"
},
"optionalDependencies": {
"@esbuild/aix-ppc64": "0.19.12",
"@esbuild/android-arm": "0.19.12",
"@esbuild/android-arm64": "0.19.12",
"@esbuild/android-x64": "0.19.12",
"@esbuild/darwin-arm64": "0.19.12",
"@esbuild/darwin-x64": "0.19.12",
"@esbuild/freebsd-arm64": "0.19.12",
"@esbuild/freebsd-x64": "0.19.12",
"@esbuild/linux-arm": "0.19.12",
"@esbuild/linux-arm64": "0.19.12",
"@esbuild/linux-ia32": "0.19.12",
"@esbuild/linux-loong64": "0.19.12",
"@esbuild/linux-mips64el": "0.19.12",
"@esbuild/linux-ppc64": "0.19.12",
"@esbuild/linux-riscv64": "0.19.12",
"@esbuild/linux-s390x": "0.19.12",
"@esbuild/linux-x64": "0.19.12",
"@esbuild/netbsd-x64": "0.19.12",
"@esbuild/openbsd-x64": "0.19.12",
"@esbuild/sunos-x64": "0.19.12",
"@esbuild/win32-arm64": "0.19.12",
"@esbuild/win32-ia32": "0.19.12",
"@esbuild/win32-x64": "0.19.12"
"@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": {
@@ -2395,7 +2399,7 @@
},
"node_modules/rc-util": {
"version": "5.44.4",
"resolved": "https://registry.npmjs.org/rc-util/-/rc-util-5.44.4.tgz",
"resolved": "https://registry.npmmirror.com/rc-util/-/rc-util-5.44.4.tgz",
"integrity": "sha512-resueRJzmHG9Q6rI/DfK6Kdv9/Lfls05vzMs1Sk3M2P+3cJa+MakaZyWY8IPfehVuhPJFKrIY1IK4GqbiaiY5w==",
"license": "MIT",
"dependencies": {
@@ -2408,10 +2412,11 @@
}
},
"node_modules/react": {
"version": "18.2.0",
"resolved": "https://registry.npmjs.org/react/-/react-18.2.0.tgz",
"integrity": "sha512-/3IjMdb2L9QbBdWiW5e3P2/npwMBaU9mHCSCUzNln0ZCYbcfTsGbTJrU/kGemdH2IWmB2ioZ+zkxtmq6g09fGQ==",
"version": "18.3.1",
"resolved": "https://registry.npmmirror.com/react/-/react-18.3.1.tgz",
"integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==",
"license": "MIT",
"peer": true,
"dependencies": {
"loose-envify": "^1.1.0"
},
@@ -2420,28 +2425,29 @@
}
},
"node_modules/react-dom": {
"version": "18.2.0",
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.2.0.tgz",
"integrity": "sha512-6IMTriUmvsjHUjNtEDudZfuDQUoWXVxKHhlEGSk81n4YFS+r/Kl99wXiwlVXtPBtJenozv2P+hxDsw9eA7Xo6g==",
"version": "18.3.1",
"resolved": "https://registry.npmmirror.com/react-dom/-/react-dom-18.3.1.tgz",
"integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==",
"license": "MIT",
"peer": true,
"dependencies": {
"loose-envify": "^1.1.0",
"scheduler": "^0.23.0"
"scheduler": "^0.23.2"
},
"peerDependencies": {
"react": "^18.2.0"
"react": "^18.3.1"
}
},
"node_modules/react-is": {
"version": "18.3.1",
"resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz",
"resolved": "https://registry.npmmirror.com/react-is/-/react-is-18.3.1.tgz",
"integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==",
"license": "MIT"
},
"node_modules/react-refresh": {
"version": "0.14.2",
"resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.14.2.tgz",
"integrity": "sha512-jCvmsr+1IUSMUyzOkRcvnVbX3ZYC6g9TDrDbFuFmRDq7PD4yaGbLKNQL6k2jnArV8hjYxh7hVhAZB6s9HDGpZA==",
"version": "0.17.0",
"resolved": "https://registry.npmmirror.com/react-refresh/-/react-refresh-0.17.0.tgz",
"integrity": "sha512-z6F7K9bV85EfseRCp2bzrpyQ0Gkw1uLoCel9XBVWPg/TjRj94SkJzUTGfOa4bs7iJvBWtQG0Wq7wnI0syw3EBQ==",
"dev": true,
"license": "MIT",
"engines": {
@@ -2596,9 +2602,9 @@
"optional": true
},
"node_modules/typescript": {
"version": "5.3.3",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.3.3.tgz",
"integrity": "sha512-pXWcraxM0uxAS+tN0AG/BF2TyqmHO014Z070UsJ+pFvYuRSq8KH8DmWpnbXe0pEPDHXZV3FcAbJkijJ5oNEnWw==",
"version": "5.9.3",
"resolved": "https://registry.npmmirror.com/typescript/-/typescript-5.9.3.tgz",
"integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
"dev": true,
"license": "Apache-2.0",
"bin": {
@@ -2650,15 +2656,16 @@
}
},
"node_modules/vite": {
"version": "5.1.0",
"resolved": "https://registry.npmjs.org/vite/-/vite-5.1.0.tgz",
"integrity": "sha512-STmSFzhY4ljuhz14bg9LkMTk3d98IO6DIArnTY6MeBwiD1Za2StcQtz7fzOUnRCqrHSD5+OS2reg4HOz1eoLnw==",
"version": "5.4.21",
"resolved": "https://registry.npmmirror.com/vite/-/vite-5.4.21.tgz",
"integrity": "sha512-o5a9xKjbtuhY6Bi5S3+HvbRERmouabWbyUcpXXUA1u+GNUKoROi9byOJ8M0nHbHYHkYICiMlqxkg1KkYmm25Sw==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"esbuild": "^0.19.3",
"postcss": "^8.4.35",
"rollup": "^4.2.0"
"esbuild": "^0.21.3",
"postcss": "^8.4.43",
"rollup": "^4.20.0"
},
"bin": {
"vite": "bin/vite.js"
@@ -2677,6 +2684,7 @@
"less": "*",
"lightningcss": "^1.21.0",
"sass": "*",
"sass-embedded": "*",
"stylus": "*",
"sugarss": "*",
"terser": "^5.4.0"
@@ -2694,6 +2702,9 @@
"sass": {
"optional": true
},
"sass-embedded": {
"optional": true
},
"stylus": {
"optional": true
},
-2
View File
@@ -1218,8 +1218,6 @@ function App() {
onOpenEcommerce={() => handleSetView("ecommerce")}
onOpenScriptReview={() => handleSetView("scriptTokens")}
onOpenTokenMonitor={() => handleSetView("tokenUsage")}
onSelectView={handleSetView}
onOpenImageTool={handleOpenImageWorkbenchTool}
/>
);
}
+91 -50
View File
@@ -1,7 +1,8 @@
import { buildApiUrl, buildAuthHeaders } from "./serverConnection";
const TEXT_MODEL = "qwen-max";
const VISION_MODEL = "qwen3.6-plus";
const VISION_MODEL = "qwen3.7-plus";
const VISION_FALLBACK_MODEL = "qwen-vl-plus";
export interface AdVideoUserConfig {
platform: string;
@@ -107,36 +108,63 @@ interface ChatMessage {
content: string;
}
const MAX_RETRIES = 3;
const RETRY_BASE_MS = 2000;
const CHAT_TIMEOUT_MS = 120_000; // 2 minutes per AI call
function isTransientError(err: unknown): boolean {
if (!(err instanceof Error)) return false;
const msg = err.message.toLowerCase();
return /\b429\b/.test(msg) || msg.includes("signal timed out") || msg.includes("aborted") || msg.includes("timeout");
}
async function retryOnTransient<T>(fn: () => Promise<T>, signal?: AbortSignal): Promise<T> {
for (let attempt = 0; attempt <= MAX_RETRIES; attempt++) {
try {
return await fn();
} catch (err) {
if (signal?.aborted) throw err;
if (attempt === MAX_RETRIES) throw err;
if (!isTransientError(err)) throw err;
const delay = RETRY_BASE_MS * 2 ** attempt + Math.random() * 1000;
await new Promise((r) => setTimeout(r, delay));
}
}
throw new Error("unreachable");
}
async function chat(
systemPrompt: string,
userContent: string,
options?: { model?: string; signal?: AbortSignal },
): Promise<string> {
const messages: ChatMessage[] = [
{ role: "system", content: systemPrompt },
{ role: "user", content: userContent },
];
const timeoutSignal = AbortSignal.timeout(60000);
const combinedSignal = options?.signal
? AbortSignal.any([options.signal, timeoutSignal])
: timeoutSignal;
const res = await fetch(buildApiUrl("ai/chat"), {
method: "POST",
headers: buildAuthHeaders(),
body: JSON.stringify({
model: options?.model ?? TEXT_MODEL,
messages,
stream: false,
temperature: 0.4,
}),
signal: combinedSignal,
});
if (!res.ok) throw new Error(`AI 调用失败 (${res.status})`);
const payload = await res.json();
const content: string =
payload?.content ?? payload?.choices?.[0]?.message?.content ?? payload?.text ?? "";
if (!content) throw new Error("模型未返回有效内容");
return content;
return retryOnTransient(async () => {
const messages: ChatMessage[] = [
{ role: "system", content: systemPrompt },
{ role: "user", content: userContent },
];
const timeoutSignal = AbortSignal.timeout(CHAT_TIMEOUT_MS);
const combinedSignal = options?.signal
? AbortSignal.any([options.signal, timeoutSignal])
: timeoutSignal;
const res = await fetch(buildApiUrl("ai/chat"), {
method: "POST",
headers: buildAuthHeaders(),
body: JSON.stringify({
model: options?.model ?? TEXT_MODEL,
messages,
stream: false,
temperature: 0.4,
}),
signal: combinedSignal,
});
if (!res.ok) throw new Error(`AI 调用失败 (${res.status})`);
const payload = await res.json();
const content: string =
payload?.content ?? payload?.choices?.[0]?.message?.content ?? payload?.text ?? "";
if (!content) throw new Error("模型未返回有效内容");
return content;
}, options?.signal);
}
async function visionChat(
@@ -149,30 +177,43 @@ async function visionChat(
...imageUrls.map((url) => ({ type: "image_url", image_url: { url } })),
{ type: "text", text },
];
const timeoutSignal = AbortSignal.timeout(60000);
const combinedSignal = signal
? AbortSignal.any([signal, timeoutSignal])
: timeoutSignal;
const res = await fetch(buildApiUrl("ai/chat"), {
method: "POST",
headers: buildAuthHeaders(),
body: JSON.stringify({
model: VISION_MODEL,
messages: [
{ role: "system", content: systemPrompt },
{ role: "user", content },
],
stream: false,
temperature: 0.3,
}),
signal: combinedSignal,
});
if (!res.ok) throw new Error(`图片理解调用失败 (${res.status})`);
const payload = await res.json();
const out: string =
payload?.content ?? payload?.choices?.[0]?.message?.content ?? payload?.text ?? "";
if (!out) throw new Error("图片理解未返回有效内容");
return out;
const messages = [
{ role: "system", content: systemPrompt },
{ role: "user", content },
];
for (const model of [VISION_MODEL, VISION_FALLBACK_MODEL]) {
const timeoutSignal = AbortSignal.timeout(CHAT_TIMEOUT_MS);
const combinedSignal = signal
? AbortSignal.any([signal, timeoutSignal])
: timeoutSignal;
try {
const out = await retryOnTransient(async () => {
const res = await fetch(buildApiUrl("ai/chat"), {
method: "POST",
headers: buildAuthHeaders(),
body: JSON.stringify({ model, messages, stream: false, temperature: 0.3 }),
signal: combinedSignal,
});
if (!res.ok) {
const errBody = await res.text().catch(() => "");
if (model === VISION_MODEL && errBody.includes("image format")) throw new Error("IMAGE_FORMAT_FALLBACK");
throw new Error(`图片理解调用失败 (${res.status})`);
}
const payload = await res.json();
const result: string =
payload?.content ?? payload?.choices?.[0]?.message?.content ?? payload?.text ?? "";
if (!result) throw new Error("图片理解未返回有效内容");
return result;
}, signal);
return out;
} catch (err) {
if (err instanceof Error && err.message === "IMAGE_FORMAT_FALLBACK") continue;
if (model === VISION_MODEL && err instanceof Error && err.message?.includes("图片理解调用失败")) continue;
throw err;
}
}
throw new Error("图片理解调用失败,所有模型均不可用");
}
const IMAGE_UNDERSTANDING_PROMPT = `你是电商产品图片分析专家。请分析用户提供的产品图片,识别产品主体、外观、颜色、材质、形状、尺寸感、品牌标识、关键部件、可视化卖点和适合展示的镜头角度。请用简洁的中文段落描述,不要编造图片中看不到的信息。`;
+18
View File
@@ -403,6 +403,24 @@ export const aiGenerationClient = {
return readJsonResponse<{ url: string; ossKey?: string }>(res, "Asset upload response failed");
},
async uploadAssetBinary(blob: Blob, options?: { name?: string; mimeType?: string; scope?: string }): Promise<{ url: string; signedUrl?: string; ossKey?: string }> {
const form = new FormData();
form.append("file", blob, options?.name || "upload.png");
if (options?.scope) form.append("scope", options.scope);
if (options?.mimeType) form.append("mimeType", options.mimeType);
// Exclude Content-Type so browser auto-sets multipart/form-data with boundary
const { "Content-Type": _ct, ...authHeaders } = buildAuthHeaders();
const res = await fetch(buildApiUrl("oss/upload-binary"), {
method: "POST",
headers: authHeaders,
body: form,
});
if (!res.ok) {
await throwResponseError(res, "Binary asset upload failed");
}
return readJsonResponse<{ url: string; ossKey?: string }>(res, "Binary asset upload response failed");
},
async uploadAssetByUrl(input: UploadAssetByUrlInput): Promise<{ url: string; signedUrl?: string; ossKey?: string }> {
const res = await fetch(buildApiUrl("oss/upload-by-url"), {
method: "POST",
+44 -23
View File
@@ -10,39 +10,50 @@ export interface ScriptEvalResult {
suggestions: string[];
}
const EVAL_SYSTEM_PROMPT = `你是一位专业的剧本评专家。请对用户提供的剧本进行六维评分分析,并以严格的 JSON 格式返回结果
const EVAL_SYSTEM_PROMPT = `你是一位资深影视剧本评专家,拥有二十年以上的编剧、制片和剧本医生经验。你精通各类影视叙事理论(三幕式、英雄之旅、起承转合、序列法),同时深度跟踪AIGC短剧/漫剧行业最新趋势。你的任务是对用户提供的剧本进行严谨、系统、多维度的量化评分
六个评分维度:
1. hook(钩子设计,满分20):开篇吸引力、悬念设置、黄金三秒法则
2. character(角色塑造,满分15):人物立体度、动机合理性、弧光设计
3. plot(剧情结构,满分20):起承转合、节奏把控、冲突设计
4. dialogue(台词对白,满分15):语言质感、角色差异化、潜台词
5. visual(画面表现,满分15):镜头感、空间层次、视觉冲击力
6. content(内容深度,满分15):主题表达、情感共鸣、思想内核
【剧本类型识别】
收到剧本后,首先判断类型:AIGC短剧/漫剧(单集5-30分钟,竖屏平台,高密度反转、强节奏)或传统影视剧本(单集40分钟以上,长视频平台,完整起承转合)。类型判定将影响各维度的评价侧重点。
请严格按以下 JSON 格式返回(不要包含任何其他文字):
【评分体系(100分制,六个维度)】
1. hook 钩子设计(20分):开篇钩子、集末钩子、场景内钩子、悬念链完整性。短剧前3秒须有即时爆点;长剧第一幕结束前须建立核心悬念。
2. plot 剧情结构(20分):结构框架、节奏控制、冲突设计、逻辑自洽。短剧"每分钟有事件",反转密度加分;长剧需处理好B线C线与主线交织。
3. character 角色塑造(18分):主角弧光、角色辨识度、角色动机、配角质量。短剧角色须在前2分钟建立;长剧需要内在矛盾和多阶段成长。
4. dialogue 台词对白(15分):角色语言区分度、信息传递效率、潜台词与留白、金句与记忆点。
5. visual 画面表现(15分):场景描写质量、视觉叙事技巧、镜头感与节奏、制作可行性。AIGC需考虑AI生成技术边界与一致性。
6. content 内容深度(12分):主题表达、情感共鸣、社会/人性洞察。
【评分铁律】
- 扣分必须明确指出剧本中的具体段落/场景/台词。
- 严禁给出任何维度的满分,必须有扣分理由。
- 优缺点都要充分展开,不可只批不夸或只夸不批。
- 不因题材类型偏见降低评分,不因某一方面出色而抬高其他维度(避免光环效应)。
- 敢于拉开各维度分数差距,避免全部给中等分数。
【等级标准】按总分百分比:S≥90 | A 80-89 | B 70-79 | C 60-69 | D<60。
请严格按以下 JSON 格式返回(不要包含任何其他文字,不要用代码块包裹以外的说明):
{
"dimensionScores": { "hook": 数字, "character": 数字, "plot": 数字, "dialogue": 数字, "visual": 数字, "content": 数字 },
"summary": "一句话总结评价",
"issues": ["问题1", "问题2", ...],
"highlights": ["亮点1", "亮点2", ...],
"suggestions": ["建议1", "建议2", ...]
"dimensionScores": { "hook": 数字, "plot": 数字, "character": 数字, "dialogue": 数字, "visual": 数字, "content": 数字 },
"summary": "200-300字综合评价,概括整体质量、市场潜力与目标受众匹配度",
"issues": ["每条指出具体维度的扣分点并引用剧本原文位置", ...],
"highlights": ["核心亮点,引用剧本具体场景", ...],
"suggestions": ["按优先级排列的改进建议(最优先/次优先/可优化)", ...]
}`;
const DIMENSION_WEIGHTS: Record<string, { maxScore: number; weight: number }> = {
hook: { maxScore: 20, weight: 0.2 },
character: { maxScore: 15, weight: 0.15 },
plot: { maxScore: 20, weight: 0.2 },
dialogue: { maxScore: 15, weight: 0.15 },
visual: { maxScore: 15, weight: 0.15 },
content: { maxScore: 15, weight: 0.15 },
const DIMENSION_WEIGHTS: Record<string, { maxScore: number }> = {
hook: { maxScore: 20 },
plot: { maxScore: 20 },
character: { maxScore: 18 },
dialogue: { maxScore: 15 },
visual: { maxScore: 15 },
content: { maxScore: 12 },
};
function computeTotalAndGrade(scores: Record<string, number>): { totalScore: number; grade: string } {
const totalScore = Math.round(
Object.entries(DIMENSION_WEIGHTS).reduce((sum, [key, dim]) => {
const score = Math.max(0, Math.min(dim.maxScore, scores[key] ?? 0));
return sum + (score / dim.maxScore) * 100 * dim.weight;
return sum + Math.max(0, Math.min(dim.maxScore, scores[key] ?? 0));
}, 0),
);
const grade = totalScore >= 90 ? "S" : totalScore >= 80 ? "A" : totalScore >= 70 ? "B" : totalScore >= 60 ? "C" : "D";
@@ -56,6 +67,7 @@ function extractJson(text: string): unknown {
}
export async function evaluateScript(script: string, signal?: AbortSignal): Promise<ScriptEvalResult> {
console.log("[API] 发送评测请求,剧本长度:", script.slice(0, 8000).length, "字符");
const res = await fetch(buildApiUrl("ai/chat"), {
method: "POST",
headers: buildAuthHeaders(),
@@ -71,11 +83,15 @@ export async function evaluateScript(script: string, signal?: AbortSignal): Prom
signal,
});
console.log("[API] 响应状态:", res.status, res.statusText);
if (!res.ok) {
throw new Error(`评测请求失败 (${res.status})`);
}
const payload = await res.json();
console.log("[API] 原始响应体:", payload);
const content: string = payload?.choices?.[0]?.message?.content
?? payload?.result?.content
?? payload?.content
@@ -84,7 +100,11 @@ export async function evaluateScript(script: string, signal?: AbortSignal): Prom
if (!content) throw new Error("模型未返回有效内容");
console.log("[API] 模型返回内容 (前500字符):", content.slice(0, 500));
const parsed = extractJson(content) as Record<string, unknown>;
console.log("[API] 解析后的JSON:", parsed);
const dimensionScores: Record<string, number> = {};
const rawScores = parsed.dimensionScores as Record<string, number> | undefined;
if (!rawScores || typeof rawScores !== "object") throw new Error("评分格式异常");
@@ -95,6 +115,7 @@ export async function evaluateScript(script: string, signal?: AbortSignal): Prom
}
const { totalScore, grade } = computeTotalAndGrade(dimensionScores);
console.log("[API] 计算后总分:", totalScore, "等级:", grade);
return {
totalScore,
Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.0 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.5 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.2 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.6 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.0 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.5 MiB

+5 -8
View File
@@ -1321,18 +1321,15 @@ function ProductClonePage(_props: ProductClonePageProps = {}) {
});
const uploadProductImages = async (): Promise<string[]> => {
const SUPPORTED_IMAGE_TYPES = new Set(["image/jpeg", "image/png", "image/webp", "image/gif"]);
const urls: string[] = [];
for (const item of productImages) {
try {
const resp = await fetch(item.src);
const blob = await resp.blob();
const dataUrl = await new Promise<string>((resolve, reject) => {
const reader = new FileReader();
reader.onload = () => resolve(String(reader.result));
reader.onerror = () => reject(reader.error);
reader.readAsDataURL(blob);
});
const { url } = await aiGenerationClient.uploadAsset({ dataUrl, name: item.name, mimeType: blob.type });
const rawBlob = await resp.blob();
const mimeType = SUPPORTED_IMAGE_TYPES.has(rawBlob.type) ? rawBlob.type : "image/png";
const blob = rawBlob.type === mimeType ? rawBlob : new Blob([rawBlob], { type: mimeType });
const { url } = await aiGenerationClient.uploadAssetBinary(blob, { name: item.name, mimeType, scope: "ecommerce-product" });
urls.push(url);
} catch {
// skip images that fail to upload
@@ -9,7 +9,6 @@ import {
type AdVideoUserConfig,
} from "../../api/adVideoPlanClient";
import { aiGenerationClient } from "../../api/aiGenerationClient";
import { uploadAssetWithProgress } from "../../api/uploadWithProgress";
import { waitForTask } from "../../api/taskSubscription";
import { resolveVideoRequestModel } from "../../utils/resolveVideoModel";
import type {
@@ -34,12 +33,18 @@ export async function runVideoPlan(
onStepStart("upload");
const imageUrls: string[] = [];
for (const dataUrl of imageDataUrls) {
const result = await uploadAssetWithProgress(
{ dataUrl, scope: "ecommerce-product", mimeType: "image/png" },
{ signal },
);
imageUrls.push(result.url);
const SUPPORTED_IMAGE_TYPES = new Set(["image/jpeg", "image/png", "image/webp", "image/gif"]);
for (const srcUrl of imageDataUrls) {
try {
const resp = await fetch(srcUrl);
const rawBlob = await resp.blob();
const mimeType = SUPPORTED_IMAGE_TYPES.has(rawBlob.type) ? rawBlob.type : "image/png";
const blob = rawBlob.type === mimeType ? rawBlob : new Blob([rawBlob], { type: mimeType });
const result = await aiGenerationClient.uploadAssetBinary(blob, { mimeType, scope: "ecommerce-product" });
imageUrls.push(result.url);
} catch {
// skip images that fail to upload
}
}
onStepDone("upload");
+2 -13
View File
@@ -8,10 +8,7 @@ import {
ThunderboltOutlined,
} from "@ant-design/icons";
import { useCallback, useEffect, useMemo, useRef, useState, type CSSProperties } from "react";
import type { WebViewKey, WebImageWorkbenchTool } from "../../types";
import WelcomeSplash from "./WelcomeSplash";
import ToolboxSection from "./ToolboxSection";
import ScriptReviewVisual from "./ScriptReviewVisual";
const OSS_MUBAN = "https://stringtest.oss-cn-hangzhou.aliyuncs.com/muban";
const heroImage1 = `${OSS_MUBAN}/hero-1.png`;
@@ -27,8 +24,6 @@ interface HomePageProps {
onOpenEcommerce: () => void;
onOpenScriptReview?: () => void;
onOpenTokenMonitor?: () => void;
onSelectView: (view: WebViewKey) => void;
onOpenImageTool?: (tool: WebImageWorkbenchTool) => void;
}
const HOME_BACKGROUND_VIDEO = "https://stringtest.oss-cn-hangzhou.aliyuncs.com/%E6%A0%B7%E7%89%87.mp4";
@@ -117,7 +112,7 @@ function getHomeCarouselCardStyle(offset: number): CSSProperties {
} as CSSProperties;
}
function HomePage({ onOpenGenerate, onOpenEcommerce, onOpenScriptReview, onOpenTokenMonitor, onSelectView, onOpenImageTool }: HomePageProps) {
function HomePage({ onOpenGenerate, onOpenEcommerce, onOpenScriptReview, onOpenTokenMonitor }: HomePageProps) {
const [splashDismissed, setSplashDismissed] = useState(() => sessionStorage.getItem("omniai:splash-seen") === "1");
const [activeSlideIndex, setActiveSlideIndex] = useState(0);
const [carouselMotion, setCarouselMotion] = useState<HomeCarouselMotion | null>(null);
@@ -301,11 +296,7 @@ function HomePage({ onOpenGenerate, onOpenEcommerce, onOpenScriptReview, onOpenT
</button>
</div>
<div className="omni-home__feature-visual" aria-hidden="true">
{feature.key === "script" ? (
<ScriptReviewVisual />
) : (
<img src={feature.imageUrl} alt="" />
)}
<img src={feature.imageUrl} alt="" />
</div>
<div className="omni-home__feature-stats" aria-hidden="true">
{feature.stats.map((item) => (
@@ -347,8 +338,6 @@ function HomePage({ onOpenGenerate, onOpenEcommerce, onOpenScriptReview, onOpenT
</button>
</div>
</section>
<ToolboxSection onSelectView={onSelectView} onOpenImageTool={onOpenImageTool} />
</main>
</section>
</>
-133
View File
@@ -1,133 +0,0 @@
import { useEffect, useRef, useState } from "react";
const DIMS = [
{ name: "钩子设计", score: 19, max: 20, hue: 145 },
{ name: "角色塑造", score: 13, max: 15, hue: 155 },
{ name: "剧情结构", score: 18, max: 20, hue: 165 },
{ name: "逻辑严密", score: 14, max: 15, hue: 175 },
{ name: "场景构建", score: 15, max: 15, hue: 185 },
{ name: "内容深度", score: 15, max: 15, hue: 195 },
];
function ScriptReviewVisual() {
const [animated, setAnimated] = useState(false);
const [activeDim, setActiveDim] = useState<number | null>(null);
const [score, setScore] = useState(0);
const scoreRef = useRef<number>(0);
const frameRef = useRef<number | null>(null);
useEffect(() => {
const el = document.getElementById("script-review-visual");
if (!el) return;
const observer = new IntersectionObserver(
([entry]) => {
if (entry?.isIntersecting) {
setAnimated(true);
observer.disconnect();
}
},
{ threshold: 0.3 }
);
observer.observe(el);
return () => observer.disconnect();
}, []);
useEffect(() => {
if (!animated) return;
const start = performance.now();
const target = 94;
const dur = 1400;
function tick(now: number) {
const t = Math.min((now - start) / dur, 1);
const e = 1 - Math.pow(1 - t, 3);
setScore(Math.round(e * target));
if (t < 1) frameRef.current = requestAnimationFrame(tick);
}
frameRef.current = requestAnimationFrame(tick);
return () => { if (frameRef.current) cancelAnimationFrame(frameRef.current); };
}, [animated]);
const totalScore = 94;
const grade = "S";
return (
<div className="omni-script-review-visual" id="script-review-visual">
<div className="omni-script-review-hero">
<div className="omni-script-review-score-row">
<span className="omni-script-review-num">{score}</span>
<span className="omni-script-review-total">/ 100</span>
<div className="omni-script-review-grade">
<span className="omni-script-review-grade-dot" />
<span>{grade}</span>
</div>
</div>
<div className="omni-script-review-bar">
<div
className="omni-script-review-bar-fill"
style={{ width: animated ? `${totalScore}%` : "0%" }}
/>
</div>
<div className="omni-script-review-beat">
<b>92%</b>
</div>
</div>
<div className="omni-script-review-chart">
<div className="omni-script-review-chart-bars">
{DIMS.map((dim, i) => {
const pct = dim.score / dim.max;
const lossPct = (dim.max - dim.score) / dim.max;
const isPerfect = dim.score === dim.max;
const height = animated ? pct * 76 : 0;
const lossHeight = animated ? lossPct * 76 : 0;
return (
<div
key={dim.name}
className={`omni-script-review-bcol${activeDim === i ? " is-active" : ""}${activeDim !== null && activeDim !== i ? " is-dimmed" : ""}`}
onClick={() => setActiveDim(activeDim === i ? null : i)}
>
<div className="omni-script-review-bbar-area">
{lossPct > 0 && (
<div
className="omni-script-review-bseg is-loss"
style={{ height: `${lossHeight}%`, transitionDelay: `${400 + i * 80}ms` }}
/>
)}
<div
className={`omni-script-review-bseg is-score${isPerfect ? " is-perfect" : ""}`}
style={{ height: `${height}%`, transitionDelay: `${400 + i * 80}ms` }}
/>
</div>
<div className="omni-script-review-blabel">
<span>{dim.name}</span>
</div>
</div>
);
})}
</div>
{activeDim !== null && (() => {
const d = DIMS[activeDim]!;
return (
<div className="omni-script-review-diminfo">
<span className="omni-script-review-diminfo-name">{d.name}</span>
<span className="omni-script-review-diminfo-score">
{d.score}<small>/{d.max}</small>
{d.score === d.max && " ★"}
</span>
</div>
);
})()}
<div className="omni-script-review-legend">
<span><span className="omni-script-review-legend-dot is-score" /> </span>
<span><span className="omni-script-review-legend-dot is-loss" /> </span>
</div>
</div>
</div>
);
}
export default ScriptReviewVisual;
-233
View File
@@ -1,233 +0,0 @@
import { ToolOutlined } from "@ant-design/icons";
import type { WebViewKey, WebImageWorkbenchTool } from "../../types";
import toolImageBefore from "../../assets/toolbox/牛仔.png";
import toolImageAfter from "../../assets/toolbox/西装.png";
import watermarkBefore from "../../assets/toolbox/去水印前.png";
import watermarkAfter from "../../assets/toolbox/去水印后.png";
interface ToolboxSectionProps {
onSelectView: (view: WebViewKey) => void;
onOpenImageTool?: (tool: WebImageWorkbenchTool) => void;
}
const TOOLS = [
{
key: "image-studio",
icon: "🎨",
name: "图片工作室",
desc: "图片二次加工,调色裁剪特效风格迁移",
},
{
key: "lens-lab",
icon: "📷",
name: "镜头实验室",
desc: "多视角镜头生成,不同角度与姿势",
},
{
key: "digital-human",
icon: "🧑",
name: "一键数字人",
desc: "上传图片和音频,生成数字人视频",
},
{
key: "watermark-removal",
icon: "✨",
name: "去除水印",
desc: "AI智能识别去除图片视频水印",
},
];
const CARDS = [
{
key: "image-studio",
title: "图片工作室",
tag: "图片加工",
icon: "🎨",
features: ["二次加工", "调色", "裁剪", "风格迁移"],
targetView: "imageWorkbench" as WebViewKey,
render: () => (
<div className="toolbox-card1-content">
<div className="toolbox-card1-side toolbox-card1-left">
<div className="toolbox-card1-img">
<img src={toolImageBefore} alt="图片加工前" />
</div>
<div className="toolbox-card1-label"></div>
</div>
<div className="toolbox-card1-divider" />
<div className="toolbox-card1-side toolbox-card1-right">
<div className="toolbox-card1-img">
<img src={toolImageAfter} alt="图片加工后" />
</div>
<div className="toolbox-card1-label"></div>
</div>
</div>
),
},
{
key: "lens-lab",
title: "镜头实验室",
tag: "多视角",
icon: "📷",
features: ["正面", "45°侧", "俯拍", "仰拍", "背面"],
targetView: "imageWorkbench" as WebViewKey,
render: () => (
<div className="toolbox-card2-content">
{["正面", "45°侧", "俯拍", "仰拍", "背面"].map((angle) => (
<div key={angle} className="toolbox-card2-frame">
<div className="toolbox-card2-product" />
<div className="toolbox-card2-shadow" />
<div className="toolbox-card2-angle-label">{angle}</div>
</div>
))}
</div>
),
},
{
key: "digital-human",
title: "一键数字人",
tag: "视频生成",
icon: "🧑",
features: ["上传人像", "匹配音频", "唇形同步", "生成视频"],
targetView: "digitalHuman" as WebViewKey,
render: () => (
<div className="toolbox-card3-content">
<div className="toolbox-card3-side toolbox-card3-left">
<div className="toolbox-card3-portrait">
<div className="toolbox-card3-portrait-mark">STATIC</div>
</div>
<div className="toolbox-card3-label"></div>
</div>
<div className="toolbox-card3-divider" />
<div className="toolbox-card3-transform"></div>
<div className="toolbox-card3-side toolbox-card3-right">
<div className="toolbox-card3-portrait">
<div className="toolbox-card3-glow-ring" />
<div className="toolbox-card3-lipsync">
<span /><span /><span /><span /><span />
</div>
<div className="toolbox-card3-gesture" />
<div className="toolbox-card3-live">LIVE</div>
</div>
<div className="toolbox-card3-label"></div>
</div>
</div>
),
},
{
key: "watermark-removal",
title: "去除水印",
tag: "AI清除",
icon: "✨",
features: ["智能识别", "精准去除", "无损画质"],
targetView: "watermarkRemoval" as WebViewKey,
render: () => (
<div className="toolbox-card4-content">
<div className="toolbox-card4-side toolbox-card4-left">
<div className="toolbox-card4-img">
<img src={watermarkBefore} alt="去水印前" />
</div>
<div className="toolbox-card4-label"></div>
</div>
<div className="toolbox-card4-divider" />
<div className="toolbox-card4-side toolbox-card4-right">
<div className="toolbox-card4-img">
<img src={watermarkAfter} alt="去水印后" />
</div>
<div className="toolbox-card4-label"></div>
</div>
</div>
),
},
];
function ToolboxSection({ onSelectView, onOpenImageTool }: ToolboxSectionProps) {
const handleCardClick = (targetView: WebViewKey) => {
onSelectView(targetView);
};
return (
<section className="omni-home__toolbox-page" aria-label="OmniAI 工具箱">
<div className="omni-home__toolbox-shell">
{/* Left Panel */}
<aside className="omni-home__toolbox-left">
<div className="omni-home__toolbox-brand">
<div className="omni-home__toolbox-brand-icon">
<ToolOutlined />
</div>
<div className="omni-home__toolbox-brand-text"></div>
</div>
<div className="omni-home__toolbox-title">
<br />
</div>
<div className="omni-home__toolbox-subtitle">
AI工具覆盖图片加工
</div>
<div className="omni-home__toolbox-list">
{TOOLS.map((tool) => (
<div
key={tool.key}
className="omni-home__toolbox-item"
onClick={() => {
const card = CARDS.find((c) => c.key === tool.key);
if (card) handleCardClick(card.targetView);
}}
>
<div className="omni-home__toolbox-item-icon">{tool.icon}</div>
<div className="omni-home__toolbox-item-info">
<div className="omni-home__toolbox-item-name">{tool.name}</div>
<div className="omni-home__toolbox-item-desc">{tool.desc}</div>
</div>
</div>
))}
</div>
<div className="omni-home__toolbox-workflow">
<div className="omni-home__toolbox-workflow-label"></div>
<div className="omni-home__toolbox-workflow-steps">
<span className="omni-home__toolbox-workflow-step"></span>
<span className="omni-home__toolbox-workflow-arrow"></span>
<span className="omni-home__toolbox-workflow-step"></span>
<span className="omni-home__toolbox-workflow-arrow"></span>
<span className="omni-home__toolbox-workflow-step">AI处理</span>
<span className="omni-home__toolbox-workflow-arrow"></span>
<span className="omni-home__toolbox-workflow-step"></span>
</div>
</div>
</aside>
{/* Grid Area */}
<div className="omni-home__toolbox-grid">
{CARDS.map((card) => (
<div
key={card.key}
className="omni-home__toolbox-card"
onClick={() => handleCardClick(card.targetView)}
>
<div className="omni-home__toolbox-card-header">
<div className="omni-home__toolbox-card-header-left">
<div className="omni-home__toolbox-card-icon">{card.icon}</div>
<div className="omni-home__toolbox-card-title">{card.title}</div>
</div>
<div className="omni-home__toolbox-card-tag">{card.tag}</div>
</div>
<div className="omni-home__toolbox-card-content">
{card.render()}
</div>
<div className="omni-home__toolbox-card-footer">
{card.features.map((feat, i) => (
<span key={feat}>
{i > 0 && <span className="omni-home__toolbox-card-feat-sep">|</span>}
<span className="omni-home__toolbox-card-feat">{feat}</span>
</span>
))}
</div>
</div>
))}
</div>
</div>
</section>
);
}
export default ToolboxSection;
+349 -417
View File
@@ -1,20 +1,13 @@
import {
CheckCircleFilled,
CopyOutlined,
DownloadOutlined,
FileTextOutlined,
UploadOutlined,
} from "@ant-design/icons";
import { useEffect, useRef, useState, type ChangeEvent, type KeyboardEvent } from "react";
import { CopyOutlined, DownOutlined, DownloadOutlined, FileTextOutlined, ReloadOutlined, TrophyOutlined, UploadOutlined } from "@ant-design/icons";
import { useEffect, useMemo, useRef, useState, type ChangeEvent, type KeyboardEvent } from "react";
import { evaluateScript } from "../../api/scriptEvalClient";
import { useSessionStore } from "../../stores";
interface ScoreDimension {
key: string;
label: string;
maxScore: number;
hint: string;
detail: string;
weight: number;
description: string;
}
interface EvalResult {
@@ -27,46 +20,103 @@ interface EvalResult {
suggestions: string[];
}
interface HistoryEntry {
name: string;
date: string;
timestamp: number;
score: number;
grade: string;
}
const RADAR_CENTER = 100;
const RADAR_RADIUS = 82;
const RADAR_ANGLES = [-90, -30, 30, 90, 150, 210];
function getGrade(score: number): string {
if (score >= 97) return "S+";
if (score >= 93) return "S";
if (score >= 88) return "A+";
if (score >= 83) return "A";
if (score >= 78) return "B+";
if (score >= 70) return "B";
return "C";
}
const HISTORY_KEY = "omniai:script-eval-history";
function loadHistory(): HistoryEntry[] {
try {
const raw = localStorage.getItem(HISTORY_KEY);
return raw ? (JSON.parse(raw) as HistoryEntry[]) : [];
} catch { return []; }
}
function saveHistory(entries: HistoryEntry[]) {
try { localStorage.setItem(HISTORY_KEY, JSON.stringify(entries.slice(0, 20))); } catch { /* quota exceeded */ }
}
const SCORE_DIMENSIONS: ScoreDimension[] = [
{ key: "hook", label: "钩子设计", maxScore: 20, hint: "开篇吸引力·悬念设置·黄金三秒", detail: "开篇即抛出高概念钩子,悬念设置紧凑有力。" },
{ key: "character", label: "角色塑造", maxScore: 15, hint: "人物立体度·动机合理性·弧光设计", detail: "主角动机有铺垫,配角功能性较强,人物弧光尚可进一步深化。" },
{ key: "plot", label: "剧情结构", maxScore: 20, hint: "起承转合·节奏把控·冲突设计", detail: "起承转合完整,节奏把控稳健,冲突设计有张力。" },
{ key: "logic", label: "逻辑严密", maxScore: 15, hint: "世界观自洽·伏笔回收·因果链", detail: "世界观整体自洽,伏笔设置到位。" },
{ key: "visual", label: "场景构建", maxScore: 15, hint: "空间描写·视听语言·画面想象力", detail: "视觉意象统一而强烈,场景描写极具画面感。" },
{ key: "content", label: "内容深度", maxScore: 15, hint: "主题表达·情感共鸣·思想内核", detail: "核心设定将科技伦理与人性困境紧密结合,主题表达深刻有力。" },
const scoreDimensions: ScoreDimension[] = [
{
key: "hook",
label: "钩子设计",
maxScore: 20,
weight: 0.2,
description: "开篇吸引力、悬念设置、黄金三秒法则",
},
{
key: "character",
label: "角色塑造",
maxScore: 18,
weight: 0.18,
description: "主角弧光、角色辨识度、动机、配角质量",
},
{
key: "plot",
label: "剧情结构",
maxScore: 20,
weight: 0.2,
description: "起承转合、节奏把控、冲突设计",
},
{
key: "dialogue",
label: "台词对白",
maxScore: 15,
weight: 0.15,
description: "语言质感、角色差异化、潜台词",
},
{
key: "visual",
label: "画面表现",
maxScore: 15,
weight: 0.15,
description: "镜头感、空间层次、视觉冲击力",
},
{
key: "content",
label: "内容深度",
maxScore: 12,
weight: 0.12,
description: "主题表达、情感共鸣、社会/人性洞察",
},
];
function radarPoint(angle: number, radius: number) {
const radians = (angle * Math.PI) / 180;
return {
x: RADAR_CENTER + radius * Math.cos(radians),
y: RADAR_CENTER + radius * Math.sin(radians),
};
}
function makeRadarPoints(scores: Record<string, number> | null) {
if (!scores) return "100,100 100,100 100,100 100,100 100,100 100,100";
return scoreDimensions
.map((dimension, index) => {
const ratio = Math.max(0, Math.min(1, (scores[dimension.key] ?? 0) / dimension.maxScore));
const point = radarPoint(RADAR_ANGLES[index] ?? 0, RADAR_RADIUS * ratio);
return `${point.x.toFixed(1)},${point.y.toFixed(1)}`;
})
.join(" ");
}
function RadarPreview({ result }: { result: EvalResult | null }) {
return (
<div className={`script-eval-v4-radar-container${result ? " has-glow" : ""}`}>
<svg className="script-eval-v4-radar-svg" viewBox="0 0 200 200" aria-hidden="true">
<defs>
<linearGradient id="scriptEvalV4RadarGradient" x1="0%" y1="0%" x2="100%" y2="100%">
<stop offset="0%" stopColor="rgba(0, 255, 136, 0.34)" />
<stop offset="100%" stopColor="rgba(123, 231, 255, 0.1)" />
</linearGradient>
</defs>
<g className="script-eval-v4-radar-grid">
<polygon points="100,15 173,55 173,145 100,185 27,145 27,55" />
<polygon points="100,35 158,68 158,132 100,165 42,132 42,68" />
<polygon points="100,55 143,81 143,119 100,145 57,119 57,81" />
<polygon points="100,75 128,94 128,106 100,125 72,106 72,94" />
<line x1="100" y1="15" x2="100" y2="185" />
<line x1="27" y1="55" x2="173" y2="145" />
<line x1="173" y1="55" x2="27" y2="145" />
</g>
<polygon
className={`script-eval-v4-radar-outline${result ? " has-data" : ""}`}
points={makeRadarPoints(result?.dimensionScores ?? null)}
/>
</svg>
</div>
);
}
function formatReportMarkdown(result: EvalResult, script: string): string {
const lines: string[] = [];
lines.push(`# 剧本评测报告`);
@@ -77,10 +127,10 @@ function formatReportMarkdown(result: EvalResult, script: string): string {
lines.push(result.summary);
lines.push("");
lines.push(`## 六维评分`);
for (const dim of SCORE_DIMENSIONS) {
for (const dim of scoreDimensions) {
const score = result.dimensionScores[dim.key] ?? 0;
const pct = Math.round((score / dim.maxScore) * 100);
lines.push(`- **${dim.label}**: ${score}/${dim.maxScore} (${pct}%) — ${dim.hint}`);
lines.push(`- **${dim.label}**: ${score}/${dim.maxScore} (${pct}%) — ${dim.description}`);
}
if (result.highlights.length > 0) {
lines.push("");
@@ -100,6 +150,13 @@ function formatReportMarkdown(result: EvalResult, script: string): string {
lines.push("");
lines.push(`---`);
lines.push(`*评测时间: ${new Date().toLocaleString("zh-CN")}*`);
lines.push("");
lines.push(`<details><summary>原始剧本 (${script.length} 字)</summary>`);
lines.push("");
lines.push("```");
lines.push(script.slice(0, 2000) + (script.length > 2000 ? "\n...(已截断)" : ""));
lines.push("```");
lines.push("</details>");
return lines.join("\n");
}
@@ -108,82 +165,79 @@ function ScriptTokensPage() {
const [loading, setLoading] = useState(false);
const [result, setResult] = useState<EvalResult | null>(null);
const [evalError, setEvalError] = useState<string | null>(null);
const [detailsExpanded, setDetailsExpanded] = useState(true);
const [uploadedFile, setUploadedFile] = useState<{ name: string; size: number } | null>(null);
const [copied, setCopied] = useState(false);
const [activeDim, setActiveDim] = useState<number | null>(null);
const [animatedScore, setAnimatedScore] = useState(0);
const [history, setHistory] = useState<HistoryEntry[]>(loadHistory);
const fileInputRef = useRef<HTMLInputElement>(null);
const scoreFrameRef = useRef<number | null>(null);
const session = useSessionStore((s) => s.session);
const hasContent = Boolean(script.trim());
// Score animation
useEffect(() => {
if (!result) return;
const start = performance.now();
const target = result.totalScore;
const dur = 1400;
function tick(now: number) {
const t = Math.min((now - start) / dur, 1);
const e = 1 - Math.pow(1 - t, 3);
setAnimatedScore(Math.round(e * target));
if (t < 1) scoreFrameRef.current = requestAnimationFrame(tick);
}
scoreFrameRef.current = requestAnimationFrame(tick);
return () => { if (scoreFrameRef.current) cancelAnimationFrame(scoreFrameRef.current); };
}, [result]);
console.log("[剧本评分] 页面已加载,ScriptTokensPage mounted");
}, []);
const hasContent = Boolean(script.trim());
const lineNumbers = useMemo(() => {
const count = Math.min(160, Math.max(10, script.split(/\r\n|\r|\n/).length));
return Array.from({ length: count }, (_, index) => index + 1);
}, [script]);
const handleUploadKeyDown = (event: KeyboardEvent<HTMLDivElement>) => {
if (event.key !== "Enter" && event.key !== " ") return;
event.preventDefault();
fileInputRef.current?.click();
};
const handleFileUpload = async (event: ChangeEvent<HTMLInputElement>) => {
const file = event.target.files?.[0];
if (!file) return;
const ext = file.name.slice(file.name.lastIndexOf(".")).toLowerCase();
const readable = [".txt", ".md"].includes(ext) || file.type === "text/plain" || file.type === "text/markdown";
setUploadedFile({ name: file.name, size: file.size });
if (readable) {
setScript(await file.text());
} else {
setScript(`[已上传文件:${file.name}]\n\n暂不支持解析 ${ext.toUpperCase()} 格式,请上传 TXT 或 MD 文件。`);
setScript(
`[已上传文件:${file.name}]\n\n暂不支持解析 ${ext.toUpperCase()} 格式,请上传 TXT 或 MD 文件,或直接粘贴剧本文本后开始评测。`,
);
}
event.target.value = "";
};
const handleEvaluate = async () => {
console.log("[剧本评测] 点击开始评测,hasContent:", hasContent, "script长度:", script.length);
if (!hasContent) return;
setLoading(true);
setResult(null);
setEvalError(null);
setAnimatedScore(0);
setActiveDim(null);
try {
console.log("[剧本评测] 开始评测,剧本长度:", script.length, "字符");
const aiResult = await evaluateScript(script);
console.log("[剧本评测] 评测完成,结果:", {
总分: aiResult.totalScore,
等级: aiResult.grade,
维度得分: aiResult.dimensionScores,
摘要: aiResult.summary,
亮点: aiResult.highlights,
问题: aiResult.issues,
建议: aiResult.suggestions,
});
setResult(aiResult);
const g = getGrade(aiResult.totalScore);
const entry: HistoryEntry = {
name: uploadedFile?.name?.replace(/\.[^.]+$/, "") ?? `剧本 ${new Date().toLocaleDateString("zh-CN")}`,
date: new Date().toLocaleDateString("zh-CN", { month: "2-digit", day: "2-digit", hour: "2-digit", minute: "2-digit" }),
timestamp: Date.now(),
score: aiResult.totalScore,
grade: g,
};
const updated = [entry, ...loadHistory().filter((h) => h.name !== entry.name || h.score !== entry.score)];
saveHistory(updated);
setHistory(updated);
} catch (err) {
console.error("[剧本评测] 评测失败:", err);
setEvalError(err instanceof Error ? err.message : "评测服务暂时不可用,请稍后重试");
}
setDetailsExpanded(true);
setLoading(false);
};
const handleReset = () => {
setScript("");
setResult(null);
setEvalError(null);
setDetailsExpanded(true);
setUploadedFile(null);
setCopied(false);
setAnimatedScore(0);
setActiveDim(null);
if (fileInputRef.current) fileInputRef.current.value = "";
};
@@ -222,348 +276,226 @@ function ScriptTokensPage() {
URL.revokeObjectURL(url);
};
const uploadKeyDown = (event: KeyboardEvent<HTMLDivElement>) => {
if (event.key !== "Enter" && event.key !== " ") return;
event.preventDefault();
fileInputRef.current?.click();
};
const grade = result ? getGrade(result.totalScore) : null;
const beatPct = result ? (result.totalScore >= 95 ? 97 : result.totalScore >= 88 ? 92 : result.totalScore >= 80 ? 85 : 72) : 0;
const compactTitle = uploadedFile?.name?.replace(/\.[^.]+$/, "") ?? "剧本评测";
const scoreStatus = loading ? "评测中" : result ? "评测完成" : "待生成评分";
const scoreHint =
result?.summary ??
(hasContent ? "点击「开始评测」生成六维雷达评分和优化路径。" : "粘贴完整剧本后,点击「开始评测」生成六维雷达评分和优化路径。");
return (
<section className="script-eval-v5 page-motion">
<div className="script-eval-v5-page">
{/* Left Panel */}
<aside className="script-eval-v5-left">
<div className="script-eval-v5-lp-section">
<div className="script-eval-v5-lp-label"></div>
<div
className="script-eval-v5-upload-zone"
role="button"
tabIndex={0}
onClick={() => fileInputRef.current?.click()}
onKeyDown={uploadKeyDown}
>
{uploadedFile ? (
<div className="script-eval-v5-upload-done is-show">
<CheckCircleFilled />
<span className="script-eval-v5-uf-name">{uploadedFile.name}</span>
<span className="script-eval-v5-uf-re" onClick={(e) => { e.stopPropagation(); handleReset(); }}>
</span>
<section className="script-token-page script-eval-v4 page-motion">
<main className="script-token-page__scroll script-eval-v4-stage">
<section className="script-eval-v4-app" aria-label="剧本评测工具">
<div className="script-eval-v4-panel-left">
<section className="script-eval-v4-glass script-eval-v4-input-card">
<div
className="script-eval-v4-upload-area"
role="button"
tabIndex={0}
onClick={() => fileInputRef.current?.click()}
onKeyDown={handleUploadKeyDown}
>
<UploadOutlined />
<div className="upload-text">
{uploadedFile ? uploadedFile.name : "粘贴文本或上传文档"}
<div className="hint">
{uploadedFile ? `${(uploadedFile.size / 1024).toFixed(1)}KB,已载入文件信息` : "建议包含场景、角色、动作和台词"}
</div>
</div>
) : (
<>
<div className="script-eval-v5-upload-icon"><UploadOutlined /></div>
<div className="script-eval-v5-upload-text"></div>
<button type="button" className="script-eval-v5-upload-btn" onClick={(e) => { e.stopPropagation(); fileInputRef.current?.click(); }}>
+
</button>
<div className="script-eval-v5-upload-hint"> .txt .md</div>
</>
)}
</div>
<input ref={fileInputRef} type="file" accept=".txt,.md" style={{ display: "none" }} onChange={handleFileUpload} />
</div>
<div className="script-eval-v5-lp-section">
<div className="script-eval-v5-lp-label">AI </div>
<div className="script-eval-v5-info-grid">
{!result ? (
<div className="script-eval-v5-info-empty"></div>
) : (
<>
<div className="script-eval-v5-info-item">
<span className="script-eval-v5-info-key"></span>
<span className="script-eval-v5-info-val"><span className="script-eval-v5-info-tag">{result.totalScore} · {grade}</span></span>
</div>
<div className="script-eval-v5-info-item">
<span className="script-eval-v5-info-key"></span>
<span className="script-eval-v5-info-val">{script.length} </span>
</div>
<div className="script-eval-v5-info-item">
<span className="script-eval-v5-info-key"></span>
<span className="script-eval-v5-info-val">{new Date().toLocaleDateString("zh-CN")}</span>
</div>
<div className="script-eval-v5-info-item">
<span className="script-eval-v5-info-key"></span>
<span className="script-eval-v5-info-val">{beatPct}%</span>
</div>
</>
)}
</div>
</div>
<div className="script-eval-v5-lp-section is-fill">
<div className="script-eval-v5-lp-label"></div>
<div className="script-eval-v5-history-list">
{!session ? (
<div className="script-eval-v5-history-empty"></div>
) : history.length === 0 ? (
<div className="script-eval-v5-history-empty"></div>
) : (
history.map((item, i) => (
<div key={i} className={`script-eval-v5-history-item${i === 0 ? " is-active" : ""}`}>
<div className="script-eval-v5-hi-left">
<div className="script-eval-v5-hi-name">{item.name}</div>
<div className="script-eval-v5-hi-date">{item.date}</div>
<div className="script-eval-v5-hi-bar">
<div className="script-eval-v5-hi-bar-fill" style={{ width: `${Math.min(92, (item.score / 100) * 100)}%` }} />
</div>
</div>
<div className="script-eval-v5-hi-right">
<div className={`script-eval-v5-hi-score${item.score >= 90 ? " is-green" : ""}`}>{item.score}</div>
<div className="script-eval-v5-hi-grade">{item.grade}</div>
</div>
</div>
))
)}
</div>
</div>
<div className="script-eval-v5-lp-bottom">
<button
type="button"
className="script-eval-v5-eval-btn"
disabled={loading || !hasContent}
onClick={() => void handleEvaluate()}
>
{loading ? "◆ 评测中..." : "◆ 开始评测"}
</button>
<button type="button" className="script-eval-v5-export-btn" disabled={!result} onClick={handleExportMarkdown}>
</button>
</div>
</aside>
{/* Right Area */}
<div className="script-eval-v5-right">
<div className="script-eval-v5-right-topbar">
<div className="script-eval-v5-right-title">
<span className="script-eval-v5-rt-green"></span>
{uploadedFile && <> · {compactTitle}</>}
</div>
<div className="script-eval-v5-right-actions">
{result && (
<>
<button type="button" className="script-eval-v5-action-btn" onClick={() => void handleCopyReport()}>
<CopyOutlined />{copied ? "已复制" : "复制"}
</button>
<button type="button" className="script-eval-v5-action-btn" onClick={handleExportMarkdown}>
<DownloadOutlined />
</button>
</>
)}
</div>
</div>
<div className="script-eval-v5-right-content">
{!result && (
<div className="script-eval-v5-input-section">
{/* Script-themed upload illustration */}
<div
className="script-eval-v5-illustration"
role="button"
tabIndex={0}
onClick={() => fileInputRef.current?.click()}
onKeyDown={uploadKeyDown}
<input ref={fileInputRef} type="file" accept=".txt,.md,.pdf,.doc,.docx" onChange={handleFileUpload} />
<button
type="button"
className="script-eval-v4-upload-btn"
onClick={(event) => {
event.stopPropagation();
fileInputRef.current?.click();
}}
>
<div className="script-eval-v5-illust-grid">
{[0, 1, 2, 3, 4, 5].map((idx) => (
<div key={idx} className={`script-eval-v5-illust-page${idx === 1 ? " is-active" : ""}`}>
<div className="script-eval-v5-illust-page-lines">
<div className="script-eval-v5-illust-line" style={{ width: `${60 + Math.sin(idx * 1.2) * 20}%` }} />
<div className="script-eval-v5-illust-line" style={{ width: `${75 + Math.cos(idx * 1.7) * 15}%` }} />
<div className="script-eval-v5-illust-line" style={{ width: `${45 + Math.sin(idx * 2.1) * 25}%` }} />
<div className="script-eval-v5-illust-line" style={{ width: `${65 + Math.cos(idx * 1.3) * 20}%` }} />
<div className="script-eval-v5-illust-line is-short" style={{ width: `${35 + Math.sin(idx * 0.8) * 15}%` }} />
</div>
</div>
))}
</div>
<div className="script-eval-v5-illust-label">
<FileTextOutlined />
<span></span>
</div>
<div className="script-eval-v5-illust-hint"> TXT / MD </div>
</div>
</button>
</div>
<div className="script-eval-v5-textarea-shell">
<textarea
className="script-eval-v5-textarea"
value={script}
onChange={(e) => setScript(e.target.value)}
placeholder={"或直接在此粘贴剧本内容...\n\n【第一幕】夜晚,城市天台。霓虹灯映照着雨后的地面。\n小凯独自站在天台边缘,手中握着一张皱巴巴的纸条..."}
/>
<div className="script-eval-v4-text-shell">
<div className="script-eval-v4-line-numbers" aria-hidden="true">
{lineNumbers.map((line) => (
<span key={line}>{line}</span>
))}
</div>
{evalError && (
<div className="script-eval-v5-error" role="alert">
<span></span><span>{evalError}</span>
<textarea
className="script-eval-v4-text-input"
value={script}
onChange={(event) => setScript(event.target.value)}
placeholder={
"在此粘贴你的剧本内容...\n\n【第一幕】夜晚,城市天台。霓虹灯映照着雨后的地面。\n小凯独自站在天台边缘,手中握着一张皱巴巴的纸条..."
}
/>
</div>
<div className="script-eval-v4-button-group">
<button
type="button"
className="script-eval-v4-btn-primary"
disabled={loading || !hasContent}
onClick={() => void handleEvaluate()}
>
<span>{loading ? "评测中..." : "开始评测"}</span>
</button>
<button type="button" className="script-eval-v4-btn-secondary" onClick={handleReset}>
<ReloadOutlined />
</button>
</div>
</section>
</div>
<aside className="script-eval-v4-panel-right" aria-label="评分结果">
{evalError ? (
<div className="script-eval-v4-error" role="alert">
<span className="script-eval-v4-error__icon"></span>
<span>{evalError}</span>
</div>
) : null}
<section className={`script-eval-v4-glass script-eval-v4-score-card${loading ? " loading" : ""}${result ? " ready" : ""}`}>
<div className="script-eval-v4-score-header">
<div className="score-title">SCORE BOARD</div>
<span className={`score-status${result ? " ready" : ""}`}>{scoreStatus}</span>
</div>
<div className="script-eval-v4-score-main">
<RadarPreview result={result} />
<div className="script-eval-v4-score-display">
<div className={`score-number${result ? " has-data" : ""}`}>
{result ? (
<>
{result.totalScore} <span>/ 100</span>
</>
) : (
"— / 100"
)}
</div>
<div className="score-label"> {result ? `· ${result.grade}` : ""}</div>
<div className="score-hint">{scoreHint}</div>
</div>
</div>
<div className="script-eval-v4-dimensions-tags">
{scoreDimensions.map((dimension) => (
<span className="tag" key={dimension.key}>
{dimension.label}
</span>
))}
</div>
</section>
<section className="script-eval-v4-glass script-eval-v4-details-card">
<button
type="button"
className="script-eval-v4-details-header"
onClick={() => setDetailsExpanded((expanded) => !expanded)}
aria-expanded={detailsExpanded}
>
<span className="details-title">
<TrophyOutlined />
DIMENSIONS
</span>
<DownOutlined className={`expand-icon${detailsExpanded ? " expanded" : ""}`} />
</button>
<div className={`script-eval-v4-details-content${detailsExpanded ? " expanded" : ""}`}>
<div className="script-eval-v4-details-list">
{scoreDimensions.map((dimension) => {
const score = result?.dimensionScores[dimension.key] ?? 0;
const pct = result ? Math.round((score / dimension.maxScore) * 100) : 0;
return (
<article className="script-eval-v4-detail-row" key={dimension.key}>
<div className="detail-row-main">
<span className="dimension-name">{dimension.label}</span>
<div className="dimension-bar" aria-hidden="true">
<span className="dimension-bar-fill" style={{ width: `${pct}%` }} />
</div>
<span className="dimension-score">{result ? `${score}/${dimension.maxScore}` : `${dimension.maxScore}`}</span>
</div>
<div className="dimension-desc">{dimension.description}</div>
</article>
);
})}
</div>
</div>
</section>
{result && (result.highlights.length > 0 || result.issues.length > 0) && (
<section className="script-eval-v4-glass script-eval-v4-insights-card">
{result.highlights.length > 0 && (
<div className="script-eval-v4-insight-group highlights">
<div className="insight-group-title">
<span className="insight-icon"></span>
HIGHLIGHTS
</div>
<ul className="insight-list">
{result.highlights.map((h, i) => (
<li key={i} className="insight-item highlight-item">{h}</li>
))}
</ul>
</div>
)}
</div>
{result.issues.length > 0 && (
<div className="script-eval-v4-insight-group issues">
<div className="insight-group-title">
<span className="insight-icon"></span>
ISSUES
</div>
<ul className="insight-list">
{result.issues.map((issue, i) => (
<li key={i} className="insight-item issue-item">{issue}</li>
))}
</ul>
</div>
)}
</section>
)}
{result && result.suggestions.length > 0 && (
<section className="script-eval-v4-glass script-eval-v4-suggestions-card">
<div className="suggestions-header">
<span className="suggestions-title">
<span className="insight-icon"></span>
SUGGESTIONS
</span>
</div>
<ul className="suggestion-list">
{result.suggestions.map((s, i) => (
<li key={i} className="suggestion-item">
<span className="suggestion-index">{i + 1}</span>
<span className="suggestion-text">{s}</span>
</li>
))}
</ul>
</section>
)}
{result && (
<>
<div className="script-eval-v5-hero">
<div className="script-eval-v5-hero-top">
<span className="script-eval-v5-hero-num">{animatedScore}</span>
<span className="script-eval-v5-hero-total">/ 100</span>
<div className="script-eval-v5-hero-grade">
<span className="script-eval-v5-hero-grade-dot" />
<span>{grade}</span>
</div>
</div>
<div className="script-eval-v5-hero-bar">
<div className="script-eval-v5-hero-bar-fill" style={{ width: `${animatedScore}%` }} />
</div>
<div className="script-eval-v5-hero-beat"> <b>{beatPct}%</b> </div>
<div className="script-eval-v5-hero-title">{compactTitle}</div>
<div className="script-eval-v5-hero-desc">{result.summary}</div>
</div>
<div className="script-eval-v5-card">
<div className="script-eval-v5-card-head">
<div className="script-eval-v5-card-head-left">
<div className="script-eval-v5-ch-dot" />
<div className="script-eval-v5-ch-title"></div>
</div>
<div className="script-eval-v5-ch-legend">
<div className="script-eval-v5-leg"><div className="script-eval-v5-ldot is-score" /></div>
<div className="script-eval-v5-leg"><div className="script-eval-v5-ldot is-loss" /></div>
</div>
</div>
<div className="script-eval-v5-card-body">
<div className="script-eval-v5-chart-container">
<div className="script-eval-v5-chart-bars">
{SCORE_DIMENSIONS.map((dim, i) => {
const score = result.dimensionScores[dim.key] ?? 0;
const pct = score / dim.maxScore;
const lossPct = (dim.maxScore - score) / dim.maxScore;
const isPerfect = score === dim.maxScore;
return (
<div
key={dim.key}
className={`script-eval-v5-bcol${activeDim === i ? " is-active" : ""}${activeDim !== null && activeDim !== i ? " is-dimmed" : ""}`}
onClick={() => setActiveDim(activeDim === i ? null : i)}
>
<div className="script-eval-v5-bbar-area">
{lossPct > 0 && (
<div className="script-eval-v5-bseg is-loss" style={{ height: `${lossPct * 80}%`, transitionDelay: `${i * 80}ms` }} />
)}
<div className={`script-eval-v5-bseg is-score${isPerfect ? " is-perfect" : ""}`} style={{ height: `${pct * 80}%`, transitionDelay: `${i * 80}ms` }} />
</div>
<div className="script-eval-v5-bscore-label">
{score}<span className="script-eval-v5-bmax">/{dim.maxScore}</span>
{isPerfect && <span className="script-eval-v5-bstar"> </span>}
</div>
</div>
);
})}
</div>
<div className="script-eval-v5-chart-bottom">
<div className="script-eval-v5-chart-dims">
{SCORE_DIMENSIONS.map((dim, i) => (
<div
key={dim.key}
className={`script-eval-v5-chart-dim${activeDim === i ? " is-active" : ""}${activeDim !== null && activeDim !== i ? " is-dimmed" : ""}`}
onClick={() => setActiveDim(activeDim === i ? null : i)}
>
<div className="script-eval-v5-chart-dim-name">{dim.label}</div>
<div className="script-eval-v5-chart-dim-hint">{dim.hint}</div>
</div>
))}
</div>
</div>
</div>
{activeDim !== null && (() => {
const d = SCORE_DIMENSIONS[activeDim]!;
const s = result.dimensionScores[d.key] ?? 0;
return (
<div className="script-eval-v5-dim-overlay is-open">
<button className="script-eval-v5-dim-overlay-close" onClick={() => setActiveDim(null)}></button>
<div className="script-eval-v5-do-inner">
<div className="script-eval-v5-do-left">
<div className="script-eval-v5-do-name">{d.label}</div>
<div className="script-eval-v5-do-score">{s}<span className="script-eval-v5-do-max">/{d.maxScore}</span></div>
<div className="script-eval-v5-do-bar"><div className="script-eval-v5-do-bar-fill" style={{ width: `${Math.round(s / d.maxScore * 100)}%` }} /></div>
<div className="script-eval-v5-do-hint">{d.hint}</div>
</div>
<div className="script-eval-v5-do-right"><div className="script-eval-v5-do-detail">{d.detail}</div></div>
</div>
</div>
);
})()}
</div>
</div>
{(result.highlights.length > 0 || result.issues.length > 0) && (
<div className="script-eval-v5-findings">
{result.highlights.length > 0 && (
<div className="script-eval-v5-find-group">
<div className="script-eval-v5-find-group-label is-green">
<span className="script-eval-v5-fg-count">{result.highlights.length}</span>
</div>
<div className="script-eval-v5-fi-list">
{result.highlights.map((h, i) => (
<div key={i} className="script-eval-v5-fi-item is-highlight"><div className="script-eval-v5-fi-marker" /><div>{h}</div></div>
))}
</div>
</div>
)}
{result.issues.length > 0 && (
<div className="script-eval-v5-find-group">
<div className="script-eval-v5-find-group-label is-orange">
<span className="script-eval-v5-fg-count">{result.issues.length}</span>
</div>
<div className="script-eval-v5-fi-list">
{result.issues.map((issue, i) => (
<div key={i} className="script-eval-v5-fi-item is-issue"><div className="script-eval-v5-fi-marker" /><div>{issue}</div></div>
))}
</div>
</div>
)}
</div>
)}
{result.suggestions.length > 0 && (
<div className="script-eval-v5-card">
<div className="script-eval-v5-card-head">
<div className="script-eval-v5-card-head-left"><div className="script-eval-v5-ch-dot" /><div className="script-eval-v5-ch-title"></div></div>
</div>
<div className="script-eval-v5-card-body">
<table className="script-eval-v5-sug-table">
<thead><tr><th style={{ width: 60 }}></th><th style={{ width: 68 }}></th><th></th></tr></thead>
<tbody>
{result.suggestions.map((s, i) => {
const isHigh = i < 2;
return (
<tr key={i} className={isHigh ? "is-high" : "is-mid"}>
<td><span className={`script-eval-v5-sug-priority${isHigh ? " is-high" : " is-mid"}`}>{isHigh ? "HIGH" : "MID"}</span></td>
<td><div className="script-eval-v5-sug-type">{isHigh ? "核心" : "增强"}</div></td>
<td>{s}</td>
</tr>
);
})}
</tbody>
</table>
</div>
</div>
)}
</>
<div className="script-eval-v4-report-actions">
<button type="button" className="script-eval-v4-report-btn" onClick={() => void handleCopyReport()}>
<CopyOutlined />
<span>{copied ? "已复制" : "复制报告"}</span>
</button>
<button type="button" className="script-eval-v4-report-btn" onClick={handleExportMarkdown}>
<DownloadOutlined />
<span> Markdown</span>
</button>
</div>
)}
</div>
<div className="script-eval-v5-statusbar">
<div className="script-eval-v5-status-dot" />
<span>{loading ? "评测中..." : result ? "评测完成" : hasContent ? "待评测" : "等待上传"}</span>
<span className="script-eval-v5-sb-right">{result ? `六维标准 · ${result.totalScore}` : "六维标准"}</span>
</div>
</div>
</div>
{!result && (
<section className="script-eval-v4-note">
<FileTextOutlined />
<span></span>
</section>
)}
</aside>
</section>
</main>
</section>
);
}
-3
View File
@@ -5,8 +5,6 @@
@import "./components/legacy-components.css";
@import "./pages/home.css";
@import "./pages/welcome-splash.css";
@import "./pages/toolbox.css";
@import "./pages/script-review-visual.css";
@import "./pages/workbench.css";
@import "./pages/ecommerce.css";
@import "./pages/ecommerce-video.css";
@@ -19,7 +17,6 @@
@import "./pages/image-workbench.css";
@import "./pages/subtitle-removal.css";
@import "./pages/size-template.css";
@import "./pages/script-tokens-v5.css";
@import "./pages/script-tokens.css";
@import "./pages/profile.css";
@import "./pages/canvas.css";
+62 -17
View File
@@ -148,37 +148,83 @@
min-width: 0;
min-height: 72px;
padding: 0 28px;
border: 1px solid var(--border-subtle);
border-radius: 8px;
background: var(--bg-inset);
border: 1px solid rgba(255, 255, 255, 0.08);
border-radius: 12px;
background: linear-gradient(180deg, rgba(20, 23, 26, 0.72) 0%, rgba(15, 17, 19, 0.84) 100%);
backdrop-filter: blur(12px);
-webkit-backdrop-filter: blur(12px);
box-shadow:
0 1px 0 rgba(255, 255, 255, 0.04) inset,
0 2px 8px rgba(0, 0, 0, 0.28);
color: var(--fg-body);
cursor: pointer;
font-size: 17px;
font-weight: 850;
transition: border-color 160ms ease, background 160ms ease, color 160ms ease, transform 160ms ease;
font-size: 16px;
font-weight: 700;
letter-spacing: 0.03em;
transition:
border-color 240ms ease,
background 240ms ease,
color 240ms ease,
transform 240ms cubic-bezier(0.34, 1.2, 0.64, 1),
box-shadow 240ms ease;
}
.omni-home__entry .anticon {
font-size: 18px;
font-size: 19px;
transition: color 240ms ease, transform 240ms ease;
}
.omni-home__entry:hover {
border-color: var(--border-default);
background: var(--bg-hover);
border-color: rgba(255, 255, 255, 0.16);
background: linear-gradient(180deg, rgba(28, 32, 36, 0.78) 0%, rgba(18, 22, 25, 0.88) 100%);
box-shadow:
0 1px 0 rgba(255, 255, 255, 0.06) inset,
0 0 24px rgba(var(--accent-rgb), 0.06),
0 4px 16px rgba(0, 0, 0, 0.36);
color: #ffffff;
transform: translateY(-1px);
transform: translateY(-2px);
}
.omni-home__entry:hover .anticon {
color: var(--accent);
transform: scale(1.08);
}
.omni-home__entry:active {
transform: translateY(0) scale(0.97);
box-shadow:
0 1px 0 rgba(255, 255, 255, 0.02) inset,
0 1px 4px rgba(0, 0, 0, 0.32);
transition-duration: 80ms;
}
.omni-home__entry--primary {
border-color: var(--accent);
background: var(--accent);
color: var(--dg-button-text, #061014);
border-color: rgba(var(--accent-rgb), 0.48);
background: linear-gradient(180deg, rgba(0, 255, 136, 0.22) 0%, rgba(0, 220, 118, 0.14) 100%), var(--accent);
box-shadow:
0 1px 0 rgba(255, 255, 255, 0.12) inset,
0 0 28px rgba(var(--accent-rgb), 0.18),
0 2px 12px rgba(0, 0, 0, 0.28);
color: #061014;
}
.omni-home__entry--primary:hover {
border-color: var(--accent-hover, var(--accent));
background: var(--accent-hover, var(--accent));
color: var(--dg-button-text, #061014);
border-color: rgba(var(--accent-rgb), 0.64);
background: linear-gradient(180deg, rgba(0, 255, 136, 0.28) 0%, rgba(0, 230, 124, 0.18) 100%), var(--accent-hover);
box-shadow:
0 1px 0 rgba(255, 255, 255, 0.16) inset,
0 0 40px rgba(var(--accent-rgb), 0.28),
0 6px 24px rgba(0, 0, 0, 0.36);
color: #061014;
}
.omni-home__entry--primary .anticon {
color: #061014;
}
.omni-home__entry--primary:hover .anticon {
color: #061014;
transform: scale(1.12);
}
.omni-home__carousel {
@@ -572,7 +618,6 @@
transform-origin: center;
}
.omni-home__feature-stats {
position: absolute;
right: clamp(22px, 7vw, 92px);
-264
View File
@@ -1,264 +0,0 @@
/* ===== 剧本评测展示 ===== */
.omni-script-review-visual {
display: flex;
flex-direction: column;
gap: 16px;
width: 100%;
height: 100%;
padding: clamp(14px, 2vw, 24px);
justify-content: center;
}
/* Hero */
.omni-script-review-hero {
display: flex;
flex-direction: column;
gap: 6px;
}
.omni-script-review-score-row {
display: flex;
align-items: flex-end;
gap: 4px;
}
.omni-script-review-num {
font-size: clamp(36px, 5vw, 56px);
font-weight: 800;
color: var(--accent);
line-height: 1;
letter-spacing: -2px;
}
.omni-script-review-total {
font-size: 14px;
color: rgb(255 255 255 / 30%);
font-weight: 400;
margin-bottom: 6px;
}
.omni-script-review-grade {
display: inline-flex;
align-items: center;
gap: 5px;
margin-left: 12px;
margin-bottom: 8px;
padding: 2px 10px;
border-radius: 4px;
background: rgba(0, 255, 136, 0.08);
border: 1px solid rgba(0, 255, 136, 0.2);
font-size: 12px;
font-weight: 600;
color: var(--accent);
}
.omni-script-review-grade-dot {
width: 6px;
height: 6px;
border-radius: 50%;
background: var(--accent);
animation: omni-sr-pulse 2s ease infinite;
}
@keyframes omni-sr-pulse {
0%, 100% { opacity: 1; }
50% { opacity: 0.4; }
}
.omni-script-review-bar {
width: 100%;
max-width: 320px;
height: 3px;
border-radius: 2px;
background: rgba(255, 255, 255, 0.08);
overflow: hidden;
}
.omni-script-review-bar-fill {
height: 100%;
border-radius: 2px;
background: var(--accent);
transition: width 1.4s ease;
}
.omni-script-review-beat {
font-size: 11px;
color: rgb(255 255 255 / 30%);
}
.omni-script-review-beat b {
color: var(--accent);
font-weight: 600;
}
/* Chart */
.omni-script-review-chart {
display: flex;
flex-direction: column;
gap: 10px;
flex: 1;
min-height: 0;
}
.omni-script-review-chart-bars {
display: flex;
gap: 10px;
flex: 1;
align-items: flex-end;
min-height: 0;
}
.omni-script-review-bcol {
flex: 1;
display: flex;
flex-direction: column;
align-items: center;
gap: 6px;
cursor: pointer;
min-height: 0;
}
.omni-script-review-bbar-area {
flex: 1;
width: 100%;
display: flex;
flex-direction: column;
justify-content: flex-end;
align-items: center;
min-height: 0;
position: relative;
}
.omni-script-review-bseg {
width: 70%;
min-height: 0;
transition: height 1s cubic-bezier(0.4, 0, 0.2, 1), filter 0.25s, opacity 0.25s;
}
.omni-script-review-bseg.is-score {
background: linear-gradient(180deg, #33ffaa, var(--accent) 40%, #00cc6a);
border-radius: 5px 5px 2px 2px;
position: relative;
overflow: hidden;
}
.omni-script-review-bseg.is-score::after {
content: '';
position: absolute;
top: 0;
left: 0;
right: 50%;
bottom: 0;
background: linear-gradient(90deg, rgba(255, 255, 255, 0.1), transparent);
border-radius: 5px 0 0 0;
pointer-events: none;
}
.omni-script-review-bseg.is-perfect {
border-radius: 5px;
}
.omni-script-review-bseg.is-loss {
background: rgba(255, 255, 255, 0.04);
border: 1px dashed rgba(255, 255, 255, 0.1);
border-bottom: none;
border-radius: 2px 2px 0 0;
}
.omni-script-review-bcol:hover .omni-script-review-bseg.is-score {
filter: brightness(1.15);
box-shadow: 0 0 10px rgba(0, 255, 136, 0.15);
}
.omni-script-review-bcol.is-active .omni-script-review-bseg.is-score {
filter: brightness(1.25);
box-shadow: 0 0 14px rgba(0, 255, 136, 0.25);
}
.omni-script-review-bcol.is-dimmed .omni-script-review-bseg {
opacity: 0.2;
}
.omni-script-review-blabel {
text-align: center;
}
.omni-script-review-blabel span {
font-size: clamp(8px, 0.9vw, 10px);
font-weight: 600;
color: rgb(255 255 255 / 55%);
white-space: nowrap;
}
.omni-script-review-bcol:hover .omni-script-review-blabel span,
.omni-script-review-bcol.is-active .omni-script-review-blabel span {
color: var(--accent);
}
.omni-script-review-diminfo {
display: flex;
align-items: center;
gap: 8px;
padding: 4px 0;
animation: omni-sr-fadeUp 0.25s ease;
}
@keyframes omni-sr-fadeUp {
from { opacity: 0; transform: translateY(4px); }
to { opacity: 1; transform: translateY(0); }
}
.omni-script-review-diminfo-name {
font-size: 12px;
font-weight: 700;
color: #fff;
}
.omni-script-review-diminfo-score {
font-size: 22px;
font-weight: 800;
color: var(--accent);
line-height: 1;
}
.omni-script-review-diminfo-score small {
font-size: 12px;
color: rgb(255 255 255 / 30%);
font-weight: 400;
}
.omni-script-review-legend {
display: flex;
gap: 14px;
justify-content: flex-end;
font-size: 9px;
color: rgb(255 255 255 / 30%);
}
.omni-script-review-legend-dot {
display: inline-block;
width: 6px;
height: 6px;
border-radius: 2px;
vertical-align: middle;
margin-right: 2px;
}
.omni-script-review-legend-dot.is-score {
background: var(--accent);
}
.omni-script-review-legend-dot.is-loss {
background: rgba(255, 255, 255, 0.04);
border: 1px dashed rgba(255, 255, 255, 0.15);
}
@media (max-width: 560px) {
.omni-script-review-chart-bars {
gap: 6px;
}
.omni-script-review-bseg {
width: 80%;
}
}
File diff suppressed because it is too large Load Diff
+7
View File
@@ -3400,6 +3400,7 @@
width: 100%;
height: 100%;
min-height: 520px;
max-height: 520px;
padding: 18px 22px;
border: none;
outline: none;
@@ -3409,6 +3410,7 @@
font-size: 14px;
line-height: 1.9;
resize: none;
overflow-y: auto;
}
.script-eval-v4-text-input::placeholder {
@@ -4268,6 +4270,11 @@
.script-eval-v4-text-shell,
.script-eval-v4-text-input {
min-height: calc(100vh - 422px);
max-height: calc(100vh - 422px);
}
.script-eval-v4-text-input {
overflow-y: auto;
}
.script-eval-v4-score-card {
-884
View File
@@ -1,884 +0,0 @@
/* ===== 工具箱功能页 ===== */
.omni-home__toolbox-page {
--toolbox-green: #00ff88;
--toolbox-blue: #4fc3f7;
--toolbox-purple: #a855f7;
--toolbox-surface: rgba(14, 16, 38, 0.75);
--toolbox-elevated: rgba(20, 22, 52, 0.85);
--toolbox-highlight: rgba(28, 31, 68, 0.9);
--toolbox-border-subtle: rgba(0, 255, 136, 0.08);
--toolbox-border-default: rgba(0, 255, 136, 0.14);
--toolbox-border-hover: rgba(0, 255, 136, 0.28);
--toolbox-text-primary: #f0f0f5;
--toolbox-text-secondary: rgba(240, 240, 245, 0.6);
--toolbox-text-tertiary: rgba(240, 240, 245, 0.4);
position: relative;
isolation: isolate;
min-height: var(--home-section-min-height);
border-top: 1px solid rgb(255 255 255 / 8%);
background:
linear-gradient(180deg, #070b10 0%, #05080d 100%),
radial-gradient(ellipse 80% 60% at 50% 40%, rgba(0, 255, 136, 0.04) 0%, transparent 70%),
radial-gradient(ellipse 60% 50% at 80% 70%, rgba(79, 195, 247, 0.03) 0%, transparent 60%),
radial-gradient(ellipse 50% 40% at 20% 80%, rgba(168, 85, 247, 0.03) 0%, transparent 60%);
scroll-snap-align: start;
scroll-snap-stop: normal;
}
.omni-home__toolbox-shell {
position: relative;
z-index: 2;
display: flex;
gap: clamp(20px, 3vw, 40px);
padding: clamp(42px, 6vw, 82px) clamp(22px, 7vw, 92px);
min-height: var(--home-section-min-height);
align-items: center;
}
/* ===== Left Panel ===== */
.omni-home__toolbox-left {
width: clamp(340px, 30vw, 440px);
flex-shrink: 0;
display: flex;
flex-direction: column;
gap: 16px;
justify-content: flex-start;
padding-top: clamp(40px, 8vh, 100px);
}
.omni-home__toolbox-brand {
display: flex;
align-items: center;
gap: 12px;
}
.omni-home__toolbox-brand-icon {
width: 52px;
height: 52px;
background: var(--toolbox-green);
border-radius: 14px;
display: flex;
align-items: center;
justify-content: center;
color: #0a0b12;
font-size: 26px;
}
.omni-home__toolbox-brand-icon .anticon {
font-size: 28px;
}
.omni-home__toolbox-brand-text {
font-weight: 900;
font-size: 30px;
color: #fff;
letter-spacing: -0.5px;
}
.omni-home__toolbox-title {
font-weight: 900;
font-size: clamp(34px, 3.6vw, 46px);
line-height: 1.15;
background: linear-gradient(135deg, var(--toolbox-green), var(--toolbox-blue));
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
}
.omni-home__toolbox-subtitle {
font-size: 17px;
line-height: 1.6;
color: var(--toolbox-text-secondary);
}
.omni-home__toolbox-list {
display: flex;
flex-direction: column;
gap: 10px;
margin-top: 8px;
}
.omni-home__toolbox-item {
display: flex;
align-items: flex-start;
gap: 16px;
padding: 18px 22px;
border-radius: 16px;
background: var(--toolbox-surface);
border: 1px solid var(--toolbox-border-subtle);
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
cursor: pointer;
animation: omni-toolbox-fadeSlideIn 0.6s ease both;
}
.omni-home__toolbox-item:nth-child(1) { animation-delay: 0.1s; }
.omni-home__toolbox-item:nth-child(2) { animation-delay: 0.2s; }
.omni-home__toolbox-item:nth-child(3) { animation-delay: 0.3s; }
.omni-home__toolbox-item:nth-child(4) { animation-delay: 0.4s; }
.omni-home__toolbox-item:hover {
border-color: var(--toolbox-border-hover);
transform: translateX(4px);
background: var(--toolbox-elevated);
}
.omni-home__toolbox-item-icon {
font-size: 28px;
flex-shrink: 0;
width: 48px;
height: 48px;
display: flex;
align-items: center;
justify-content: center;
border-radius: 12px;
background: rgba(0, 255, 136, 0.08);
}
.omni-home__toolbox-item-info {
display: flex;
flex-direction: column;
gap: 5px;
}
.omni-home__toolbox-item-name {
font-weight: 700;
font-size: 17px;
color: var(--toolbox-text-primary);
}
.omni-home__toolbox-item-desc {
font-size: 14px;
color: var(--toolbox-text-tertiary);
line-height: 1.5;
}
@keyframes omni-toolbox-fadeSlideIn {
from { opacity: 0; transform: translateX(-12px); }
to { opacity: 1; transform: translateX(0); }
}
.omni-home__toolbox-workflow {
margin-top: auto;
padding: 20px 24px;
border-radius: 16px;
background: var(--toolbox-surface);
border: 1px solid var(--toolbox-border-subtle);
}
.omni-home__toolbox-workflow-label {
font-size: 14px;
font-weight: 700;
color: var(--toolbox-green);
margin-bottom: 12px;
letter-spacing: 0.5px;
text-transform: uppercase;
}
.omni-home__toolbox-workflow-steps {
display: flex;
align-items: center;
gap: 10px;
font-size: 14px;
color: var(--toolbox-text-tertiary);
}
.omni-home__toolbox-workflow-step {
color: var(--toolbox-text-secondary);
}
.omni-home__toolbox-workflow-arrow {
color: var(--toolbox-green);
font-size: 14px;
}
/* ===== Grid Area ===== */
.omni-home__toolbox-grid {
flex: 1;
display: grid;
grid-template-columns: 1fr 1fr;
grid-template-rows: 1fr 1fr;
gap: 16px;
min-height: clamp(360px, 40vw, 520px);
}
/* ===== Tool Cards ===== */
.omni-home__toolbox-card {
position: relative;
border-radius: 18px;
background: var(--toolbox-elevated);
border: 1px solid var(--toolbox-border-default);
backdrop-filter: blur(20px);
overflow: hidden;
transition: all 0.4s cubic-bezier(0.4, 0, 0.2, 1);
display: flex;
flex-direction: column;
cursor: pointer;
animation: omni-toolbox-cardIn 0.7s ease both;
}
.omni-home__toolbox-card:nth-child(1) { animation-delay: 0.15s; }
.omni-home__toolbox-card:nth-child(2) { animation-delay: 0.25s; }
.omni-home__toolbox-card:nth-child(3) { animation-delay: 0.35s; }
.omni-home__toolbox-card:nth-child(4) { animation-delay: 0.45s; }
.omni-home__toolbox-card:hover {
transform: translateY(-6px) scale(1.01);
border-color: var(--toolbox-border-hover);
box-shadow:
0 12px 40px rgba(0, 255, 136, 0.08),
0 0 60px rgba(0, 255, 136, 0.04);
}
@keyframes omni-toolbox-cardIn {
from { opacity: 0; transform: translateY(20px) scale(0.97); }
to { opacity: 1; transform: translateY(0) scale(1); }
}
.omni-home__toolbox-card-header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 14px 18px 0;
}
.omni-home__toolbox-card-header-left {
display: flex;
align-items: center;
gap: 10px;
}
.omni-home__toolbox-card-icon {
width: 32px;
height: 32px;
border-radius: 9px;
display: flex;
align-items: center;
justify-content: center;
font-size: 16px;
background: rgba(0, 255, 136, 0.1);
border: 1px solid rgba(0, 255, 136, 0.12);
}
.omni-home__toolbox-card-title {
font-weight: 900;
font-size: 14px;
color: var(--toolbox-text-primary);
}
.omni-home__toolbox-card-tag {
padding: 3px 10px;
border-radius: 20px;
font-size: 10px;
font-weight: 700;
color: var(--toolbox-green);
background: rgba(0, 255, 136, 0.1);
border: 1px solid rgba(0, 255, 136, 0.2);
letter-spacing: 0.3px;
}
.omni-home__toolbox-card-content {
flex: 1;
display: flex;
align-items: center;
justify-content: center;
padding: 10px 18px;
}
.omni-home__toolbox-card-footer {
padding: 8px 18px 12px;
display: flex;
align-items: center;
gap: 6px;
flex-wrap: wrap;
}
.omni-home__toolbox-card-feat {
padding: 2px 8px;
border-radius: 4px;
font-size: 10px;
color: var(--toolbox-text-tertiary);
background: rgba(255, 255, 255, 0.04);
}
.omni-home__toolbox-card-feat-sep {
color: rgba(0, 255, 136, 0.2);
font-size: 10px;
}
/* === Card 1: 图片工作室 === */
.toolbox-card1-content {
width: 100%;
height: 100%;
display: flex;
gap: 0;
position: relative;
}
.toolbox-card1-side {
flex: 1;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
border-radius: 10px;
position: relative;
overflow: hidden;
padding: 6px;
}
.toolbox-card1-left {
background: rgba(255, 255, 255, 0.02);
margin-right: 1px;
}
.toolbox-card1-right {
background: rgba(0, 255, 136, 0.02);
margin-left: 1px;
}
.toolbox-card1-img {
width: 100%;
flex: 1;
border-radius: 8px;
position: relative;
overflow: hidden;
}
.toolbox-card1-img img {
width: 100%;
height: 100%;
object-fit: cover;
display: block;
border-radius: 8px;
}
.toolbox-card1-left .toolbox-card1-img {
box-shadow: inset 0 0 20px rgba(0, 0, 0, 0.2);
}
.toolbox-card1-right .toolbox-card1-img {
box-shadow: 0 0 12px rgba(0, 255, 136, 0.06);
}
.toolbox-card1-label {
font-size: 10px;
color: var(--toolbox-text-tertiary);
margin-top: 6px;
font-weight: 700;
letter-spacing: 0.5px;
}
.toolbox-card1-left .toolbox-card1-label {
color: rgba(255, 255, 255, 0.35);
}
.toolbox-card1-right .toolbox-card1-label {
color: rgba(0, 255, 136, 0.5);
}
.toolbox-card1-divider {
width: 1px;
background: linear-gradient(to bottom, transparent, rgba(0, 255, 136, 0.3), transparent);
position: absolute;
left: 50%;
top: 8%;
height: 84%;
}
/* === Card 2: 镜头实验室 === */
.toolbox-card2-content {
width: 100%;
height: 100%;
display: flex;
align-items: center;
justify-content: center;
gap: 6px;
padding: 0 4px;
}
.toolbox-card2-frame {
flex: 1;
height: 85%;
border-radius: 8px;
position: relative;
overflow: hidden;
display: flex;
flex-direction: column;
align-items: center;
justify-content: flex-end;
background: linear-gradient(180deg, #1a1d42 0%, #141230 100%);
border: 1px solid rgba(0, 255, 136, 0.06);
transition: all 0.35s cubic-bezier(0.4, 0, 0.2, 1);
}
.toolbox-card2-frame:hover {
border-color: rgba(0, 255, 136, 0.2);
box-shadow: 0 0 16px rgba(0, 255, 136, 0.08);
transform: scale(1.04);
}
.toolbox-card2-product {
position: absolute;
top: 14%;
left: 50%;
transform: translateX(-50%);
width: 55%;
height: 50%;
border-radius: 6px;
transition: all 0.3s;
}
.toolbox-card2-frame:nth-child(1) .toolbox-card2-product {
background: repeating-linear-gradient(0deg, #6b9b7a 0px, #6b9b7a 2px, #d4dfc8 2px, #d4dfc8 4px);
}
.toolbox-card2-frame:nth-child(2) .toolbox-card2-product {
background: repeating-linear-gradient(0deg, #6b9b7a 0px, #6b9b7a 2px, #d4dfc8 2px, #d4dfc8 4px);
transform: translateX(-50%) perspective(200px) rotateY(25deg);
width: 48%;
}
.toolbox-card2-frame:nth-child(3) .toolbox-card2-product {
background: repeating-linear-gradient(90deg, #6b9b7a 0px, #6b9b7a 2px, #d4dfc8 2px, #d4dfc8 4px);
width: 50%;
height: 40%;
border-radius: 50%;
}
.toolbox-card2-frame:nth-child(4) .toolbox-card2-product {
background: repeating-linear-gradient(0deg, #6b9b7a 0px, #6b9b7a 2px, #d4dfc8 2px, #d4dfc8 4px);
width: 58%;
transform: translateX(-50%) perspective(200px) rotateX(-15deg);
}
.toolbox-card2-frame:nth-child(5) .toolbox-card2-product {
background: repeating-linear-gradient(0deg, #5a7a4e 0px, #5a7a4e 2px, #b8c8a8 2px, #b8c8a8 4px);
width: 50%;
opacity: 0.8;
}
.toolbox-card2-shadow {
position: absolute;
top: 66%;
left: 50%;
transform: translateX(-50%);
width: 40%;
height: 4px;
border-radius: 50%;
background: rgba(0, 255, 136, 0.06);
filter: blur(3px);
}
.toolbox-card2-angle-label {
position: relative;
z-index: 1;
font-size: 9px;
font-weight: 700;
color: var(--toolbox-text-tertiary);
margin-bottom: 10%;
letter-spacing: 0.5px;
padding: 2px 8px;
border-radius: 4px;
background: rgba(0, 255, 136, 0.06);
border: 1px solid rgba(0, 255, 136, 0.08);
}
.toolbox-card2-frame:nth-child(1) .toolbox-card2-angle-label {
color: var(--toolbox-green);
background: rgba(0, 255, 136, 0.1);
border-color: rgba(0, 255, 136, 0.2);
}
/* === Card 3: 一键数字人 === */
.toolbox-card3-content {
width: 100%;
height: 100%;
display: flex;
gap: 0;
position: relative;
}
.toolbox-card3-side {
flex: 1;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
border-radius: 10px;
position: relative;
overflow: hidden;
}
.toolbox-card3-left {
background: rgba(255, 255, 255, 0.02);
margin-right: 1px;
}
.toolbox-card3-right {
background: rgba(0, 255, 136, 0.02);
margin-left: 1px;
}
.toolbox-card3-portrait {
width: 70%;
aspect-ratio: 3/4;
border-radius: 10px;
position: relative;
overflow: hidden;
}
.toolbox-card3-left .toolbox-card3-portrait {
background: linear-gradient(180deg, #2a2d5e, #1e2050);
box-shadow: inset 0 0 20px rgba(0, 0, 0, 0.3);
}
.toolbox-card3-left .toolbox-card3-portrait::before {
content: '';
position: absolute;
top: 14%;
left: 50%;
transform: translateX(-50%);
width: 32%;
aspect-ratio: 1;
border-radius: 50%;
background: rgba(200, 190, 220, 0.1);
}
.toolbox-card3-left .toolbox-card3-portrait::after {
content: '';
position: absolute;
top: 42%;
left: 50%;
transform: translateX(-50%);
width: 50%;
height: 40%;
border-radius: 20% 20% 5% 5%;
background: rgba(200, 190, 220, 0.06);
}
.toolbox-card3-portrait-mark {
position: absolute;
bottom: 8px;
left: 8px;
font-size: 8px;
font-weight: 700;
color: rgba(255, 255, 255, 0.25);
background: rgba(0, 0, 0, 0.4);
padding: 2px 6px;
border-radius: 3px;
letter-spacing: 1px;
}
.toolbox-card3-right .toolbox-card3-portrait {
background: linear-gradient(180deg, #1a3a2e, #0d2a20);
border: 1px solid rgba(0, 255, 136, 0.12);
box-shadow:
0 0 30px rgba(0, 255, 136, 0.08),
inset 0 0 20px rgba(0, 255, 136, 0.04);
}
.toolbox-card3-right .toolbox-card3-portrait::before {
content: '';
position: absolute;
top: 14%;
left: 50%;
transform: translateX(-50%);
width: 32%;
aspect-ratio: 1;
border-radius: 50%;
background: rgba(0, 255, 136, 0.1);
box-shadow: 0 0 20px rgba(0, 255, 136, 0.15);
}
.toolbox-card3-right .toolbox-card3-portrait::after {
content: '';
position: absolute;
top: 42%;
left: 50%;
transform: translateX(-50%);
width: 50%;
height: 40%;
border-radius: 20% 20% 5% 5%;
background: rgba(0, 255, 136, 0.06);
box-shadow: 0 0 15px rgba(0, 255, 136, 0.08);
}
.toolbox-card3-glow-ring {
position: absolute;
inset: -4px;
border-radius: 14px;
border: 1.5px solid rgba(0, 255, 136, 0.2);
animation: omni-toolbox-glowPulse 2.5s ease-in-out infinite;
}
@keyframes omni-toolbox-glowPulse {
0%, 100% { opacity: 0.3; box-shadow: 0 0 10px rgba(0, 255, 136, 0.05); }
50% { opacity: 1; box-shadow: 0 0 25px rgba(0, 255, 136, 0.15); }
}
.toolbox-card3-lipsync {
position: absolute;
top: 32%;
left: 62%;
display: flex;
align-items: center;
gap: 1.5px;
}
.toolbox-card3-lipsync span {
width: 2px;
border-radius: 1px;
background: var(--toolbox-green);
animation: omni-toolbox-lipsync 0.8s ease-in-out infinite;
}
.toolbox-card3-lipsync span:nth-child(1) { height: 4px; animation-delay: 0s; }
.toolbox-card3-lipsync span:nth-child(2) { height: 8px; animation-delay: 0.1s; }
.toolbox-card3-lipsync span:nth-child(3) { height: 5px; animation-delay: 0.2s; }
.toolbox-card3-lipsync span:nth-child(4) { height: 10px; animation-delay: 0.3s; }
.toolbox-card3-lipsync span:nth-child(5) { height: 4px; animation-delay: 0.4s; }
@keyframes omni-toolbox-lipsync {
0%, 100% { transform: scaleY(1); opacity: 0.6; }
50% { transform: scaleY(0.3); opacity: 1; }
}
.toolbox-card3-gesture {
position: absolute;
top: 55%;
left: 20%;
width: 24px;
height: 2px;
border-radius: 1px;
opacity: 0.5;
}
.toolbox-card3-gesture::before {
content: '';
position: absolute;
inset: 0;
background: linear-gradient(90deg, transparent, rgba(0, 255, 136, 0.5), transparent);
animation: omni-toolbox-gestureMove 2s ease-in-out infinite;
}
.toolbox-card3-gesture::after {
content: '';
position: absolute;
top: -6px;
right: -4px;
width: 8px;
height: 8px;
border-radius: 50%;
border: 1.5px solid rgba(0, 255, 136, 0.3);
animation: omni-toolbox-gestureMove 2s ease-in-out infinite;
}
@keyframes omni-toolbox-gestureMove {
0%, 100% { opacity: 0.2; transform: translateX(0); }
50% { opacity: 0.8; transform: translateX(6px); }
}
.toolbox-card3-live {
position: absolute;
top: 8px;
right: 8px;
font-size: 8px;
font-weight: 900;
color: #0a0b12;
background: var(--toolbox-green);
padding: 2px 7px;
border-radius: 4px;
letter-spacing: 1px;
animation: omni-toolbox-livePulse 1.5s ease-in-out infinite;
}
@keyframes omni-toolbox-livePulse {
0%, 100% { box-shadow: 0 0 6px rgba(0, 255, 136, 0.3); }
50% { box-shadow: 0 0 16px rgba(0, 255, 136, 0.6); }
}
.toolbox-card3-transform {
position: absolute;
left: 50%;
top: 50%;
transform: translate(-50%, -50%);
z-index: 2;
width: 32px;
height: 32px;
border-radius: 50%;
background: var(--toolbox-green);
display: flex;
align-items: center;
justify-content: center;
font-size: 14px;
color: #0a0b12;
box-shadow: 0 0 20px rgba(0, 255, 136, 0.3);
animation: omni-toolbox-transformSpin 3s ease-in-out infinite;
}
@keyframes omni-toolbox-transformSpin {
0%, 100% { box-shadow: 0 0 20px rgba(0, 255, 136, 0.3); }
50% { box-shadow: 0 0 30px rgba(0, 255, 136, 0.5); }
}
.toolbox-card3-label {
font-size: 10px;
font-weight: 700;
letter-spacing: 0.5px;
margin-top: 8px;
}
.toolbox-card3-left .toolbox-card3-label {
color: rgba(255, 255, 255, 0.3);
}
.toolbox-card3-right .toolbox-card3-label {
color: rgba(0, 255, 136, 0.5);
}
.toolbox-card3-divider {
width: 1px;
background: linear-gradient(to bottom, transparent, rgba(0, 255, 136, 0.25), transparent);
position: absolute;
left: 50%;
top: 8%;
height: 84%;
}
/* === Card 4: 去除水印 === */
.toolbox-card4-content {
width: 100%;
height: 100%;
display: flex;
gap: 0;
position: relative;
}
.toolbox-card4-side {
flex: 1;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
border-radius: 10px;
position: relative;
overflow: hidden;
}
.toolbox-card4-left {
background: rgba(255, 255, 255, 0.02);
margin-right: 1px;
}
.toolbox-card4-right {
background: rgba(0, 255, 136, 0.02);
margin-left: 1px;
}
.toolbox-card4-img {
width: 100%;
flex: 1;
border-radius: 8px;
position: relative;
overflow: hidden;
}
.toolbox-card4-img img {
width: 100%;
height: 100%;
object-fit: cover;
display: block;
border-radius: 8px;
}
.toolbox-card4-left .toolbox-card4-img {
box-shadow: inset 0 0 20px rgba(0, 0, 0, 0.2);
}
.toolbox-card4-right .toolbox-card4-img {
box-shadow: 0 0 12px rgba(0, 255, 136, 0.06);
}
.toolbox-card4-label {
font-size: 10px;
color: var(--toolbox-text-tertiary);
margin-top: 8px;
font-weight: 700;
letter-spacing: 0.5px;
}
.toolbox-card4-left .toolbox-card4-label {
color: rgba(255, 200, 200, 0.5);
}
.toolbox-card4-right .toolbox-card4-label {
color: rgba(0, 255, 136, 0.5);
}
.toolbox-card4-divider {
width: 1px;
background: linear-gradient(to bottom, transparent, rgba(0, 255, 136, 0.3), transparent);
position: absolute;
left: 50%;
top: 8%;
height: 84%;
}
/* ===== Responsive ===== */
@media (max-width: 980px) {
.omni-home__toolbox-shell {
flex-direction: column;
padding: 48px 22px 64px;
}
.omni-home__toolbox-left {
width: 100%;
flex-shrink: unset;
}
.omni-home__toolbox-grid {
width: 100%;
min-height: clamp(480px, 70vw, 700px);
}
.omni-home__toolbox-workflow {
margin-top: 0;
}
}
@media (max-width: 560px) {
.omni-home__toolbox-shell {
padding: 36px 18px 48px;
}
.omni-home__toolbox-title {
font-size: 20px;
}
.omni-home__toolbox-grid {
grid-template-columns: 1fr;
grid-template-rows: auto;
min-height: auto;
}
.omni-home__toolbox-card {
min-height: 200px;
}
}
@media (prefers-reduced-motion: reduce) {
.omni-home__toolbox-item,
.omni-home__toolbox-card {
animation: none;
}
.toolbox-card3-glow-ring,
.toolbox-card3-lipsync span,
.toolbox-card3-gesture::before,
.toolbox-card3-gesture::after,
.toolbox-card3-live,
.toolbox-card3-transform {
animation: none;
}
}
+1 -1
View File
@@ -22,7 +22,7 @@ export default defineConfig({
host: "127.0.0.1",
},
esbuild: {
drop: ["console", "debugger"],
drop: ["debugger"],
},
build: {
sourcemap: "hidden",