Bin29 commited on
Commit
076e8e3
·
1 Parent(s): 60efc31

图片预览下载增强与双模式提示词完善

Browse files
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
- imageUrls.forEach((url, index) => {
18
- setTimeout(() => {
19
- const link = document.createElement('a');
20
- link.href = url;
21
- link.download = `manim-image-${index + 1}.png`;
22
- link.click();
23
- }, index * 150);
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
- <button
32
- onClick={handleDownloadAll}
33
- disabled={imageUrls.length === 0}
34
- 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"
35
- >
36
- <svg className="w-3.5 h-3.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
37
- <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" />
38
- </svg>
39
- 下载全部
40
- </button>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
41
  </div>
42
 
43
  <div className="flex-1 bg-black/90 flex items-center justify-center">
44
  {activeImage ? (
45
- <img src={activeImage} alt={`图片 ${activeIndex + 1}`} className="w-full h-full object-contain" />
 
 
 
 
 
 
 
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
- function replaceVariables(template: string, variables: Record<string, string | number | undefined>): string {
 
 
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, string | number | undefined>): 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, string | number | undefined>)
139
 
140
  // 4. 替换变量占位符
141
- result = replaceVariables(result, variables as Record<string, string | number | undefined>)
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(concept: string, seed: string): string {
226
- return getRoleUserPrompt('conceptDesigner', { concept, seed })
 
 
 
 
 
 
 
 
 
 
227
  }
228
 
229
  /** @deprecated 使用 getRoleUserPrompt('codeGeneration', { concept, seed, sceneDesign }) */
230
- export function generateCodeGenerationPrompt(concept: string, seed: string, sceneDesign?: string): string {
231
- return getRoleUserPrompt('codeGeneration', { concept, seed, sceneDesign })
 
 
 
 
 
 
 
 
 
 
 
 
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(concept: string, instructions: string, code: string): string {
246
- return getRoleUserPrompt('codeEdit', { concept, instructions, code })
 
 
 
 
 
 
 
 
 
 
 
 
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
- - **锚点协议**:使用 ### START ### 开始### END ### 结束,仅输出锚点之间的代码
12
- - **结构规范**:场景类固定为 `MainScene`,统一使用 `from manim import *`
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
- - **锚点协议**:输出必须以 ### START ### 开始,以 ### END ### 结束,两个锚点之间只允许出现代码
20
- - **结构规范**:核心类名固定为 `MainScene`(若为 3D 场景则继承自 `ThreeDScene`)。必须使用全部导入 `from manim import *`
21
- - **逻辑表达**:必须通过动态动画不仅仅是静态展示)来深度解读 `{{concept}}` 的数学内涵
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
- // React 前端的 index.html
96
- const indexPath = path.join(__dirname, '..', 'public', 'index.html')
97
- res.sendFile(indexPath, (err) => {
98
- if (err) {
 
 
 
 
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 imageModeSuffix = `
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
- function buildImageDesignerPrompt(concept: string, seed: string): string {
115
- return `你静态数学可视化总导演,是动画导演。请输出结构化自然语言设计方案,不要代码。
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