Spaces:
Sleeping
Sleeping
图片预览下载增强与双模式提示词完善
Browse files- frontend/package-lock.json +94 -1
- frontend/package.json +1 -0
- frontend/src/components/ImagePreview.tsx +160 -21
- frontend/vite.config.ts +4 -0
- src/prompts/loader.ts +52 -10
- src/prompts/templates/roles/code-edit.md +17 -2
- src/prompts/templates/roles/code-generation.md +23 -3
- src/prompts/templates/roles/concept-designer.md +14 -0
- src/server.ts +22 -16
- src/services/code-edit.ts +5 -16
- src/services/concept-designer.ts +7 -61
frontend/package-lock.json
CHANGED
|
@@ -9,6 +9,7 @@
|
|
| 9 |
"version": "0.0.0",
|
| 10 |
"dependencies": {
|
| 11 |
"@types/react-syntax-highlighter": "^15.5.13",
|
|
|
|
| 12 |
"react": "^19.2.0",
|
| 13 |
"react-dom": "^19.2.0",
|
| 14 |
"react-syntax-highlighter": "^16.1.0"
|
|
@@ -2029,6 +2030,12 @@
|
|
| 2029 |
"dev": true,
|
| 2030 |
"license": "MIT"
|
| 2031 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2032 |
"node_modules/cross-spawn": {
|
| 2033 |
"version": "7.0.6",
|
| 2034 |
"dev": true,
|
|
@@ -2592,6 +2599,12 @@
|
|
| 2592 |
"node": ">= 4"
|
| 2593 |
}
|
| 2594 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2595 |
"node_modules/import-fresh": {
|
| 2596 |
"version": "3.3.1",
|
| 2597 |
"dev": true,
|
|
@@ -2615,6 +2628,12 @@
|
|
| 2615 |
"node": ">=0.8.19"
|
| 2616 |
}
|
| 2617 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2618 |
"node_modules/is-alphabetical": {
|
| 2619 |
"version": "2.0.1",
|
| 2620 |
"license": "MIT",
|
|
@@ -2703,6 +2722,12 @@
|
|
| 2703 |
"node": ">=0.12.0"
|
| 2704 |
}
|
| 2705 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2706 |
"node_modules/isexe": {
|
| 2707 |
"version": "2.0.0",
|
| 2708 |
"dev": true,
|
|
@@ -2770,6 +2795,18 @@
|
|
| 2770 |
"node": ">=6"
|
| 2771 |
}
|
| 2772 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2773 |
"node_modules/keyv": {
|
| 2774 |
"version": "4.5.4",
|
| 2775 |
"dev": true,
|
|
@@ -2790,6 +2827,15 @@
|
|
| 2790 |
"node": ">= 0.8.0"
|
| 2791 |
}
|
| 2792 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2793 |
"node_modules/lilconfig": {
|
| 2794 |
"version": "3.1.3",
|
| 2795 |
"dev": true,
|
|
@@ -2986,6 +3032,12 @@
|
|
| 2986 |
"url": "https://github.com/sponsors/sindresorhus"
|
| 2987 |
}
|
| 2988 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2989 |
"node_modules/parent-module": {
|
| 2990 |
"version": "1.0.1",
|
| 2991 |
"dev": true,
|
|
@@ -3236,6 +3288,12 @@
|
|
| 3236 |
"node": ">=6"
|
| 3237 |
}
|
| 3238 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 3239 |
"node_modules/property-information": {
|
| 3240 |
"version": "7.1.0",
|
| 3241 |
"license": "MIT",
|
|
@@ -3323,6 +3381,21 @@
|
|
| 3323 |
"pify": "^2.3.0"
|
| 3324 |
}
|
| 3325 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 3326 |
"node_modules/readdirp": {
|
| 3327 |
"version": "3.6.0",
|
| 3328 |
"dev": true,
|
|
@@ -3449,6 +3522,12 @@
|
|
| 3449 |
"queue-microtask": "^1.2.2"
|
| 3450 |
}
|
| 3451 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 3452 |
"node_modules/scheduler": {
|
| 3453 |
"version": "0.27.0",
|
| 3454 |
"license": "MIT"
|
|
@@ -3461,6 +3540,12 @@
|
|
| 3461 |
"semver": "bin/semver.js"
|
| 3462 |
}
|
| 3463 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 3464 |
"node_modules/shebang-command": {
|
| 3465 |
"version": "2.0.0",
|
| 3466 |
"dev": true,
|
|
@@ -3496,6 +3581,15 @@
|
|
| 3496 |
"url": "https://github.com/sponsors/wooorm"
|
| 3497 |
}
|
| 3498 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 3499 |
"node_modules/strip-json-comments": {
|
| 3500 |
"version": "3.1.1",
|
| 3501 |
"dev": true,
|
|
@@ -3765,7 +3859,6 @@
|
|
| 3765 |
},
|
| 3766 |
"node_modules/util-deprecate": {
|
| 3767 |
"version": "1.0.2",
|
| 3768 |
-
"dev": true,
|
| 3769 |
"license": "MIT"
|
| 3770 |
},
|
| 3771 |
"node_modules/vite": {
|
|
|
|
| 9 |
"version": "0.0.0",
|
| 10 |
"dependencies": {
|
| 11 |
"@types/react-syntax-highlighter": "^15.5.13",
|
| 12 |
+
"jszip": "^3.10.1",
|
| 13 |
"react": "^19.2.0",
|
| 14 |
"react-dom": "^19.2.0",
|
| 15 |
"react-syntax-highlighter": "^16.1.0"
|
|
|
|
| 2030 |
"dev": true,
|
| 2031 |
"license": "MIT"
|
| 2032 |
},
|
| 2033 |
+
"node_modules/core-util-is": {
|
| 2034 |
+
"version": "1.0.3",
|
| 2035 |
+
"resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz",
|
| 2036 |
+
"integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==",
|
| 2037 |
+
"license": "MIT"
|
| 2038 |
+
},
|
| 2039 |
"node_modules/cross-spawn": {
|
| 2040 |
"version": "7.0.6",
|
| 2041 |
"dev": true,
|
|
|
|
| 2599 |
"node": ">= 4"
|
| 2600 |
}
|
| 2601 |
},
|
| 2602 |
+
"node_modules/immediate": {
|
| 2603 |
+
"version": "3.0.6",
|
| 2604 |
+
"resolved": "https://registry.npmjs.org/immediate/-/immediate-3.0.6.tgz",
|
| 2605 |
+
"integrity": "sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ==",
|
| 2606 |
+
"license": "MIT"
|
| 2607 |
+
},
|
| 2608 |
"node_modules/import-fresh": {
|
| 2609 |
"version": "3.3.1",
|
| 2610 |
"dev": true,
|
|
|
|
| 2628 |
"node": ">=0.8.19"
|
| 2629 |
}
|
| 2630 |
},
|
| 2631 |
+
"node_modules/inherits": {
|
| 2632 |
+
"version": "2.0.4",
|
| 2633 |
+
"resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
|
| 2634 |
+
"integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==",
|
| 2635 |
+
"license": "ISC"
|
| 2636 |
+
},
|
| 2637 |
"node_modules/is-alphabetical": {
|
| 2638 |
"version": "2.0.1",
|
| 2639 |
"license": "MIT",
|
|
|
|
| 2722 |
"node": ">=0.12.0"
|
| 2723 |
}
|
| 2724 |
},
|
| 2725 |
+
"node_modules/isarray": {
|
| 2726 |
+
"version": "1.0.0",
|
| 2727 |
+
"resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz",
|
| 2728 |
+
"integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==",
|
| 2729 |
+
"license": "MIT"
|
| 2730 |
+
},
|
| 2731 |
"node_modules/isexe": {
|
| 2732 |
"version": "2.0.0",
|
| 2733 |
"dev": true,
|
|
|
|
| 2795 |
"node": ">=6"
|
| 2796 |
}
|
| 2797 |
},
|
| 2798 |
+
"node_modules/jszip": {
|
| 2799 |
+
"version": "3.10.1",
|
| 2800 |
+
"resolved": "https://registry.npmjs.org/jszip/-/jszip-3.10.1.tgz",
|
| 2801 |
+
"integrity": "sha512-xXDvecyTpGLrqFrvkrUSoxxfJI5AH7U8zxxtVclpsUtMCq4JQ290LY8AW5c7Ggnr/Y/oK+bQMbqK2qmtk3pN4g==",
|
| 2802 |
+
"license": "(MIT OR GPL-3.0-or-later)",
|
| 2803 |
+
"dependencies": {
|
| 2804 |
+
"lie": "~3.3.0",
|
| 2805 |
+
"pako": "~1.0.2",
|
| 2806 |
+
"readable-stream": "~2.3.6",
|
| 2807 |
+
"setimmediate": "^1.0.5"
|
| 2808 |
+
}
|
| 2809 |
+
},
|
| 2810 |
"node_modules/keyv": {
|
| 2811 |
"version": "4.5.4",
|
| 2812 |
"dev": true,
|
|
|
|
| 2827 |
"node": ">= 0.8.0"
|
| 2828 |
}
|
| 2829 |
},
|
| 2830 |
+
"node_modules/lie": {
|
| 2831 |
+
"version": "3.3.0",
|
| 2832 |
+
"resolved": "https://registry.npmjs.org/lie/-/lie-3.3.0.tgz",
|
| 2833 |
+
"integrity": "sha512-UaiMJzeWRlEujzAuw5LokY1L5ecNQYZKfmyZ9L7wDHb/p5etKaxXhohBcrw0EYby+G/NA52vRSN4N39dxHAIwQ==",
|
| 2834 |
+
"license": "MIT",
|
| 2835 |
+
"dependencies": {
|
| 2836 |
+
"immediate": "~3.0.5"
|
| 2837 |
+
}
|
| 2838 |
+
},
|
| 2839 |
"node_modules/lilconfig": {
|
| 2840 |
"version": "3.1.3",
|
| 2841 |
"dev": true,
|
|
|
|
| 3032 |
"url": "https://github.com/sponsors/sindresorhus"
|
| 3033 |
}
|
| 3034 |
},
|
| 3035 |
+
"node_modules/pako": {
|
| 3036 |
+
"version": "1.0.11",
|
| 3037 |
+
"resolved": "https://registry.npmjs.org/pako/-/pako-1.0.11.tgz",
|
| 3038 |
+
"integrity": "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==",
|
| 3039 |
+
"license": "(MIT AND Zlib)"
|
| 3040 |
+
},
|
| 3041 |
"node_modules/parent-module": {
|
| 3042 |
"version": "1.0.1",
|
| 3043 |
"dev": true,
|
|
|
|
| 3288 |
"node": ">=6"
|
| 3289 |
}
|
| 3290 |
},
|
| 3291 |
+
"node_modules/process-nextick-args": {
|
| 3292 |
+
"version": "2.0.1",
|
| 3293 |
+
"resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz",
|
| 3294 |
+
"integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==",
|
| 3295 |
+
"license": "MIT"
|
| 3296 |
+
},
|
| 3297 |
"node_modules/property-information": {
|
| 3298 |
"version": "7.1.0",
|
| 3299 |
"license": "MIT",
|
|
|
|
| 3381 |
"pify": "^2.3.0"
|
| 3382 |
}
|
| 3383 |
},
|
| 3384 |
+
"node_modules/readable-stream": {
|
| 3385 |
+
"version": "2.3.8",
|
| 3386 |
+
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz",
|
| 3387 |
+
"integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==",
|
| 3388 |
+
"license": "MIT",
|
| 3389 |
+
"dependencies": {
|
| 3390 |
+
"core-util-is": "~1.0.0",
|
| 3391 |
+
"inherits": "~2.0.3",
|
| 3392 |
+
"isarray": "~1.0.0",
|
| 3393 |
+
"process-nextick-args": "~2.0.0",
|
| 3394 |
+
"safe-buffer": "~5.1.1",
|
| 3395 |
+
"string_decoder": "~1.1.1",
|
| 3396 |
+
"util-deprecate": "~1.0.1"
|
| 3397 |
+
}
|
| 3398 |
+
},
|
| 3399 |
"node_modules/readdirp": {
|
| 3400 |
"version": "3.6.0",
|
| 3401 |
"dev": true,
|
|
|
|
| 3522 |
"queue-microtask": "^1.2.2"
|
| 3523 |
}
|
| 3524 |
},
|
| 3525 |
+
"node_modules/safe-buffer": {
|
| 3526 |
+
"version": "5.1.2",
|
| 3527 |
+
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz",
|
| 3528 |
+
"integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==",
|
| 3529 |
+
"license": "MIT"
|
| 3530 |
+
},
|
| 3531 |
"node_modules/scheduler": {
|
| 3532 |
"version": "0.27.0",
|
| 3533 |
"license": "MIT"
|
|
|
|
| 3540 |
"semver": "bin/semver.js"
|
| 3541 |
}
|
| 3542 |
},
|
| 3543 |
+
"node_modules/setimmediate": {
|
| 3544 |
+
"version": "1.0.5",
|
| 3545 |
+
"resolved": "https://registry.npmjs.org/setimmediate/-/setimmediate-1.0.5.tgz",
|
| 3546 |
+
"integrity": "sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA==",
|
| 3547 |
+
"license": "MIT"
|
| 3548 |
+
},
|
| 3549 |
"node_modules/shebang-command": {
|
| 3550 |
"version": "2.0.0",
|
| 3551 |
"dev": true,
|
|
|
|
| 3581 |
"url": "https://github.com/sponsors/wooorm"
|
| 3582 |
}
|
| 3583 |
},
|
| 3584 |
+
"node_modules/string_decoder": {
|
| 3585 |
+
"version": "1.1.1",
|
| 3586 |
+
"resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz",
|
| 3587 |
+
"integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==",
|
| 3588 |
+
"license": "MIT",
|
| 3589 |
+
"dependencies": {
|
| 3590 |
+
"safe-buffer": "~5.1.0"
|
| 3591 |
+
}
|
| 3592 |
+
},
|
| 3593 |
"node_modules/strip-json-comments": {
|
| 3594 |
"version": "3.1.1",
|
| 3595 |
"dev": true,
|
|
|
|
| 3859 |
},
|
| 3860 |
"node_modules/util-deprecate": {
|
| 3861 |
"version": "1.0.2",
|
|
|
|
| 3862 |
"license": "MIT"
|
| 3863 |
},
|
| 3864 |
"node_modules/vite": {
|
frontend/package.json
CHANGED
|
@@ -11,6 +11,7 @@
|
|
| 11 |
},
|
| 12 |
"dependencies": {
|
| 13 |
"@types/react-syntax-highlighter": "^15.5.13",
|
|
|
|
| 14 |
"react": "^19.2.0",
|
| 15 |
"react-dom": "^19.2.0",
|
| 16 |
"react-syntax-highlighter": "^16.1.0"
|
|
|
|
| 11 |
},
|
| 12 |
"dependencies": {
|
| 13 |
"@types/react-syntax-highlighter": "^15.5.13",
|
| 14 |
+
"jszip": "^3.10.1",
|
| 15 |
"react": "^19.2.0",
|
| 16 |
"react-dom": "^19.2.0",
|
| 17 |
"react-syntax-highlighter": "^16.1.0"
|
frontend/src/components/ImagePreview.tsx
CHANGED
|
@@ -1,4 +1,5 @@
|
|
| 1 |
-
import { useEffect, useState } from 'react';
|
|
|
|
| 2 |
|
| 3 |
interface ImagePreviewProps {
|
| 4 |
imageUrls: string[];
|
|
@@ -6,43 +7,139 @@ interface ImagePreviewProps {
|
|
| 6 |
|
| 7 |
export function ImagePreview({ imageUrls }: ImagePreviewProps) {
|
| 8 |
const [activeIndex, setActiveIndex] = useState(0);
|
|
|
|
|
|
|
|
|
|
|
|
|
| 9 |
|
| 10 |
useEffect(() => {
|
| 11 |
setActiveIndex(0);
|
| 12 |
}, [imageUrls.join('|')]);
|
| 13 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 14 |
const activeImage = imageUrls[activeIndex];
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 15 |
|
| 16 |
-
const handleDownloadAll = () => {
|
| 17 |
-
|
| 18 |
-
|
| 19 |
-
|
| 20 |
-
|
| 21 |
-
|
| 22 |
-
|
| 23 |
-
|
| 24 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 25 |
};
|
| 26 |
|
| 27 |
return (
|
| 28 |
<div className="h-full flex flex-col bg-bg-secondary/30 rounded-2xl overflow-hidden">
|
| 29 |
<div className="flex items-center justify-between px-4 py-2.5">
|
| 30 |
<h3 className="text-xs font-medium text-text-secondary/80 uppercase tracking-wide">图片预览</h3>
|
| 31 |
-
<
|
| 32 |
-
|
| 33 |
-
|
| 34 |
-
|
| 35 |
-
|
| 36 |
-
|
| 37 |
-
<
|
| 38 |
-
|
| 39 |
-
|
| 40 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 41 |
</div>
|
| 42 |
|
| 43 |
<div className="flex-1 bg-black/90 flex items-center justify-center">
|
| 44 |
{activeImage ? (
|
| 45 |
-
<
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 46 |
) : (
|
| 47 |
<p className="text-xs text-text-secondary/60">暂无图片输出</p>
|
| 48 |
)}
|
|
@@ -70,6 +167,48 @@ export function ImagePreview({ imageUrls }: ImagePreviewProps) {
|
|
| 70 |
</div>
|
| 71 |
</div>
|
| 72 |
)}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 73 |
</div>
|
| 74 |
);
|
| 75 |
}
|
|
|
|
| 1 |
+
import { useEffect, useMemo, useState } from 'react';
|
| 2 |
+
import JSZip from 'jszip';
|
| 3 |
|
| 4 |
interface ImagePreviewProps {
|
| 5 |
imageUrls: string[];
|
|
|
|
| 7 |
|
| 8 |
export function ImagePreview({ imageUrls }: ImagePreviewProps) {
|
| 9 |
const [activeIndex, setActiveIndex] = useState(0);
|
| 10 |
+
const [isLightboxOpen, setIsLightboxOpen] = useState(false);
|
| 11 |
+
const [zoom, setZoom] = useState(1);
|
| 12 |
+
const [isDownloadingSingle, setIsDownloadingSingle] = useState(false);
|
| 13 |
+
const [isDownloadingAll, setIsDownloadingAll] = useState(false);
|
| 14 |
|
| 15 |
useEffect(() => {
|
| 16 |
setActiveIndex(0);
|
| 17 |
}, [imageUrls.join('|')]);
|
| 18 |
|
| 19 |
+
useEffect(() => {
|
| 20 |
+
if (!isLightboxOpen) {
|
| 21 |
+
setZoom(1);
|
| 22 |
+
}
|
| 23 |
+
}, [isLightboxOpen]);
|
| 24 |
+
|
| 25 |
const activeImage = imageUrls[activeIndex];
|
| 26 |
+
const hasImages = imageUrls.length > 0;
|
| 27 |
+
|
| 28 |
+
const timestampPrefix = useMemo(() => {
|
| 29 |
+
const now = new Date();
|
| 30 |
+
const pad = (value: number) => String(value).padStart(2, '0');
|
| 31 |
+
return `${now.getFullYear()}${pad(now.getMonth() + 1)}${pad(now.getDate())}-${pad(now.getHours())}${pad(now.getMinutes())}${pad(now.getSeconds())}`;
|
| 32 |
+
}, [imageUrls.join('|')]);
|
| 33 |
+
|
| 34 |
+
const getAbsoluteUrl = (url: string): string => {
|
| 35 |
+
if (/^https?:\/\//i.test(url)) {
|
| 36 |
+
return url;
|
| 37 |
+
}
|
| 38 |
+
return new URL(url, window.location.origin).toString();
|
| 39 |
+
};
|
| 40 |
+
|
| 41 |
+
const downloadBlob = (blob: Blob, filename: string) => {
|
| 42 |
+
const blobUrl = URL.createObjectURL(blob);
|
| 43 |
+
const link = document.createElement('a');
|
| 44 |
+
link.href = blobUrl;
|
| 45 |
+
link.download = filename;
|
| 46 |
+
link.click();
|
| 47 |
+
setTimeout(() => URL.revokeObjectURL(blobUrl), 1000);
|
| 48 |
+
};
|
| 49 |
+
|
| 50 |
+
const handleDownloadSingle = async () => {
|
| 51 |
+
if (!activeImage || isDownloadingSingle) {
|
| 52 |
+
return;
|
| 53 |
+
}
|
| 54 |
+
setIsDownloadingSingle(true);
|
| 55 |
+
try {
|
| 56 |
+
const response = await fetch(getAbsoluteUrl(activeImage));
|
| 57 |
+
if (!response.ok) {
|
| 58 |
+
throw new Error(`下载失败: ${response.status}`);
|
| 59 |
+
}
|
| 60 |
+
const blob = await response.blob();
|
| 61 |
+
downloadBlob(blob, `${timestampPrefix}-image-${activeIndex + 1}.png`);
|
| 62 |
+
} catch (error) {
|
| 63 |
+
console.error('单张下载失败', error);
|
| 64 |
+
} finally {
|
| 65 |
+
setIsDownloadingSingle(false);
|
| 66 |
+
}
|
| 67 |
+
};
|
| 68 |
|
| 69 |
+
const handleDownloadAll = async () => {
|
| 70 |
+
if (!hasImages || isDownloadingAll) {
|
| 71 |
+
return;
|
| 72 |
+
}
|
| 73 |
+
setIsDownloadingAll(true);
|
| 74 |
+
try {
|
| 75 |
+
const zip = new JSZip();
|
| 76 |
+
await Promise.all(
|
| 77 |
+
imageUrls.map(async (url, index) => {
|
| 78 |
+
const response = await fetch(getAbsoluteUrl(url));
|
| 79 |
+
if (!response.ok) {
|
| 80 |
+
throw new Error(`图片 ${index + 1} 下载失败: ${response.status}`);
|
| 81 |
+
}
|
| 82 |
+
const blob = await response.blob();
|
| 83 |
+
zip.file(`${timestampPrefix}-image-${index + 1}.png`, blob);
|
| 84 |
+
})
|
| 85 |
+
);
|
| 86 |
+
const zipBlob = await zip.generateAsync({ type: 'blob' });
|
| 87 |
+
downloadBlob(zipBlob, `${timestampPrefix}-images.zip`);
|
| 88 |
+
} catch (error) {
|
| 89 |
+
console.error('打包下载失败', error);
|
| 90 |
+
} finally {
|
| 91 |
+
setIsDownloadingAll(false);
|
| 92 |
+
}
|
| 93 |
};
|
| 94 |
|
| 95 |
return (
|
| 96 |
<div className="h-full flex flex-col bg-bg-secondary/30 rounded-2xl overflow-hidden">
|
| 97 |
<div className="flex items-center justify-between px-4 py-2.5">
|
| 98 |
<h3 className="text-xs font-medium text-text-secondary/80 uppercase tracking-wide">图片预览</h3>
|
| 99 |
+
<div className="flex items-center gap-3">
|
| 100 |
+
<button
|
| 101 |
+
onClick={() => setIsLightboxOpen(true)}
|
| 102 |
+
disabled={!hasImages}
|
| 103 |
+
className="text-xs text-text-secondary/70 hover:text-accent transition-colors flex items-center gap-1.5 disabled:opacity-40 disabled:cursor-not-allowed"
|
| 104 |
+
>
|
| 105 |
+
<svg className="w-3.5 h-3.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
| 106 |
+
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M15 3h6m0 0v6m0-6l-7 7M9 21H3m0 0v-6m0 6l7-7" />
|
| 107 |
+
</svg>
|
| 108 |
+
放大预览
|
| 109 |
+
</button>
|
| 110 |
+
<button
|
| 111 |
+
onClick={handleDownloadSingle}
|
| 112 |
+
disabled={!hasImages || isDownloadingSingle || isDownloadingAll}
|
| 113 |
+
className="text-xs text-text-secondary/70 hover:text-accent transition-colors flex items-center gap-1.5 disabled:opacity-40 disabled:cursor-not-allowed"
|
| 114 |
+
>
|
| 115 |
+
<svg className="w-3.5 h-3.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
| 116 |
+
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4 16v1a3 3 0 003 3h10a3 3 0 003-3v-1m-4-4l-4 4m0 0l-4-4m4 4V4" />
|
| 117 |
+
</svg>
|
| 118 |
+
{isDownloadingSingle ? '下载中...' : '下载'}
|
| 119 |
+
</button>
|
| 120 |
+
<button
|
| 121 |
+
onClick={handleDownloadAll}
|
| 122 |
+
disabled={!hasImages || isDownloadingAll || isDownloadingSingle}
|
| 123 |
+
className="text-xs text-text-secondary/70 hover:text-accent transition-colors flex items-center gap-1.5 disabled:opacity-40 disabled:cursor-not-allowed"
|
| 124 |
+
>
|
| 125 |
+
<svg className="w-3.5 h-3.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
| 126 |
+
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4 16v1a3 3 0 003 3h10a3 3 0 003-3v-1m-4-4l-4 4m0 0l-4-4m4 4V4" />
|
| 127 |
+
</svg>
|
| 128 |
+
{isDownloadingAll ? '打包中...' : '下载全部'}
|
| 129 |
+
</button>
|
| 130 |
+
</div>
|
| 131 |
</div>
|
| 132 |
|
| 133 |
<div className="flex-1 bg-black/90 flex items-center justify-center">
|
| 134 |
{activeImage ? (
|
| 135 |
+
<button
|
| 136 |
+
type="button"
|
| 137 |
+
onClick={() => setIsLightboxOpen(true)}
|
| 138 |
+
className="w-full h-full cursor-zoom-in"
|
| 139 |
+
title="点击放大预览"
|
| 140 |
+
>
|
| 141 |
+
<img src={activeImage} alt={`图片 ${activeIndex + 1}`} className="w-full h-full object-contain" />
|
| 142 |
+
</button>
|
| 143 |
) : (
|
| 144 |
<p className="text-xs text-text-secondary/60">暂无图片输出</p>
|
| 145 |
)}
|
|
|
|
| 167 |
</div>
|
| 168 |
</div>
|
| 169 |
)}
|
| 170 |
+
|
| 171 |
+
{isLightboxOpen && activeImage && (
|
| 172 |
+
<div className="fixed inset-0 z-[80] bg-black/85 backdrop-blur-sm flex flex-col">
|
| 173 |
+
<div className="flex items-center justify-between px-5 py-3 text-white/90">
|
| 174 |
+
<div className="text-xs">
|
| 175 |
+
放大预览 · 图片 {activeIndex + 1}/{imageUrls.length}
|
| 176 |
+
</div>
|
| 177 |
+
<div className="flex items-center gap-3">
|
| 178 |
+
<button
|
| 179 |
+
type="button"
|
| 180 |
+
onClick={() => setZoom((z) => Math.max(0.5, Math.round((z - 0.1) * 10) / 10))}
|
| 181 |
+
className="px-2 py-1 rounded bg-white/15 hover:bg-white/25 text-xs"
|
| 182 |
+
>
|
| 183 |
+
-
|
| 184 |
+
</button>
|
| 185 |
+
<span className="text-xs tabular-nums">{Math.round(zoom * 100)}%</span>
|
| 186 |
+
<button
|
| 187 |
+
type="button"
|
| 188 |
+
onClick={() => setZoom((z) => Math.min(3, Math.round((z + 0.1) * 10) / 10))}
|
| 189 |
+
className="px-2 py-1 rounded bg-white/15 hover:bg-white/25 text-xs"
|
| 190 |
+
>
|
| 191 |
+
+
|
| 192 |
+
</button>
|
| 193 |
+
<button
|
| 194 |
+
type="button"
|
| 195 |
+
onClick={() => setIsLightboxOpen(false)}
|
| 196 |
+
className="px-2 py-1 rounded bg-white/15 hover:bg-white/25 text-xs"
|
| 197 |
+
>
|
| 198 |
+
关闭
|
| 199 |
+
</button>
|
| 200 |
+
</div>
|
| 201 |
+
</div>
|
| 202 |
+
<div className="flex-1 flex items-center justify-center overflow-auto px-6 pb-6">
|
| 203 |
+
<img
|
| 204 |
+
src={activeImage}
|
| 205 |
+
alt={`放大图片 ${activeIndex + 1}`}
|
| 206 |
+
className="max-w-none"
|
| 207 |
+
style={{ transform: `scale(${zoom})`, transformOrigin: 'center center' }}
|
| 208 |
+
/>
|
| 209 |
+
</div>
|
| 210 |
+
</div>
|
| 211 |
+
)}
|
| 212 |
</div>
|
| 213 |
);
|
| 214 |
}
|
frontend/vite.config.ts
CHANGED
|
@@ -32,6 +32,10 @@ export default defineConfig({
|
|
| 32 |
target: 'http://localhost:3000',
|
| 33 |
changeOrigin: true,
|
| 34 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
| 35 |
},
|
| 36 |
},
|
| 37 |
})
|
|
|
|
| 32 |
target: 'http://localhost:3000',
|
| 33 |
changeOrigin: true,
|
| 34 |
},
|
| 35 |
+
'/images': {
|
| 36 |
+
target: 'http://localhost:3000',
|
| 37 |
+
changeOrigin: true,
|
| 38 |
+
},
|
| 39 |
},
|
| 40 |
},
|
| 41 |
})
|
src/prompts/loader.ts
CHANGED
|
@@ -28,6 +28,9 @@ export interface TemplateVariables {
|
|
| 28 |
attempt?: number
|
| 29 |
instructions?: string
|
| 30 |
code?: string
|
|
|
|
|
|
|
|
|
|
| 31 |
}
|
| 32 |
|
| 33 |
/** 提示词覆盖配置 */
|
|
@@ -97,7 +100,9 @@ export function clearTemplateCache(): void {
|
|
| 97 |
/**
|
| 98 |
* 替换简单变量占位符 {{variable}}
|
| 99 |
*/
|
| 100 |
-
|
|
|
|
|
|
|
| 101 |
return template.replace(/\{\{(\w+)\}\}/g, (match, key) => {
|
| 102 |
const value = variables[key]
|
| 103 |
return value !== undefined ? String(value) : match
|
|
@@ -107,11 +112,14 @@ function replaceVariables(template: string, variables: Record<string, string | n
|
|
| 107 |
/**
|
| 108 |
* 处理条件块 {{#if variable}}...{{/if}}
|
| 109 |
*/
|
| 110 |
-
function processConditionals(template: string, variables: Record<string,
|
| 111 |
return template.replace(
|
| 112 |
/\{\{#if\s+(\w+)\}\}([\s\S]*?)\{\{\/if\}\}/g,
|
| 113 |
(_, key, content) => {
|
| 114 |
const value = variables[key]
|
|
|
|
|
|
|
|
|
|
| 115 |
return value !== undefined && value !== '' ? content : ''
|
| 116 |
}
|
| 117 |
)
|
|
@@ -135,10 +143,10 @@ function assembleTemplate(
|
|
| 135 |
.replace(/\{\{rules\}\}/g, rules)
|
| 136 |
|
| 137 |
// 3. 处理条件块
|
| 138 |
-
result = processConditionals(result, variables as Record<string,
|
| 139 |
|
| 140 |
// 4. 替换变量占位符
|
| 141 |
-
result = replaceVariables(result, variables as Record<string,
|
| 142 |
|
| 143 |
return result.trim()
|
| 144 |
}
|
|
@@ -222,13 +230,35 @@ export function getAllDefaultTemplates(): {
|
|
| 222 |
// ============================================================================
|
| 223 |
|
| 224 |
/** @deprecated 使用 getRoleUserPrompt('conceptDesigner', { concept, seed }) */
|
| 225 |
-
export function generateConceptDesignerPrompt(
|
| 226 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 227 |
}
|
| 228 |
|
| 229 |
/** @deprecated 使用 getRoleUserPrompt('codeGeneration', { concept, seed, sceneDesign }) */
|
| 230 |
-
export function generateCodeGenerationPrompt(
|
| 231 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 232 |
}
|
| 233 |
|
| 234 |
/** @deprecated 使用 getRoleUserPrompt('codeRetry', { concept, errorMessage, code, attempt }) */
|
|
@@ -242,6 +272,18 @@ export function generateCodeFixPrompt(
|
|
| 242 |
}
|
| 243 |
|
| 244 |
/** @deprecated 使用 getRoleUserPrompt('codeEdit', { concept, instructions, code }) */
|
| 245 |
-
export function generateCodeEditPrompt(
|
| 246 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 247 |
}
|
|
|
|
| 28 |
attempt?: number
|
| 29 |
instructions?: string
|
| 30 |
code?: string
|
| 31 |
+
outputMode?: 'video' | 'image'
|
| 32 |
+
isImage?: boolean
|
| 33 |
+
isVideo?: boolean
|
| 34 |
}
|
| 35 |
|
| 36 |
/** 提示词覆盖配置 */
|
|
|
|
| 100 |
/**
|
| 101 |
* 替换简单变量占位符 {{variable}}
|
| 102 |
*/
|
| 103 |
+
type TemplateValue = string | number | boolean | undefined
|
| 104 |
+
|
| 105 |
+
function replaceVariables(template: string, variables: Record<string, TemplateValue>): string {
|
| 106 |
return template.replace(/\{\{(\w+)\}\}/g, (match, key) => {
|
| 107 |
const value = variables[key]
|
| 108 |
return value !== undefined ? String(value) : match
|
|
|
|
| 112 |
/**
|
| 113 |
* 处理条件块 {{#if variable}}...{{/if}}
|
| 114 |
*/
|
| 115 |
+
function processConditionals(template: string, variables: Record<string, TemplateValue>): string {
|
| 116 |
return template.replace(
|
| 117 |
/\{\{#if\s+(\w+)\}\}([\s\S]*?)\{\{\/if\}\}/g,
|
| 118 |
(_, key, content) => {
|
| 119 |
const value = variables[key]
|
| 120 |
+
if (typeof value === 'boolean') {
|
| 121 |
+
return value ? content : ''
|
| 122 |
+
}
|
| 123 |
return value !== undefined && value !== '' ? content : ''
|
| 124 |
}
|
| 125 |
)
|
|
|
|
| 143 |
.replace(/\{\{rules\}\}/g, rules)
|
| 144 |
|
| 145 |
// 3. 处理条件块
|
| 146 |
+
result = processConditionals(result, variables as Record<string, TemplateValue>)
|
| 147 |
|
| 148 |
// 4. 替换变量占位符
|
| 149 |
+
result = replaceVariables(result, variables as Record<string, TemplateValue>)
|
| 150 |
|
| 151 |
return result.trim()
|
| 152 |
}
|
|
|
|
| 230 |
// ============================================================================
|
| 231 |
|
| 232 |
/** @deprecated 使用 getRoleUserPrompt('conceptDesigner', { concept, seed }) */
|
| 233 |
+
export function generateConceptDesignerPrompt(
|
| 234 |
+
concept: string,
|
| 235 |
+
seed: string,
|
| 236 |
+
outputMode: 'video' | 'image' = 'video'
|
| 237 |
+
): string {
|
| 238 |
+
return getRoleUserPrompt('conceptDesigner', {
|
| 239 |
+
concept,
|
| 240 |
+
seed,
|
| 241 |
+
outputMode,
|
| 242 |
+
isImage: outputMode === 'image',
|
| 243 |
+
isVideo: outputMode === 'video'
|
| 244 |
+
})
|
| 245 |
}
|
| 246 |
|
| 247 |
/** @deprecated 使用 getRoleUserPrompt('codeGeneration', { concept, seed, sceneDesign }) */
|
| 248 |
+
export function generateCodeGenerationPrompt(
|
| 249 |
+
concept: string,
|
| 250 |
+
seed: string,
|
| 251 |
+
sceneDesign?: string,
|
| 252 |
+
outputMode: 'video' | 'image' = 'video'
|
| 253 |
+
): string {
|
| 254 |
+
return getRoleUserPrompt('codeGeneration', {
|
| 255 |
+
concept,
|
| 256 |
+
seed,
|
| 257 |
+
sceneDesign,
|
| 258 |
+
outputMode,
|
| 259 |
+
isImage: outputMode === 'image',
|
| 260 |
+
isVideo: outputMode === 'video'
|
| 261 |
+
})
|
| 262 |
}
|
| 263 |
|
| 264 |
/** @deprecated 使用 getRoleUserPrompt('codeRetry', { concept, errorMessage, code, attempt }) */
|
|
|
|
| 272 |
}
|
| 273 |
|
| 274 |
/** @deprecated 使用 getRoleUserPrompt('codeEdit', { concept, instructions, code }) */
|
| 275 |
+
export function generateCodeEditPrompt(
|
| 276 |
+
concept: string,
|
| 277 |
+
instructions: string,
|
| 278 |
+
code: string,
|
| 279 |
+
outputMode: 'video' | 'image' = 'video'
|
| 280 |
+
): string {
|
| 281 |
+
return getRoleUserPrompt('codeEdit', {
|
| 282 |
+
concept,
|
| 283 |
+
instructions,
|
| 284 |
+
code,
|
| 285 |
+
outputMode,
|
| 286 |
+
isImage: outputMode === 'image',
|
| 287 |
+
isVideo: outputMode === 'video'
|
| 288 |
+
})
|
| 289 |
}
|
src/prompts/templates/roles/code-edit.md
CHANGED
|
@@ -8,8 +8,23 @@
|
|
| 8 |
### 输出要求
|
| 9 |
|
| 10 |
- **仅输出代码**:禁止解释或 Markdown 包裹
|
| 11 |
-
- **
|
| 12 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 13 |
|
| 14 |
{{knowledge}}
|
| 15 |
|
|
|
|
| 8 |
### 输出要求
|
| 9 |
|
| 10 |
- **仅输出代码**:禁止解释或 Markdown 包裹
|
| 11 |
+
- **画布边界(硬约束)**:x in [-8, 8],y in [-4.5, 4.5]。修改后任何元素都不得越界。
|
| 12 |
+
{{#if isVideo}}
|
| 13 |
+
- **锚点协议(视频)**:使用 ### START ### 开始,### END ### 结束,仅输出锚点之间的代码
|
| 14 |
+
- **结构规范(视频)**:场景类固定为 `MainScene`,统一使用 `from manim import *`
|
| 15 |
+
{{/if}}
|
| 16 |
+
{{#if isImage}}
|
| 17 |
+
- **锚点协议(图片)**:仅允许输出 YON_IMAGE 锚点块,块外禁止任何字符。
|
| 18 |
+
- **格式(图片)**:
|
| 19 |
+
- `### YON_IMAGE_1_START ###`
|
| 20 |
+
- `...python code...`
|
| 21 |
+
- `### YON_IMAGE_1_END ###`
|
| 22 |
+
- `### YON_IMAGE_2_START ###`
|
| 23 |
+
- `...python code...`
|
| 24 |
+
- `### YON_IMAGE_2_END ###`
|
| 25 |
+
- **编号规则(图片)**:编号必须从 1 开始连续递增。
|
| 26 |
+
- **结构规范(图片)**:每个块都必须包含可渲染 Scene 类,统一使用 `from manim import *`。
|
| 27 |
+
{{/if}}
|
| 28 |
|
| 29 |
{{knowledge}}
|
| 30 |
|
src/prompts/templates/roles/code-generation.md
CHANGED
|
@@ -12,13 +12,30 @@
|
|
| 12 |
|
| 13 |
- **{{concept}}**: 用户输入的数学概念或可视化需求
|
| 14 |
- **{{seed}}**: 随机种子(用于在保持逻辑严谨的前提下,对布局和细节进行微调)
|
|
|
|
| 15 |
|
| 16 |
### 产出要求
|
| 17 |
|
| 18 |
- **纯代码输出**:**严禁**输出 Markdown 代码块标识符(如 ```python),**严禁**包含任何解释性文字。输出内容应能直接作为 `.py` 文件运行
|
| 19 |
-
|
| 20 |
-
- **
|
| 21 |
-
- **
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 22 |
|
| 23 |
{{knowledge}}
|
| 24 |
|
|
@@ -32,6 +49,9 @@
|
|
| 32 |
- **逻辑关联性**:具有相同数学含义的元素必须使用相同或相近的颜色
|
| 33 |
- **视觉对比度**:重点强调的元素(如目标结论)使用高饱和度颜色(如 `YELLOW` 或 `PURE_RED`),辅助元素使用低对比度颜色(如 `GRAY` 或 `BLUE_E`)
|
| 34 |
4. **代码实现**:对照 API 索引表,确保每个方法的参数合法
|
|
|
|
|
|
|
|
|
|
| 35 |
|
| 36 |
{{rules}}
|
| 37 |
|
|
|
|
| 12 |
|
| 13 |
- **{{concept}}**: 用户输入的数学概念或可视化需求
|
| 14 |
- **{{seed}}**: 随机种子(用于在保持逻辑严谨的前提下,对布局和细节进行微调)
|
| 15 |
+
- **画布边界(硬约束)**:x in [-8, 8],y in [-4.5, 4.5]。所有对象与标签的最终位置和关键中间位置都不得越界。
|
| 16 |
|
| 17 |
### 产出要求
|
| 18 |
|
| 19 |
- **纯代码输出**:**严禁**输出 Markdown 代码块标识符(如 ```python),**严禁**包含任何解释性文字。输出内容应能直接作为 `.py` 文件运行
|
| 20 |
+
{{#if isVideo}}
|
| 21 |
+
- **锚点协议(视频)**:输出必须以 ### START ### 开始,以 ### END ### 结束,两个锚点之间只允许出现代码
|
| 22 |
+
- **结构规范(视频)**:核心类名固定为 `MainScene`(若为 3D 场景则继承自 `ThreeDScene`)。必须使用全部导入 `from manim import *`
|
| 23 |
+
- **逻辑表达(视频)**:必须通过动态动画(不仅仅是静态展示)来深度解读 `{{concept}}` 的数学内涵
|
| 24 |
+
{{/if}}
|
| 25 |
+
{{#if isImage}}
|
| 26 |
+
- **锚点协议(图片)**:输出必须只包含 YON_IMAGE 锚点块,块外禁止任何字符。
|
| 27 |
+
- **图片锚点格式**:
|
| 28 |
+
- `### YON_IMAGE_1_START ###`
|
| 29 |
+
- `...python code...`
|
| 30 |
+
- `### YON_IMAGE_1_END ###`
|
| 31 |
+
- `### YON_IMAGE_2_START ###`
|
| 32 |
+
- `...python code...`
|
| 33 |
+
- `### YON_IMAGE_2_END ###`
|
| 34 |
+
- **编号规则(图片)**:编号必须从 1 开始且连续递增。
|
| 35 |
+
- **结构规范(图片)**:每个锚点块只负责一张图,必须包含可渲染的 Scene 类,统一使用 `from manim import *`。
|
| 36 |
+
- **逻辑表达(图片)**:静态构图优先,强调“分栏布局 + 覆盖式推导”,禁止元素重叠。
|
| 37 |
+
- **边界规则(图片)**:若内容过多必须拆成更多锚点块,禁止通过越界放置来容纳内容。
|
| 38 |
+
{{/if}}
|
| 39 |
|
| 40 |
{{knowledge}}
|
| 41 |
|
|
|
|
| 49 |
- **逻辑关联性**:具有相同数学含义的元素必须使用相同或相近的颜色
|
| 50 |
- **视觉对比度**:重点强调的元素(如目标结论)使用高饱和度颜色(如 `YELLOW` 或 `PURE_RED`),辅助元素使用低对比度颜色(如 `GRAY` 或 `BLUE_E`)
|
| 51 |
4. **代码实现**:对照 API 索引表,确保每个方法的参数合法
|
| 52 |
+
{{#if isImage}}
|
| 53 |
+
5. **多图组织**:将内容拆分为多张静态图时,每张图只承载一个明确目标(概念/推导/结论),并用锚点分块输出。
|
| 54 |
+
{{/if}}
|
| 55 |
|
| 56 |
{{rules}}
|
| 57 |
|
src/prompts/templates/roles/concept-designer.md
CHANGED
|
@@ -6,6 +6,15 @@
|
|
| 6 |
|
| 7 |
**概念**: {{concept}}
|
| 8 |
**种子**: {{seed}}(用于微调方案细节,不影响核心设计)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 9 |
|
| 10 |
### 知识层(必须使用)
|
| 11 |
|
|
@@ -13,6 +22,7 @@
|
|
| 13 |
- 画布比例:16:9
|
| 14 |
- 画布中心:(0, 0)
|
| 15 |
- 坐标边界:x in [-8, 8],y in [-4.5, 4.5]
|
|
|
|
| 16 |
|
| 17 |
请在分镜中使用标准术语:Transform、Focus、Fade In、Fade Out。
|
| 18 |
|
|
@@ -45,6 +55,7 @@
|
|
| 45 |
5. 公式、标注、图形在任意时刻都不得重叠(遮挡零容忍)。
|
| 46 |
6. 对于多选项/多分支推导,禁止全量纵向堆叠,必须使用分栏布局。
|
| 47 |
7. 对于多选项/多分支推导,必须使用覆盖式推导:上一项中间推导先淡出,再进入下一项。
|
|
|
|
| 48 |
|
| 49 |
### 分镜输出要求
|
| 50 |
|
|
@@ -64,6 +75,9 @@
|
|
| 64 |
- 每一步必须建立“逻辑推导(公式/文本)”与“几何表现(图形/运动)”的时间锚点对应关系
|
| 65 |
- 每一步必须写明动态对象的起始状态、变化过程、最终状态
|
| 66 |
- 对于多选项/多分支推导,每一步必须标注当前采用的分栏结构(如左推导右结论),并写明覆盖式切换细节(谁淡出、谁保留、谁新进场)
|
|
|
|
|
|
|
|
|
|
| 67 |
|
| 68 |
4. **视觉元素规划与布局**
|
| 69 |
- 列出需要展示的几何图形/公式/函数
|
|
|
|
| 6 |
|
| 7 |
**概念**: {{concept}}
|
| 8 |
**种子**: {{seed}}(用于微调方案细节,不影响核心设计)
|
| 9 |
+
**输出模式**: {{outputMode}}
|
| 10 |
+
|
| 11 |
+
{{#if isImage}}
|
| 12 |
+
### 图片模式附加要求
|
| 13 |
+
|
| 14 |
+
- 本次任务是**静态图片模式**,不是视频动画模式。
|
| 15 |
+
- 方案必须面向“多张静态图”组织内容:每张图一个明确目标(概念图/推导图/结论图或按选项分图)。
|
| 16 |
+
- 优先确保构图清晰、分栏稳定、标注不重叠,不要求复杂运动设计。
|
| 17 |
+
{{/if}}
|
| 18 |
|
| 19 |
### 知识层(必须使用)
|
| 20 |
|
|
|
|
| 22 |
- 画布比例:16:9
|
| 23 |
- 画布中心:(0, 0)
|
| 24 |
- 坐标边界:x in [-8, 8],y in [-4.5, 4.5]
|
| 25 |
+
- **边界强约束**:所有几何图形、公式文本、标签、箭头在任意步骤(含淡入淡出和变换中间态)都不得超出上述坐标边界。
|
| 26 |
|
| 27 |
请在分镜中使用标准术语:Transform、Focus、Fade In、Fade Out。
|
| 28 |
|
|
|
|
| 55 |
5. 公式、标注、图形在任意时刻都不得重叠(遮挡零容忍)。
|
| 56 |
6. 对于多选项/多分支推导,禁止全量纵向堆叠,必须使用分栏布局。
|
| 57 |
7. 对于多选项/多分支推导,必须使用覆盖式推导:上一项中间推导先淡出,再进入下一项。
|
| 58 |
+
8. 禁止把任意元素放置到画布边界之外;若内容较多,必须缩放或分图,不得越界。
|
| 59 |
|
| 60 |
### 分镜输出要求
|
| 61 |
|
|
|
|
| 75 |
- 每一步必须建立“逻辑推导(公式/文本)”与“几何表现(图形/运动)”的时间锚点对应关系
|
| 76 |
- 每一步必须写明动态对象的起始状态、变化过程、最终状态
|
| 77 |
- 对于多选项/多分支推导,每一步必须标注当前采用的分栏结构(如左推导右结论),并写明覆盖式切换细节(谁淡出、谁保留、谁新进场)
|
| 78 |
+
{{#if isImage}}
|
| 79 |
+
- 图片模式下,步骤可解释为“逐图切换流程”:每一步对应一张静态图,并说明与上一张图的覆盖替换关系
|
| 80 |
+
{{/if}}
|
| 81 |
|
| 82 |
4. **视觉元素规划与布局**
|
| 83 |
- 列出需要展示的几何图形/公式/函数
|
src/server.ts
CHANGED
|
@@ -75,27 +75,33 @@ async function initializeApp(): Promise<void> {
|
|
| 75 |
next(err)
|
| 76 |
})
|
| 77 |
|
| 78 |
-
// 请求日志(开发环境)
|
| 79 |
-
if (isDevelopment()) {
|
| 80 |
-
app.use(requestLogger)
|
| 81 |
-
}
|
| 82 |
-
|
| 83 |
-
// 静态文件服务
|
| 84 |
-
app.use(express.static('public'))
|
|
|
|
|
|
|
| 85 |
|
| 86 |
// 挂载所有路由(包括健康检查和 API 路由)
|
| 87 |
app.use(routes)
|
| 88 |
|
| 89 |
// SPA fallback:任何非 API 请求都返回 React 的 index.html
|
| 90 |
-
app.get('*', (req, res) => {
|
| 91 |
-
// 跳过健康检查和 API 路由
|
| 92 |
-
if (req.path.startsWith('/health') || req.path.startsWith('/api')) {
|
| 93 |
-
return notFoundHandler(req, res, () => {})
|
| 94 |
-
}
|
| 95 |
-
//
|
| 96 |
-
|
| 97 |
-
|
| 98 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 99 |
return notFoundHandler(req, res, () => {})
|
| 100 |
}
|
| 101 |
})
|
|
|
|
| 75 |
next(err)
|
| 76 |
})
|
| 77 |
|
| 78 |
+
// 请求日志(开发环境)
|
| 79 |
+
if (isDevelopment()) {
|
| 80 |
+
app.use(requestLogger)
|
| 81 |
+
}
|
| 82 |
+
|
| 83 |
+
// 静态文件服务(图片/视频资源找不到时返回 404,避免错误回退到 index.html)
|
| 84 |
+
app.use('/images', express.static(path.join(process.cwd(), 'public', 'images'), { fallthrough: false }))
|
| 85 |
+
app.use('/videos', express.static(path.join(process.cwd(), 'public', 'videos'), { fallthrough: false }))
|
| 86 |
+
app.use(express.static('public'))
|
| 87 |
|
| 88 |
// 挂载所有路由(包括健康检查和 API 路由)
|
| 89 |
app.use(routes)
|
| 90 |
|
| 91 |
// SPA fallback:任何非 API 请求都返回 React 的 index.html
|
| 92 |
+
app.get('*', (req, res) => {
|
| 93 |
+
// 跳过健康检查和 API 路由
|
| 94 |
+
if (req.path.startsWith('/health') || req.path.startsWith('/api')) {
|
| 95 |
+
return notFoundHandler(req, res, () => {})
|
| 96 |
+
}
|
| 97 |
+
// 带扩展名的资源请求若未命中静态文件,不应回退 SPA
|
| 98 |
+
if (path.extname(req.path)) {
|
| 99 |
+
return notFoundHandler(req, res, () => {})
|
| 100 |
+
}
|
| 101 |
+
// 返回 React 前端的 index.html
|
| 102 |
+
const indexPath = path.join(__dirname, '..', 'public', 'index.html')
|
| 103 |
+
res.sendFile(indexPath, (err) => {
|
| 104 |
+
if (err) {
|
| 105 |
return notFoundHandler(req, res, () => {})
|
| 106 |
}
|
| 107 |
})
|
src/services/code-edit.ts
CHANGED
|
@@ -96,25 +96,14 @@ export async function generateEditedManimCode(
|
|
| 96 |
return ''
|
| 97 |
}
|
| 98 |
|
| 99 |
-
try {
|
| 100 |
const baseSystemPrompt = promptOverrides?.roles?.codeEdit?.system || SYSTEM_PROMPTS.codeEdit
|
| 101 |
const userPromptOverride = promptOverrides?.roles?.codeEdit?.user
|
| 102 |
const baseUserPrompt = userPromptOverride
|
| 103 |
-
? applyPromptTemplate(userPromptOverride, { concept, instructions, code }, promptOverrides)
|
| 104 |
-
: generateCodeEditPrompt(concept, instructions, code)
|
| 105 |
-
const
|
| 106 |
-
|
| 107 |
-
图片模式强约束:
|
| 108 |
-
1. 仅输出 YON_IMAGE 锚点块代码,块外禁止任何字符。
|
| 109 |
-
2. 锚点格式必须为 ### YON_IMAGE_n_START ### / ### YON_IMAGE_n_END ###。
|
| 110 |
-
3. 编号从 1 连续递增,检测到几组就渲染几张。
|
| 111 |
-
4. 每个块都必须包含可渲染的 Scene 类。`
|
| 112 |
-
const systemPrompt = outputMode === 'image'
|
| 113 |
-
? `${baseSystemPrompt}\n\n当前任务为图片模式,请严格遵守 YON_IMAGE 多图锚点协议。`
|
| 114 |
-
: baseSystemPrompt
|
| 115 |
-
const userPrompt = outputMode === 'image'
|
| 116 |
-
? `${baseUserPrompt}${imageModeSuffix}`
|
| 117 |
-
: baseUserPrompt
|
| 118 |
|
| 119 |
logger.info('开始 AI 修改代码', { concept, outputMode })
|
| 120 |
|
|
|
|
| 96 |
return ''
|
| 97 |
}
|
| 98 |
|
| 99 |
+
try {
|
| 100 |
const baseSystemPrompt = promptOverrides?.roles?.codeEdit?.system || SYSTEM_PROMPTS.codeEdit
|
| 101 |
const userPromptOverride = promptOverrides?.roles?.codeEdit?.user
|
| 102 |
const baseUserPrompt = userPromptOverride
|
| 103 |
+
? applyPromptTemplate(userPromptOverride, { concept, instructions, code, outputMode }, promptOverrides)
|
| 104 |
+
: generateCodeEditPrompt(concept, instructions, code, outputMode)
|
| 105 |
+
const systemPrompt = baseSystemPrompt
|
| 106 |
+
const userPrompt = baseUserPrompt
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 107 |
|
| 108 |
logger.info('开始 AI 修改代码', { concept, outputMode })
|
| 109 |
|
src/services/concept-designer.ts
CHANGED
|
@@ -111,55 +111,9 @@ function buildVisionUserMessage(
|
|
| 111 |
]
|
| 112 |
}
|
| 113 |
|
| 114 |
-
|
| 115 |
-
|
| 116 |
-
|
| 117 |
-
任务概念:${concept}
|
| 118 |
-
随机种子:${seed}
|
| 119 |
-
|
| 120 |
-
硬约束:
|
| 121 |
-
1. 画布采用 16:9,中心 (0,0),坐标边界 x[-8,8], y[-4.5,4.5]。
|
| 122 |
-
2. 每张图必须使用分栏布局,不允许把推导全部纵向堆叠。
|
| 123 |
-
3. 对多步骤/多选项题,采用覆盖式推进:当前项讲完后淡出,再展示下一项。
|
| 124 |
-
4. 零重叠:文本、公式、几何标注在静止与运动描述中都不能重叠。
|
| 125 |
-
5. 禁止伪代码,禁止输出编程语言片段。
|
| 126 |
-
6. 输出包含:核心概念拆解、分屏布局定义、分步执行指令(含镜头术语)、全局视觉规格表。
|
| 127 |
-
|
| 128 |
-
术语库(请直接使用):Transform, Focus, Fade In, Fade Out。`
|
| 129 |
-
}
|
| 130 |
-
|
| 131 |
-
function buildImageCodePrompt(concept: string, seed: string, sceneDesign: string): string {
|
| 132 |
-
return `你需要基于设计方案生成 Manim 代码,目标是“多张静态图”。
|
| 133 |
-
|
| 134 |
-
概念:${concept}
|
| 135 |
-
种子:${seed}
|
| 136 |
-
|
| 137 |
-
设计方案:
|
| 138 |
-
${sceneDesign}
|
| 139 |
-
|
| 140 |
-
输出协议(必须严格遵守):
|
| 141 |
-
1. 只输出代码,不要解释,不要 Markdown。
|
| 142 |
-
2. 代码必须由若干锚点块组成,且块外禁止任何字符:
|
| 143 |
-
### YON_IMAGE_1_START ###
|
| 144 |
-
...python code...
|
| 145 |
-
### YON_IMAGE_1_END ###
|
| 146 |
-
### YON_IMAGE_2_START ###
|
| 147 |
-
...python code...
|
| 148 |
-
### YON_IMAGE_2_END ###
|
| 149 |
-
3. 编号从 1 连续递增。
|
| 150 |
-
4. 每个块只负责一张图,必须包含可渲染的 Scene 类。
|
| 151 |
-
5. 静态构图优先:以布局、标注、结论呈现为主,避免复杂动画。
|
| 152 |
-
6. 所有图都要保证“分栏+覆盖式推导”要求,不重叠。
|
| 153 |
-
|
| 154 |
-
实现规范:
|
| 155 |
-
- 使用 from manim import *
|
| 156 |
-
- 兼容 Manim Community v0.19.2
|
| 157 |
-
- 代码必须可运行`
|
| 158 |
-
}
|
| 159 |
-
|
| 160 |
-
/**
|
| 161 |
-
* 判断是否应该在不带图片的情况下重试
|
| 162 |
-
*/
|
| 163 |
function shouldRetryWithoutImages(error: unknown): boolean {
|
| 164 |
if (!(error instanceof OpenAI.APIError)) {
|
| 165 |
return false
|
|
@@ -309,12 +263,8 @@ async function generateSceneDesign(
|
|
| 309 |
const systemPrompt = promptOverrides?.roles?.conceptDesigner?.system || SYSTEM_PROMPTS.conceptDesigner
|
| 310 |
const userPromptOverride = promptOverrides?.roles?.conceptDesigner?.user
|
| 311 |
const userPrompt = userPromptOverride
|
| 312 |
-
? applyPromptTemplate(userPromptOverride, { concept, seed }, promptOverrides)
|
| 313 |
-
: (
|
| 314 |
-
outputMode === 'image'
|
| 315 |
-
? buildImageDesignerPrompt(concept, seed)
|
| 316 |
-
: generateConceptDesignerPrompt(concept, seed)
|
| 317 |
-
)
|
| 318 |
|
| 319 |
logger.info('开始阶段1:生成场景设计方案', { concept, outputMode, seed, hasImages: !!referenceImages?.length })
|
| 320 |
|
|
@@ -438,12 +388,8 @@ async function generateCodeFromDesign(
|
|
| 438 |
const systemPrompt = promptOverrides?.roles?.codeGeneration?.system || SYSTEM_PROMPTS.codeGeneration
|
| 439 |
const userPromptOverride = promptOverrides?.roles?.codeGeneration?.user
|
| 440 |
const userPrompt = userPromptOverride
|
| 441 |
-
? applyPromptTemplate(userPromptOverride, { concept, seed, sceneDesign }, promptOverrides)
|
| 442 |
-
: (
|
| 443 |
-
outputMode === 'image'
|
| 444 |
-
? buildImageCodePrompt(concept, seed, sceneDesign)
|
| 445 |
-
: generateCodeGenerationPrompt(concept, seed, sceneDesign)
|
| 446 |
-
)
|
| 447 |
|
| 448 |
logger.info('开始阶段2:根据设计方案生成代码', { concept, outputMode, seed })
|
| 449 |
|
|
|
|
| 111 |
]
|
| 112 |
}
|
| 113 |
|
| 114 |
+
/**
|
| 115 |
+
* 判断是否应该在不带图片的情况下重试
|
| 116 |
+
*/
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 117 |
function shouldRetryWithoutImages(error: unknown): boolean {
|
| 118 |
if (!(error instanceof OpenAI.APIError)) {
|
| 119 |
return false
|
|
|
|
| 263 |
const systemPrompt = promptOverrides?.roles?.conceptDesigner?.system || SYSTEM_PROMPTS.conceptDesigner
|
| 264 |
const userPromptOverride = promptOverrides?.roles?.conceptDesigner?.user
|
| 265 |
const userPrompt = userPromptOverride
|
| 266 |
+
? applyPromptTemplate(userPromptOverride, { concept, seed, outputMode }, promptOverrides)
|
| 267 |
+
: generateConceptDesignerPrompt(concept, seed, outputMode)
|
|
|
|
|
|
|
|
|
|
|
|
|
| 268 |
|
| 269 |
logger.info('开始阶段1:生成场景设计方案', { concept, outputMode, seed, hasImages: !!referenceImages?.length })
|
| 270 |
|
|
|
|
| 388 |
const systemPrompt = promptOverrides?.roles?.codeGeneration?.system || SYSTEM_PROMPTS.codeGeneration
|
| 389 |
const userPromptOverride = promptOverrides?.roles?.codeGeneration?.user
|
| 390 |
const userPrompt = userPromptOverride
|
| 391 |
+
? applyPromptTemplate(userPromptOverride, { concept, seed, sceneDesign, outputMode }, promptOverrides)
|
| 392 |
+
: generateCodeGenerationPrompt(concept, seed, sceneDesign, outputMode)
|
|
|
|
|
|
|
|
|
|
|
|
|
| 393 |
|
| 394 |
logger.info('开始阶段2:根据设计方案生成代码', { concept, outputMode, seed })
|
| 395 |
|