Bin29 commited on
Commit
92d1381
·
1 Parent(s): f41e3a0

新增二次编辑功能

Browse files
README.md CHANGED
@@ -75,7 +75,7 @@ pinned: false
75
 
76
  <br>
77
 
78
- ## 前言
79
 
80
  很荣幸在这里介绍我的新项目ManimCat,它是~一只猫~
81
 
@@ -85,7 +85,7 @@ pinned: false
85
 
86
  用户只需输入自然语言描述,系统便会通过 AI 自动生成 Manim 代码并渲染出精美的数学可视化视频,支持 LaTeX 公式、模板化生成以及代码错误自动修复,让复杂概念的动态展示变得触手可及。
87
 
88
- ## 样例
89
 
90
  ### 01
91
 
@@ -98,7 +98,7 @@ pinned: false
98
  </div>
99
 
100
 
101
- ## 技术
102
 
103
  ### 技术栈
104
 
@@ -200,11 +200,11 @@ AI_TEMPERATURE=0.7
200
  CODE_RETRY_MAX_RETRIES=4
201
  ```
202
 
203
- ## 部署
204
 
205
  请查看[部署文档](DEPLOYMENT.md)。
206
 
207
- ## 贡献
208
 
209
  我对原作品进行了比较大量的修改和重构,使其更符合我的设计想法:
210
 
@@ -295,7 +295,12 @@ CODE_RETRY_MAX_RETRIES=4
295
 
296
  </details>
297
 
298
- ## 开源与版权声明
 
 
 
 
 
299
 
300
  ### 1. 软件协议
301
  本项目后端架构及前端部分实现参考/使用了 [manim-video-generator](https://github.com/rohitg00/manim-video-generator) 的核心思想。
@@ -319,9 +324,9 @@ CODE_RETRY_MAX_RETRIES=4
319
 
320
  > ManimCat 的诞生正是为了对标并挑战这些闭源商业软件。 我希望通过开源的方式,让每一位老师都能廉价地享受到 AI 带来的教学可视化便利————你只需要支付api的费用,幸运的是,对于优秀的中国LLM大模型来说,这些花费很廉价。为了保护这一愿景不被商业机构剽窃并反向收割用户,我坚决禁止任何对本项目核心提示词及索引数据的商业授权。
321
 
322
- ## 维护说明
323
 
324
- 由于作者精力有限(个人业余兴趣开发者,非专业背景),目前完全无法对外部代码进行有效的审查和长期维护。因此本项目暂支持团队协同开发,不接受 PR。感谢理解。
325
 
326
  如果你有好的建议或发现了 Bug,欢迎提交 Issue 进行讨论,我会根据自己的节奏进行改进。如果你希望在本项目基础上进行大规模修改,欢迎 Fork 出属于你自己的版本。
327
 
 
75
 
76
  <br>
77
 
78
+ # 前言
79
 
80
  很荣幸在这里介绍我的新项目ManimCat,它是~一只猫~
81
 
 
85
 
86
  用户只需输入自然语言描述,系统便会通过 AI 自动生成 Manim 代码并渲染出精美的数学可视化视频,支持 LaTeX 公式、模板化生成以及代码错误自动修复,让复杂概念的动态展示变得触手可及。
87
 
88
+ # 样例
89
 
90
  ### 01
91
 
 
98
  </div>
99
 
100
 
101
+ # 技术
102
 
103
  ### 技术栈
104
 
 
200
  CODE_RETRY_MAX_RETRIES=4
201
  ```
202
 
203
+ # 部署
204
 
205
  请查看[部署文档](DEPLOYMENT.md)。
206
 
207
+ # 贡献
208
 
209
  我对原作品进行了比较大量的修改和重构,使其更符合我的设计想法:
210
 
 
295
 
296
  </details>
297
 
298
+
299
+ # 现状
300
+
301
+ 目前,该项目功能非常初级,最大的缺点是生成与渲染花费时间不小,非常慢。我正在试图解决这个问题!
302
+
303
+ # 开源与版权声明
304
 
305
  ### 1. 软件协议
306
  本项目后端架构及前端部分实现参考/使用了 [manim-video-generator](https://github.com/rohitg00/manim-video-generator) 的核心思想。
 
324
 
325
  > ManimCat 的诞生正是为了对标并挑战这些闭源商业软件。 我希望通过开源的方式,让每一位老师都能廉价地享受到 AI 带来的教学可视化便利————你只需要支付api的费用,幸运的是,对于优秀的中国LLM大模型来说,这些花费很廉价。为了保护这一愿景不被商业机构剽窃并反向收割用户,我坚决禁止任何对本项目核心提示词及索引数据的商业授权。
326
 
327
+ # 维护说明
328
 
329
+ 由于作者精力有限(个人业余兴趣开发者,非专业背景),可能无法对外部代码进行有效的审查和长期维护。欢迎PR修复各类问题,不过过多的修改可能作者需要很长时间的审核。感谢理解。
330
 
331
  如果你有好的建议或发现了 Bug,欢迎提交 Issue 进行讨论,我会根据自己的节奏进行改进。如果你希望在本项目基础上进行大规模修改,欢迎 Fork 出属于你自己的版本。
332
 
frontend/src/App.tsx CHANGED
@@ -1,197 +1,262 @@
1
- // 主应用组件
2
-
3
- import { useState } from 'react';
4
- import { useGeneration } from './hooks/useGeneration';
5
- import { InputForm } from './components/InputForm';
6
- import { LoadingSpinner } from './components/LoadingSpinner';
7
- import { ResultSection } from './components/ResultSection';
8
- import { ThemeToggle } from './components/ThemeToggle';
9
- import { SettingsModal } from './components/SettingsModal';
10
- import { PromptsManager } from './components/PromptsManager';
11
- import { DonationModal } from './components/DonationModal';
12
- import ManimCatLogo from './components/ManimCatLogo';
13
- import type { Quality } from './types/api';
14
-
15
- function App() {
16
- const { status, result, error, jobId, stage, generate, reset, cancel } = useGeneration();
17
- const [settingsOpen, setSettingsOpen] = useState(false);
18
- const [promptsOpen, setPromptsOpen] = useState(false);
19
- const [donationOpen, setDonationOpen] = useState(false);
20
-
21
- const handleSubmit = (data: { concept: string; quality: Quality; forceRefresh: boolean }) => {
22
- generate(data);
23
- };
24
-
25
- return (
26
- <div className="min-h-screen bg-bg-primary transition-colors duration-300">
27
- {/* 左上角图标 */}
28
- <div className="fixed top-4 left-4 z-50 flex items-center gap-2">
29
- <a
30
- href="https://github.com/Wing900/ManimCat"
31
- target="_blank"
32
- rel="noopener noreferrer"
33
- className="p-2.5 text-text-secondary/70 hover:text-text-secondary hover:bg-bg-secondary/50 rounded-full transition-all active:scale-90 active:duration-75"
34
- title="GitHub"
35
- >
36
- <svg className="w-5 h-5" fill="currentColor" viewBox="0 0 24 24">
37
- <path fillRule="evenodd" d="M12 2C6.477 2 2 6.484 2 12.017c0 4.425 2.865 8.18 6.839 9.504.5.092.682-.217.682-.483 0-.237-.008-.868-.013-1.703-2.782.605-3.369-1.343-3.369-1.343-.454-1.158-1.11-1.466-1.11-1.466-.908-.62.069-.608.069-.608 1.003.07 1.531 1.032 1.531 1.032.892 1.53 2.341 1.088 2.91.832.092-.647.35-1.088.636-1.338-2.22-.253-4.555-1.113-4.555-4.951 0-1.093.39-1.988 1.029-2.688-.103-.253-.446-1.272.098-2.65 0 0 .84-.27 2.75 1.026A9.564 9.564 0 0112 6.844c.85.004 1.705.115 2.504.337 1.909-1.296 2.747-1.027 2.747-1.027.546 1.379.202 2.398.1 2.651.64.7 1.028 1.595 1.028 2.688 0 3.848-2.339 4.695-4.566 4.943.359.309.678.92.678 1.855 0 1.338-.012 2.419-.012 2.747 0 .268.18.58.688.482A10.019 10.019 0 0022 12.017C22 6.484 17.522 2 12 2z" clipRule="evenodd" />
38
- </svg>
39
- </a>
40
- <button
41
- onClick={() => setDonationOpen(true)}
42
- className="p-2.5 text-text-secondary/70 hover:text-text-secondary hover:bg-bg-secondary/50 rounded-full transition-all active:scale-90 active:duration-75"
43
- title="请喝可乐"
44
- >
45
- <svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
46
- <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M17 8h1a4 4 0 010 8h-1m-3.413-8.866A6.501 6.501 0 0012 3c-1.93 0-3.694.84-4.9 2.176M4 20h16a1 1 0 001-1v-1a1 1 0 00-1-1H4a1 1 0 00-1 1v1a1 1 0 001 1zm1-9.5V12a3 3 0 003 3h8a3 3 0 003-3v-1.5M9 8h6" />
47
- </svg>
48
- </button>
49
- </div>
50
-
51
- {/* 右上角按钮 */}
52
- <div className="fixed top-4 right-4 z-50 flex items-center gap-2">
53
- <button
54
- onClick={() => setPromptsOpen(true)}
55
- className="p-2.5 text-text-secondary/70 hover:text-text-secondary hover:bg-bg-secondary/50 rounded-full transition-all active:scale-90 active:duration-75"
56
- title="提示词管理"
57
- >
58
- <svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
59
- <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M7 8h10M7 12h4m1 8l-4-4H5a2 2 0 01-2-2V6a2 2 0 012-2h14a2 2 0 012 2v8a2 2 0 01-2 2h-3l-4 4z" />
60
- </svg>
61
- </button>
62
- <button
63
- onClick={() => setSettingsOpen(true)}
64
- className="p-2.5 text-text-secondary/70 hover:text-text-secondary hover:bg-bg-secondary/50 rounded-full transition-all active:scale-90 active:duration-75"
65
- title="API 设置"
66
- >
67
- <svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
68
- <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M10.325 4.317c.426-1.756 2.924-1.756 3.35 0a1.724 1.724 0 002.573 1.066c1.543-.94 3.31.826 2.37 2.37a1.724 1.724 0 001.065 2.572c1.756.426 1.756 2.924 0 3.35a1.724 1.724 0 00-1.066 2.573c.94 1.543-.826 3.31-2.37 2.37a1.724 1.724 0 00-2.572 1.065c-.426 1.756-2.924 1.756-3.35 0a1.724 1.724 0 00-2.573-1.066c-1.543.94-3.31-.826-2.37-2.37a1.724 1.724 0 00-1.065-2.572c-1.756-.426-1.756-2.924 0-3.35a1.724 1.724 0 001.066-2.573c-.94-1.543.826-3.31 2.37-2.37.996.608 2.296.07 2.572-1.065z" />
69
- <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M15 12a3 3 0 11-6 0 3 3 0 016 0z" />
70
- </svg>
71
- </button>
72
- <ThemeToggle />
73
- </div>
74
-
75
- {/* 主容器 - 使用黄金分割比例调整垂直位置 */}
76
- <div className="max-w-4xl mx-auto px-4 min-h-screen flex flex-col justify-center" style={{ paddingTop: '20vh', paddingBottom: '32vh' }}>
77
- {/* 标题 */}
78
- <div className="text-center mb-12">
79
- <div className="flex items-center justify-center gap-4 mb-3">
80
- <ManimCatLogo className="w-16 h-16" />
81
- <h1 className="text-5xl sm:text-6xl font-light tracking-tight text-text-primary">
82
- ManimCat
83
- </h1>
84
- </div>
85
- <p className="text-sm text-text-secondary/70 max-w-lg mx-auto">
86
- AI 驱动 Manim 生成数学动画
87
- </p>
88
- </div>
89
-
90
- {/* 状态显示区域 */}
91
- <div className="mb-6">
92
- {status === 'idle' && (
93
- <InputForm
94
- onSubmit={handleSubmit}
95
- loading={false}
96
- />
97
- )}
98
-
99
- {status === 'processing' && (
100
- <div className="bg-bg-secondary/20 rounded-2xl p-8">
101
- <LoadingSpinner stage={stage} jobId={jobId || undefined} onCancel={cancel} />
102
- </div>
103
- )}
104
-
105
- {status === 'completed' && result && (
106
- <div
107
- className="space-y-6 animate-fade-in"
108
- style={{
109
- animation: 'fadeInUp 0.5s ease-out forwards',
110
- }}
111
- >
112
- {/* 结果展示 */}
113
- <ResultSection
114
- code={result.code || ''}
115
- videoUrl={result.video_url || ''}
116
- usedAI={result.used_ai || false}
117
- renderQuality={result.render_quality || ''}
118
- generationType={result.generation_type || ''}
119
- />
120
-
121
- {/* 重新生成按钮 */}
122
- <div className="text-center">
123
- <button
124
- onClick={reset}
125
- className="px-8 py-2.5 text-sm text-text-secondary/80 hover:text-accent transition-colors bg-bg-secondary/30 hover:bg-bg-secondary/50 rounded-full"
126
- >
127
- 生成新的动画
128
- </button>
129
- </div>
130
- </div>
131
- )}
132
-
133
- {status === 'error' && (
134
- <div className="bg-red-50/80 dark:bg-red-900/20 rounded-2xl p-6">
135
- <div className="flex items-start gap-3">
136
- <div className="text-red-500 mt-0.5">
137
- <svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
138
- <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 8v4m0 4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
139
- </svg>
140
- </div>
141
- <div className="flex-1">
142
- <p className="text-text-primary font-medium mb-1">出错了</p>
143
- <p className="text-text-secondary text-sm">{error || '生成失败,请重试'}</p>
144
- </div>
145
- </div>
146
- <div className="mt-4 flex gap-3">
147
- <button
148
- onClick={reset}
149
- className="px-4 py-2 text-sm text-accent hover:text-accent-hover transition-colors"
150
- >
151
- 重试
152
- </button>
153
- </div>
154
- </div>
155
- )}
156
- </div>
157
- </div>
158
-
159
- {/* 添加淡入上浮动画 */}
160
- <style>{`
161
- @keyframes fadeInUp {
162
- 0% {
163
- opacity: 0;
164
- transform: translateY(30px);
165
- }
166
- 100% {
167
- opacity: 1;
168
- transform: translateY(0);
169
- }
170
- }
171
- `}</style>
172
-
173
- {/* 设置模态框 */}
174
- <SettingsModal
175
- isOpen={settingsOpen}
176
- onClose={() => setSettingsOpen(false)}
177
- onSave={(config) => {
178
- console.log('保存配置:', config);
179
- }}
180
- />
181
-
182
- {/* 提示词管理 */}
183
- <PromptsManager
184
- isOpen={promptsOpen}
185
- onClose={() => setPromptsOpen(false)}
186
- />
187
-
188
- {/* 捐赠模态框 */}
189
- <DonationModal
190
- isOpen={donationOpen}
191
- onClose={() => setDonationOpen(false)}
192
- />
193
- </div>
194
- );
195
- }
196
-
197
- export default App;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // 主应用组件
2
+
3
+ import { useState, useEffect } from 'react';
4
+ import { useGeneration } from './hooks/useGeneration';
5
+ import { InputForm } from './components/InputForm';
6
+ import { LoadingSpinner } from './components/LoadingSpinner';
7
+ import { ResultSection } from './components/ResultSection';
8
+ import { AiModifyModal } from './components/AiModifyModal';
9
+ import { ThemeToggle } from './components/ThemeToggle';
10
+ import { SettingsModal } from './components/SettingsModal';
11
+ import { PromptsManager } from './components/PromptsManager';
12
+ import { DonationModal } from './components/DonationModal';
13
+ import ManimCatLogo from './components/ManimCatLogo';
14
+ import type { Quality } from './types/api';
15
+
16
+ function App() {
17
+ const { status, result, error, jobId, stage, generate, renderWithCode, modifyWithAI, reset, cancel } = useGeneration();
18
+ const [settingsOpen, setSettingsOpen] = useState(false);
19
+ const [currentCode, setCurrentCode] = useState('');
20
+ const [lastRequest, setLastRequest] = useState<{ concept: string; quality: Quality; forceRefresh: boolean } | null>(null);
21
+ const [aiModifyOpen, setAiModifyOpen] = useState(false);
22
+ const [aiModifyInput, setAiModifyInput] = useState('');
23
+
24
+ const [promptsOpen, setPromptsOpen] = useState(false);
25
+ const [donationOpen, setDonationOpen] = useState(false);
26
+
27
+ useEffect(() => {
28
+ if (result?.code) {
29
+ setCurrentCode(result.code);
30
+ }
31
+ }, [result?.code]);
32
+
33
+ const handleSubmit = (data: { concept: string; quality: Quality; forceRefresh: boolean }) => {
34
+ setLastRequest(data);
35
+ generate(data);
36
+ };
37
+
38
+ const handleRerender = () => {
39
+ if (!lastRequest || !currentCode.trim()) {
40
+ return;
41
+ }
42
+ renderWithCode({ ...lastRequest, code: currentCode });
43
+ };
44
+
45
+ const handleAiModifySubmit = () => {
46
+ if (!lastRequest || !currentCode.trim()) {
47
+ return;
48
+ }
49
+ const instructions = aiModifyInput.trim();
50
+ if (!instructions) {
51
+ return;
52
+ }
53
+ setAiModifyOpen(false);
54
+ setAiModifyInput('');
55
+ modifyWithAI({
56
+ concept: lastRequest.concept,
57
+ quality: lastRequest.quality,
58
+ instructions,
59
+ code: currentCode
60
+ });
61
+ };
62
+
63
+ const isBusy = status === 'processing';
64
+
65
+ return (
66
+ <div className="min-h-screen bg-bg-primary transition-colors duration-300">
67
+ {/* 左上角图标 */}
68
+ <div className="fixed top-4 left-4 z-50 flex items-center gap-2">
69
+ <a
70
+ href="https://github.com/Wing900/ManimCat"
71
+ target="_blank"
72
+ rel="noopener noreferrer"
73
+ className="p-2.5 text-text-secondary/70 hover:text-text-secondary hover:bg-bg-secondary/50 rounded-full transition-all active:scale-90 active:duration-75"
74
+ title="GitHub"
75
+ >
76
+ <svg className="w-5 h-5" fill="currentColor" viewBox="0 0 24 24">
77
+ <path fillRule="evenodd" d="M12 2C6.477 2 2 6.484 2 12.017c0 4.425 2.865 8.18 6.839 9.504.5.092.682-.217.682-.483 0-.237-.008-.868-.013-1.703-2.782.605-3.369-1.343-3.369-1.343-.454-1.158-1.11-1.466-1.11-1.466-.908-.62.069-.608.069-.608 1.003.07 1.531 1.032 1.531 1.032.892 1.53 2.341 1.088 2.91.832.092-.647.35-1.088.636-1.338-2.22-.253-4.555-1.113-4.555-4.951 0-1.093.39-1.988 1.029-2.688-.103-.253-.446-1.272.098-2.65 0 0 .84-.27 2.75 1.026A9.564 9.564 0 0112 6.844c.85.004 1.705.115 2.504.337 1.909-1.296 2.747-1.027 2.747-1.027.546 1.379.202 2.398.1 2.651.64.7 1.028 1.595 1.028 2.688 0 3.848-2.339 4.695-4.566 4.943.359.309.678.92.678 1.855 0 1.338-.012 2.419-.012 2.747 0 .268.18.58.688.482A10.019 10.019 0 0022 12.017C22 6.484 17.522 2 12 2z" clipRule="evenodd" />
78
+ </svg>
79
+ </a>
80
+ <button
81
+ onClick={() => setDonationOpen(true)}
82
+ className="p-2.5 text-text-secondary/70 hover:text-text-secondary hover:bg-bg-secondary/50 rounded-full transition-all active:scale-90 active:duration-75"
83
+ title="请喝可乐"
84
+ >
85
+ <svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
86
+ <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M17 8h1a4 4 0 010 8h-1m-3.413-8.866A6.501 6.501 0 0012 3c-1.93 0-3.694.84-4.9 2.176M4 20h16a1 1 0 001-1v-1a1 1 0 00-1-1H4a1 1 0 00-1 1v1a1 1 0 001 1zm1-9.5V12a3 3 0 003 3h8a3 3 0 003-3v-1.5M9 8h6" />
87
+ </svg>
88
+ </button>
89
+ </div>
90
+
91
+ {/* 右上角按钮 */}
92
+ <div className="fixed top-4 right-4 z-50 flex items-center gap-2">
93
+ <button
94
+ onClick={() => setPromptsOpen(true)}
95
+ className="p-2.5 text-text-secondary/70 hover:text-text-secondary hover:bg-bg-secondary/50 rounded-full transition-all active:scale-90 active:duration-75"
96
+ title="提示词管理"
97
+ >
98
+ <svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
99
+ <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M7 8h10M7 12h4m1 8l-4-4H5a2 2 0 01-2-2V6a2 2 0 012-2h14a2 2 0 012 2v8a2 2 0 01-2 2h-3l-4 4z" />
100
+ </svg>
101
+ </button>
102
+ <button
103
+ onClick={() => setSettingsOpen(true)}
104
+ className="p-2.5 text-text-secondary/70 hover:text-text-secondary hover:bg-bg-secondary/50 rounded-full transition-all active:scale-90 active:duration-75"
105
+ title="API 设置"
106
+ >
107
+ <svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
108
+ <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M10.325 4.317c.426-1.756 2.924-1.756 3.35 0a1.724 1.724 0 002.573 1.066c1.543-.94 3.31.826 2.37 2.37a1.724 1.724 0 001.065 2.572c1.756.426 1.756 2.924 0 3.35a1.724 1.724 0 00-1.066 2.573c.94 1.543-.826 3.31-2.37 2.37a1.724 1.724 0 00-2.572 1.065c-.426 1.756-2.924 1.756-3.35 0a1.724 1.724 0 00-2.573-1.066c-1.543.94-3.31-.826-2.37-2.37a1.724 1.724 0 00-1.065-2.572c-1.756-.426-1.756-2.924 0-3.35a1.724 1.724 0 001.066-2.573c-.94-1.543.826-3.31 2.37-2.37.996.608 2.296.07 2.572-1.065z" />
109
+ <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M15 12a3 3 0 11-6 0 3 3 0 016 0z" />
110
+ </svg>
111
+ </button>
112
+ <ThemeToggle />
113
+ </div>
114
+
115
+ {/* 主容器 - 使用黄金分割比例调整垂直位置 */}
116
+ <div className="max-w-4xl mx-auto px-4 min-h-screen flex flex-col justify-center" style={{ paddingTop: '20vh', paddingBottom: '32vh' }}>
117
+ {/* 标题 */}
118
+ <div className="text-center mb-12">
119
+ <div className="flex items-center justify-center gap-4 mb-3">
120
+ <ManimCatLogo className="w-16 h-16" />
121
+ <h1 className="text-5xl sm:text-6xl font-light tracking-tight text-text-primary">
122
+ ManimCat
123
+ </h1>
124
+ </div>
125
+ <p className="text-sm text-text-secondary/70 max-w-lg mx-auto">
126
+ 用 AI 驱动 Manim 生成数学动画
127
+ </p>
128
+ </div>
129
+
130
+ {/* 状态显示区域 */}
131
+ <div className="mb-6">
132
+ {status === 'idle' && (
133
+ <InputForm
134
+ onSubmit={handleSubmit}
135
+ loading={false}
136
+ />
137
+ )}
138
+
139
+ {status === 'processing' && (
140
+ <div className="bg-bg-secondary/20 rounded-2xl p-8">
141
+ <LoadingSpinner stage={stage} jobId={jobId || undefined} onCancel={cancel} />
142
+ </div>
143
+ )}
144
+
145
+ {status === 'completed' && result && (
146
+ <div
147
+ className="space-y-6 animate-fade-in"
148
+ style={{
149
+ animation: 'fadeInUp 0.5s ease-out forwards',
150
+ }}
151
+ >
152
+ {/* 结果展示 */}
153
+ <ResultSection
154
+ code={currentCode || result.code || ''}
155
+ videoUrl={result.video_url || ''}
156
+ usedAI={result.used_ai || false}
157
+ renderQuality={result.render_quality || ''}
158
+ generationType={result.generation_type || ''}
159
+ onCodeChange={setCurrentCode}
160
+ onRerender={handleRerender}
161
+ onAiModify={() => setAiModifyOpen(true)}
162
+ isBusy={isBusy}
163
+ />
164
+
165
+ {/* 重新生成按钮 */}
166
+ <div className="text-center">
167
+ <button
168
+ onClick={() => {
169
+ reset();
170
+ setCurrentCode('');
171
+ setLastRequest(null);
172
+ setAiModifyInput('');
173
+ setAiModifyOpen(false);
174
+ }}
175
+ className="px-8 py-2.5 text-sm text-text-secondary/80 hover:text-accent transition-colors bg-bg-secondary/30 hover:bg-bg-secondary/50 rounded-full"
176
+ >
177
+ 生成新的动画
178
+ </button>
179
+ </div>
180
+ </div>
181
+ )}
182
+
183
+ {status === 'error' && (
184
+ <div className="bg-red-50/80 dark:bg-red-900/20 rounded-2xl p-6">
185
+ <div className="flex items-start gap-3">
186
+ <div className="text-red-500 mt-0.5">
187
+ <svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
188
+ <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 8v4m0 4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
189
+ </svg>
190
+ </div>
191
+ <div className="flex-1">
192
+ <p className="text-text-primary font-medium mb-1">出错了</p>
193
+ <p className="text-text-secondary text-sm">{error || '生成失败,请重试'}</p>
194
+ </div>
195
+ </div>
196
+ <div className="mt-4 flex gap-3">
197
+ <button
198
+ onClick={() => {
199
+ reset();
200
+ setCurrentCode('');
201
+ setLastRequest(null);
202
+ setAiModifyInput('');
203
+ setAiModifyOpen(false);
204
+ }}
205
+ className="px-4 py-2 text-sm text-accent hover:text-accent-hover transition-colors"
206
+ >
207
+ 重试
208
+ </button>
209
+ </div>
210
+ </div>
211
+ )}
212
+ </div>
213
+ </div>
214
+
215
+ {/* 添加淡入上浮动画 */}
216
+ <style>{`
217
+ @keyframes fadeInUp {
218
+ 0% {
219
+ opacity: 0;
220
+ transform: translateY(30px);
221
+ }
222
+ 100% {
223
+ opacity: 1;
224
+ transform: translateY(0);
225
+ }
226
+ }
227
+ `}</style>
228
+
229
+ {/* 设置模态框 */}
230
+ <SettingsModal
231
+ isOpen={settingsOpen}
232
+ onClose={() => setSettingsOpen(false)}
233
+ onSave={(config) => {
234
+ console.log('保存配置:', config);
235
+ }}
236
+ />
237
+
238
+ {/* 提示词管理 */}
239
+ <PromptsManager
240
+ isOpen={promptsOpen}
241
+ onClose={() => setPromptsOpen(false)}
242
+ />
243
+
244
+ {/* 捐赠模态框 */}
245
+ <DonationModal
246
+ isOpen={donationOpen}
247
+ onClose={() => setDonationOpen(false)}
248
+ />
249
+
250
+ <AiModifyModal
251
+ isOpen={aiModifyOpen}
252
+ value={aiModifyInput}
253
+ loading={isBusy}
254
+ onChange={setAiModifyInput}
255
+ onClose={() => setAiModifyOpen(false)}
256
+ onSubmit={handleAiModifySubmit}
257
+ />
258
+ </div>
259
+ );
260
+ }
261
+
262
+ export default App;
frontend/src/components/AiModifyModal.tsx ADDED
@@ -0,0 +1,94 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // AI 修改对话框
2
+
3
+ interface AiModifyModalProps {
4
+ isOpen: boolean;
5
+ value: string;
6
+ loading?: boolean;
7
+ onChange: (value: string) => void;
8
+ onClose: () => void;
9
+ onSubmit: () => void;
10
+ }
11
+
12
+ export function AiModifyModal({ isOpen, value, loading = false, onChange, onClose, onSubmit }: AiModifyModalProps) {
13
+ if (!isOpen) return null;
14
+
15
+ return (
16
+ <div className="fixed inset-0 z-50 flex items-center justify-center p-4">
17
+ <div className="absolute inset-0 bg-black/50 backdrop-blur-sm" onClick={onClose} />
18
+
19
+ <div className="relative w-full max-w-lg bg-bg-secondary rounded-2xl p-6 shadow-xl animate-fade-in">
20
+ <div className="flex items-center justify-between mb-4">
21
+ <div className="flex items-center gap-2">
22
+ <svg className="w-6 h-6 text-accent" fill="none" stroke="currentColor" viewBox="0 0 24 24">
23
+ <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 4.5v15m7.5-7.5h-15" />
24
+ </svg>
25
+ <h2 className="text-lg font-medium text-text-primary">AI修改</h2>
26
+ </div>
27
+ <button
28
+ onClick={onClose}
29
+ className="p-1.5 text-text-secondary/70 hover:text-text-secondary hover:bg-bg-primary/50 rounded-full transition-all"
30
+ >
31
+ <svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
32
+ <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
33
+ </svg>
34
+ </button>
35
+ </div>
36
+
37
+ <p className="text-text-secondary text-sm mb-4">
38
+ 请描述你希望 AI 如何修改当前代码。
39
+ </p>
40
+
41
+ <div className="relative mb-6">
42
+ <label
43
+ htmlFor="aiModifyInput"
44
+ className="absolute left-4 -top-2.5 px-2 bg-bg-secondary text-xs font-medium text-text-secondary"
45
+ >
46
+ 修改意见
47
+ </label>
48
+ <textarea
49
+ id="aiModifyInput"
50
+ rows={5}
51
+ value={value}
52
+ onChange={(e) => onChange(e.target.value)}
53
+ placeholder="例如:让标题更醒目,加入颜色渐变,缩短动画时长..."
54
+ className="w-full px-4 py-4 bg-bg-secondary/50 rounded-2xl text-sm text-text-primary placeholder-text-secondary/40 focus:outline-none focus:ring-2 focus:ring-accent/20 focus:bg-bg-secondary/70 transition-all resize-none"
55
+ />
56
+ </div>
57
+
58
+ <div className="flex gap-3">
59
+ <button
60
+ onClick={onClose}
61
+ disabled={loading}
62
+ className="flex-1 px-4 py-2.5 text-sm text-text-secondary hover:text-text-primary bg-bg-primary hover:bg-bg-tertiary rounded-xl transition-all active:scale-95 disabled:opacity-50 disabled:cursor-not-allowed"
63
+ >
64
+ 取消
65
+ </button>
66
+ <button
67
+ onClick={onSubmit}
68
+ disabled={loading || value.trim().length === 0}
69
+ className="flex-1 px-4 py-2.5 text-sm text-white bg-accent hover:bg-accent-hover rounded-xl transition-all active:scale-95 disabled:opacity-50 disabled:cursor-not-allowed"
70
+ >
71
+ {loading ? '提交中...' : '提交修改'}
72
+ </button>
73
+ </div>
74
+ </div>
75
+
76
+ <style>{`
77
+ @keyframes fadeIn {
78
+ 0% {
79
+ opacity: 0;
80
+ transform: scale(0.95);
81
+ }
82
+ 100% {
83
+ opacity: 1;
84
+ transform: scale(1);
85
+ }
86
+ }
87
+ .animate-fade-in {
88
+ animation: fadeIn 0.2s ease-out forwards;
89
+ }
90
+ `}</style>
91
+ </div>
92
+ );
93
+ }
94
+
frontend/src/components/CodeView.tsx CHANGED
@@ -1,4 +1,4 @@
1
- // 代码预览组件
2
 
3
  import { Prism as SyntaxHighlighter } from 'react-syntax-highlighter';
4
  import { vscDarkPlus } from 'react-syntax-highlighter/dist/esm/styles/prism';
@@ -6,9 +6,12 @@ import { useState } from 'react';
6
 
7
  interface CodeViewProps {
8
  code: string;
 
 
 
9
  }
10
 
11
- export function CodeView({ code }: CodeViewProps) {
12
  const [copied, setCopied] = useState(false);
13
 
14
  const handleCopy = async () => {
@@ -17,6 +20,14 @@ export function CodeView({ code }: CodeViewProps) {
17
  setTimeout(() => setCopied(false), 2000);
18
  };
19
 
 
 
 
 
 
 
 
 
20
  return (
21
  <div className="h-full flex flex-col bg-bg-secondary/30 rounded-2xl overflow-hidden">
22
  {/* 顶部工具栏 */}
@@ -46,22 +57,32 @@ export function CodeView({ code }: CodeViewProps) {
46
 
47
  {/* 代码区域 */}
48
  <div className="flex-1 overflow-auto">
49
- <SyntaxHighlighter
50
- language="python"
51
- style={vscDarkPlus}
52
- customStyle={{
53
- margin: 0,
54
- padding: '1rem',
55
- fontSize: '0.75rem',
56
- lineHeight: '1.6',
57
- fontFamily: 'Monaco, Cascadia Code, Roboto Mono, monospace',
58
- background: 'transparent',
59
- }}
60
- showLineNumbers
61
- >
62
- {code}
63
- </SyntaxHighlighter>
 
 
 
 
 
 
 
 
 
 
64
  </div>
65
  </div>
66
  );
67
- }
 
1
+ // 代码预览组件
2
 
3
  import { Prism as SyntaxHighlighter } from 'react-syntax-highlighter';
4
  import { vscDarkPlus } from 'react-syntax-highlighter/dist/esm/styles/prism';
 
6
 
7
  interface CodeViewProps {
8
  code: string;
9
+ editable?: boolean;
10
+ onChange?: (value: string) => void;
11
+ disabled?: boolean;
12
  }
13
 
14
+ export function CodeView({ code, editable = false, onChange, disabled = false }: CodeViewProps) {
15
  const [copied, setCopied] = useState(false);
16
 
17
  const handleCopy = async () => {
 
20
  setTimeout(() => setCopied(false), 2000);
21
  };
22
 
23
+ const textareaClassName = [
24
+ 'w-full h-full resize-none bg-transparent p-4 text-[0.75rem] leading-relaxed',
25
+ 'font-mono text-text-primary/90 focus:outline-none',
26
+ disabled ? 'opacity-60 cursor-not-allowed' : ''
27
+ ]
28
+ .filter(Boolean)
29
+ .join(' ');
30
+
31
  return (
32
  <div className="h-full flex flex-col bg-bg-secondary/30 rounded-2xl overflow-hidden">
33
  {/* 顶部工具栏 */}
 
57
 
58
  {/* 代码区域 */}
59
  <div className="flex-1 overflow-auto">
60
+ {editable ? (
61
+ <textarea
62
+ value={code}
63
+ onChange={(event) => onChange?.(event.target.value)}
64
+ className={textareaClassName}
65
+ disabled={disabled}
66
+ spellCheck={false}
67
+ />
68
+ ) : (
69
+ <SyntaxHighlighter
70
+ language="python"
71
+ style={vscDarkPlus}
72
+ customStyle={{
73
+ margin: 0,
74
+ padding: '1rem',
75
+ fontSize: '0.75rem',
76
+ lineHeight: '1.6',
77
+ fontFamily: 'Monaco, Cascadia Code, Roboto Mono, monospace',
78
+ background: 'transparent'
79
+ }}
80
+ showLineNumbers
81
+ >
82
+ {code}
83
+ </SyntaxHighlighter>
84
+ )}
85
  </div>
86
  </div>
87
  );
88
+ }
frontend/src/components/PromptSidebar.tsx CHANGED
@@ -1,189 +1,220 @@
1
- // 提示词管理侧边栏组件
2
-
3
- import type { ReactNode } from 'react';
4
-
5
- interface SidebarItemProps {
6
- icon: ReactNode;
7
- label: string;
8
- active?: boolean;
9
- onClick?: () => void;
10
- children?: ReactNode;
11
- expanded?: boolean;
12
- onToggle?: () => void;
13
- indent?: boolean;
14
- }
15
-
16
- function SidebarItem({
17
- icon,
18
- label,
19
- active = false,
20
- onClick,
21
- children,
22
- expanded = true,
23
- onToggle,
24
- indent = false
25
- }: SidebarItemProps) {
26
- const hasChildren = !!children;
27
-
28
- return (
29
- <div className={`${indent ? 'ml-4' : ''}`}>
30
- <button
31
- onClick={() => hasChildren ? onToggle?.() : onClick?.()}
32
- className={`w-full flex items-center gap-3 px-4 py-2.5 text-sm transition-colors ${
33
- active
34
- ? 'text-accent bg-bg-secondary/50 rounded-lg'
35
- : 'text-text-secondary hover:text-text-primary hover:bg-bg-secondary/30 rounded-lg'
36
- }`}
37
- >
38
- <span className="w-5 h-5 flex items-center justify-center">{icon}</span>
39
- <span className="flex-1 text-left">{label}</span>
40
- {hasChildren && (
41
- <svg
42
- className={`w-4 h-4 transition-transform ${expanded ? 'rotate-180' : ''}`}
43
- fill="none"
44
- stroke="currentColor"
45
- viewBox="0 0 24 24"
46
- >
47
- <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 9l-7 7-7-7" />
48
- </svg>
49
- )}
50
- </button>
51
- {hasChildren && expanded && (
52
- <div className="mt-1 space-y-1">
53
- {children}
54
- </div>
55
- )}
56
- </div>
57
- );
58
- }
59
-
60
- interface PromptSidebarProps {
61
- activeSection: string;
62
- activePrompt: string;
63
- onSectionChange: (section: string, prompt: string) => void;
64
- }
65
-
66
- export function PromptSidebar({
67
- activeSection,
68
- activePrompt,
69
- onSectionChange
70
- }: PromptSidebarProps) {
71
- return (
72
- <div className="w-64 bg-bg-secondary/30 border-r border-bg-secondary/50 overflow-y-auto">
73
- <div className="p-4 space-y-6">
74
- {/* 概念设计提示词 */}
75
- <div>
76
- <h3 className="px-4 text-xs font-medium text-text-secondary/60 uppercase tracking-wider mb-2">
77
- 概念设计
78
- </h3>
79
- <div className="space-y-1">
80
- <SidebarItem
81
- icon={
82
- <svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
83
- <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M7 21a4 4 0 01-4-4V5a2 2 0 012-2h4a2 2 0 012 2v12a4 4 0 01-4 4zm0 0h12a2 2 0 002-2v-4a2 2 0 00-2-2h-2.343M11 7.343l1.657-1.657a2 2 0 012.828 0l2.829 2.829a2 2 0 010 2.828l-8.486 8.485M7 17h.01" />
84
- </svg>
85
- }
86
- label="系统示词"
87
- active={activeSection === 'system' && activePrompt === 'conceptDesigner'}
88
- onClick={() => onSectionChange('system', 'conceptDesigner')}
89
- indent
90
- />
91
- <SidebarItem
92
- icon={
93
- <svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
94
- <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M15.232 5.232l3.536 3.536m-2.036-5.036a2.5 2.5 0 113.536 3.536L6.5 21.036H3v-3.572L16.732 3.732z" />
95
- </svg>
96
- }
97
- label="用户提示词"
98
- active={activeSection === 'user' && activePrompt === 'conceptDesigner'}
99
- onClick={() => onSectionChange('user', 'conceptDesigner')}
100
- indent
101
- />
102
- </div>
103
- </div>
104
-
105
- {/* 代码生成提示词 */}
106
- <div>
107
- <h3 className="px-4 text-xs font-medium text-text-secondary/60 uppercase tracking-wider mb-2">
108
- 代码生成
109
- </h3>
110
- <div className="space-y-1">
111
- <SidebarItem
112
- icon={
113
- <svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
114
- <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M8 9l4-4 4 4m0 6l-4 4-4-4" />
115
- </svg>
116
- }
117
- label="系统提示词"
118
- active={activeSection === 'system' && activePrompt === 'codeGeneration'}
119
- onClick={() => onSectionChange('system', 'codeGeneration')}
120
- indent
121
- />
122
- <SidebarItem
123
- icon={
124
- <svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
125
- <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M10 20l4-16m4 4l4 4-4 4M6 16l-4-4 4-4" />
126
- </svg>
127
- }
128
- label="用户提示词"
129
- active={activeSection === 'user' && activePrompt === 'codeGeneration'}
130
- onClick={() => onSectionChange('user', 'codeGeneration')}
131
- indent
132
- />
133
- </div>
134
- </div>
135
-
136
- {/* 代码修复提示词 */}
137
- <div>
138
- <h3 className="px-4 text-xs font-medium text-text-secondary/60 uppercase tracking-wider mb-2">
139
- 代码修复
140
- </h3>
141
- <div className="space-y-1">
142
- <SidebarItem
143
- icon={
144
- <svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
145
- <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z" />
146
- </svg>
147
- }
148
- label="初始重试提示词"
149
- active={activeSection === 'user' && activePrompt === 'codeRetryInitial'}
150
- onClick={() => onSectionChange('user', 'codeRetryInitial')}
151
- indent
152
- />
153
- <SidebarItem
154
- icon={
155
- <svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
156
- <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M13 10V3L4 14h7v7l9-11h-7z" />
157
- </svg>
158
- }
159
- label="修复提示词"
160
- active={activeSection === 'user' && activePrompt === 'codeRetryFix'}
161
- onClick={() => onSectionChange('user', 'codeRetryFix')}
162
- indent
163
- />
164
- </div>
165
- </div>
166
-
167
- {/* 系统重试提示词 */}
168
- <div>
169
- <h3 className="px-4 text-xs font-medium text-text-secondary/60 uppercase tracking-wider mb-2">
170
- 系统重试
171
- </h3>
172
- <div className="space-y-1">
173
- <SidebarItem
174
- icon={
175
- <svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
176
- <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15" />
177
- </svg>
178
- }
179
- label="重试提示词"
180
- active={activeSection === 'system' && activePrompt === 'codeRetry'}
181
- onClick={() => onSectionChange('system', 'codeRetry')}
182
- indent
183
- />
184
- </div>
185
- </div>
186
- </div>
187
- </div>
188
- );
189
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // 提示词管理侧边栏组件
2
+
3
+ import type { ReactNode } from 'react';
4
+
5
+ interface SidebarItemProps {
6
+ icon: ReactNode;
7
+ label: string;
8
+ active?: boolean;
9
+ onClick?: () => void;
10
+ children?: ReactNode;
11
+ expanded?: boolean;
12
+ onToggle?: () => void;
13
+ indent?: boolean;
14
+ }
15
+
16
+ function SidebarItem({
17
+ icon,
18
+ label,
19
+ active = false,
20
+ onClick,
21
+ children,
22
+ expanded = true,
23
+ onToggle,
24
+ indent = false
25
+ }: SidebarItemProps) {
26
+ const hasChildren = !!children;
27
+
28
+ return (
29
+ <div className={`${indent ? 'ml-4' : ''}`}>
30
+ <button
31
+ onClick={() => hasChildren ? onToggle?.() : onClick?.()}
32
+ className={`w-full flex items-center gap-3 px-4 py-2.5 text-sm transition-colors ${
33
+ active
34
+ ? 'text-accent bg-bg-secondary/50 rounded-lg'
35
+ : 'text-text-secondary hover:text-text-primary hover:bg-bg-secondary/30 rounded-lg'
36
+ }`}
37
+ >
38
+ <span className="w-5 h-5 flex items-center justify-center">{icon}</span>
39
+ <span className="flex-1 text-left">{label}</span>
40
+ {hasChildren && (
41
+ <svg
42
+ className={`w-4 h-4 transition-transform ${expanded ? 'rotate-180' : ''}`}
43
+ fill="none"
44
+ stroke="currentColor"
45
+ viewBox="0 0 24 24"
46
+ >
47
+ <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 9l-7 7-7-7" />
48
+ </svg>
49
+ )}
50
+ </button>
51
+ {hasChildren && expanded && (
52
+ <div className="mt-1 space-y-1">
53
+ {children}
54
+ </div>
55
+ )}
56
+ </div>
57
+ );
58
+ }
59
+
60
+ interface PromptSidebarProps {
61
+ activeSection: string;
62
+ activePrompt: string;
63
+ onSectionChange: (section: string, prompt: string) => void;
64
+ }
65
+
66
+ export function PromptSidebar({
67
+ activeSection,
68
+ activePrompt,
69
+ onSectionChange
70
+ }: PromptSidebarProps) {
71
+ return (
72
+ <div className="w-64 bg-bg-secondary/30 border-r border-bg-secondary/50 overflow-y-auto">
73
+ <div className="p-4 space-y-6">
74
+ {/* 概念设计提示词 */}
75
+ <div>
76
+ <h3 className="px-4 text-xs font-medium text-text-secondary/60 uppercase tracking-wider mb-2">
77
+ 概念设计
78
+ </h3>
79
+ <div className="space-y-1">
80
+ <SidebarItem
81
+ icon={
82
+ <svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
83
+ <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M7 21a4 4 0 01-4-4V5a2 2 0 012-2h4a2 2 0 012 2v12a4 4 0 01-4 4zm0 0h12a2 2 0 002-2v-4a2 2 0 00-2-2h-2.343M11 7.343l1.657-1.657a2 2 0 012.828 0l2.829 2.829a2 2 0 010 2.828l-8.486 8.485M7 17h.01" />
84
+ </svg>
85
+ }
86
+ label="系统���示词"
87
+ active={activeSection === 'system' && activePrompt === 'conceptDesigner'}
88
+ onClick={() => onSectionChange('system', 'conceptDesigner')}
89
+ indent
90
+ />
91
+ <SidebarItem
92
+ icon={
93
+ <svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
94
+ <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M15.232 5.232l3.536 3.536m-2.036-5.036a2.5 2.5 0 113.536 3.536L6.5 21.036H3v-3.572L16.732 3.732z" />
95
+ </svg>
96
+ }
97
+ label="用户提示词"
98
+ active={activeSection === 'user' && activePrompt === 'conceptDesigner'}
99
+ onClick={() => onSectionChange('user', 'conceptDesigner')}
100
+ indent
101
+ />
102
+ </div>
103
+ </div>
104
+
105
+ {/* 代码生成提示词 */}
106
+ <div>
107
+ <h3 className="px-4 text-xs font-medium text-text-secondary/60 uppercase tracking-wider mb-2">
108
+ 代码生成
109
+ </h3>
110
+ <div className="space-y-1">
111
+ <SidebarItem
112
+ icon={
113
+ <svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
114
+ <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M8 9l4-4 4 4m0 6l-4 4-4-4" />
115
+ </svg>
116
+ }
117
+ label="系统提示词"
118
+ active={activeSection === 'system' && activePrompt === 'codeGeneration'}
119
+ onClick={() => onSectionChange('system', 'codeGeneration')}
120
+ indent
121
+ />
122
+ <SidebarItem
123
+ icon={
124
+ <svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
125
+ <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M10 20l4-16m4 4l4 4-4 4M6 16l-4-4 4-4" />
126
+ </svg>
127
+ }
128
+ label="用户提示词"
129
+ active={activeSection === 'user' && activePrompt === 'codeGeneration'}
130
+ onClick={() => onSectionChange('user', 'codeGeneration')}
131
+ indent
132
+ />
133
+ </div>
134
+ </div>
135
+
136
+ {/* 代码修复提示词 */}
137
+ <div>
138
+ <h3 className="px-4 text-xs font-medium text-text-secondary/60 uppercase tracking-wider mb-2">
139
+ 代码修复
140
+ </h3>
141
+ <div className="space-y-1">
142
+ <SidebarItem
143
+ icon={
144
+ <svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
145
+ <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z" />
146
+ </svg>
147
+ }
148
+ label="初始重试提示词"
149
+ active={activeSection === 'user' && activePrompt === 'codeRetryInitial'}
150
+ onClick={() => onSectionChange('user', 'codeRetryInitial')}
151
+ indent
152
+ />
153
+ <SidebarItem
154
+ icon={
155
+ <svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
156
+ <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M13 10V3L4 14h7v7l9-11h-7z" />
157
+ </svg>
158
+ }
159
+ label="修复提示词"
160
+ active={activeSection === 'user' && activePrompt === 'codeRetryFix'}
161
+ onClick={() => onSectionChange('user', 'codeRetryFix')}
162
+ indent
163
+ />
164
+ </div>
165
+ </div>
166
+
167
+ {/* AI修改提示词 */}
168
+ <div>
169
+ <h3 className="px-4 text-xs font-medium text-text-secondary/60 uppercase tracking-wider mb-2">
170
+ AI修改
171
+ </h3>
172
+ <div className="space-y-1">
173
+ <SidebarItem
174
+ icon={
175
+ <svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
176
+ <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 4.354a4 4 0 110 15.292M15 21H9m6 0a3 3 0 01-6 0m6 0H9" />
177
+ </svg>
178
+ }
179
+ label="系统提示词"
180
+ active={activeSection === 'system' && activePrompt === 'codeEdit'}
181
+ onClick={() => onSectionChange('system', 'codeEdit')}
182
+ indent
183
+ />
184
+ <SidebarItem
185
+ icon={
186
+ <svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
187
+ <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 4v16m8-8H4" />
188
+ </svg>
189
+ }
190
+ label="用户提示词"
191
+ active={activeSection === 'user' && activePrompt === 'codeEdit'}
192
+ onClick={() => onSectionChange('user', 'codeEdit')}
193
+ indent
194
+ />
195
+ </div>
196
+ </div>
197
+
198
+ {/* 系统重试提示词 */}
199
+ <div>
200
+ <h3 className="px-4 text-xs font-medium text-text-secondary/60 uppercase tracking-wider mb-2">
201
+ 系统重试
202
+ </h3>
203
+ <div className="space-y-1">
204
+ <SidebarItem
205
+ icon={
206
+ <svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
207
+ <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15" />
208
+ </svg>
209
+ }
210
+ label="重试提示词"
211
+ active={activeSection === 'system' && activePrompt === 'codeRetry'}
212
+ onClick={() => onSectionChange('system', 'codeRetry')}
213
+ indent
214
+ />
215
+ </div>
216
+ </div>
217
+ </div>
218
+ </div>
219
+ );
220
+ }
frontend/src/components/PromptsManager.tsx CHANGED
@@ -1,214 +1,224 @@
1
- // 提示词管理主页面组件
2
- // 包含侧边栏导航和主编辑区的完整布局
3
-
4
- import { useEffect, useState } from 'react';
5
- import { PromptSidebar } from './PromptSidebar';
6
- import { PromptInput } from './PromptInput';
7
- import { usePrompts } from '../hooks/usePrompts';
8
-
9
- interface PromptsManagerProps {
10
- isOpen: boolean;
11
- onClose: () => void;
12
- }
13
-
14
- // 提示词类型配置
15
- const PROMPT_CONFIG = {
16
- system: {
17
- conceptDesigner: {
18
- label: '概念设计系统提示词',
19
- placeholder: '输入概念设计阶段的系统提示词...',
20
- description: '用于指导 AI 理解数学概念并设计动画场景'
21
- },
22
- codeGeneration: {
23
- label: '代码生成系统提示词',
24
- placeholder: '输入代码生成阶段的系统提示词...',
25
- description: '用于指导 AI 生成符合规范的 Manim 代码'
26
- },
27
- codeRetry: {
28
- label: '系统重试提示词',
29
- placeholder: '输入系统重试阶段的提示词...',
30
- description: '用于指导 AI 在代码失败时进行重试和优化'
31
- }
32
- },
33
- user: {
34
- conceptDesigner: {
35
- label: '概念设计户提示词',
36
- placeholder: '输入概念设计阶段的用户提示词...',
37
- description: '补充说明概念设计的具体需求和风格'
38
- },
39
- codeGeneration: {
40
- label: '代码生成用户提示词',
41
- placeholder: '输入代码生成阶段的用户提示词...',
42
- description: '补充说明代码生成的具体求和规范'
43
- },
44
- codeRetryInitial: {
45
- label: '代码修复初始重试提示词',
46
- placeholder: '输入代码修复初始重试阶段的提示词...',
47
- description: '代码第一次失败时修复指导'
48
- },
49
- codeRetryFix: {
50
- label: '代码修复提示词',
51
- placeholder: '输入代码修复阶段的提示词...',
52
- description: '代码第次失败时的详细修复指导'
53
- }
54
- }
55
- };
56
-
57
- export function PromptsManager({ isOpen, onClose }: PromptsManagerProps) {
58
- const {
59
- isLoading,
60
- activeSection,
61
- activePrompt,
62
- setActiveSection,
63
- setActivePrompt,
64
- getCurrentPrompt,
65
- setCurrentPrompt,
66
- restoreDefault
67
- } = usePrompts();
68
-
69
- const [saveStatus, setSaveStatus] = useState<'idle' | 'saving' | 'success' | 'error'>('idle');
70
-
71
- const TRANSITION_MS = 400;
72
-
73
- const [shouldRender, setShouldRender] = useState(isOpen);
74
-
75
- const [isVisible, setIsVisible] = useState(isOpen);
76
-
77
- useEffect(() => {
78
- if (isOpen) {
79
- setShouldRender(true);
80
- // 延迟一帧再显示,确保初始 opacity-0 生效
81
- setTimeout(() => setIsVisible(true), 50);
82
- } else {
83
- setIsVisible(false);
84
- const timeout = window.setTimeout(() => setShouldRender(false), 400);
85
- return () => window.clearTimeout(timeout);
86
- }
87
- }, [isOpen]);
88
-
89
- // 处理侧边栏导航
90
- const handleSectionChange = (section: string, prompt: string) => {
91
- setActiveSection(section);
92
- setActivePrompt(prompt);
93
- setSaveStatus('idle');
94
- };
95
-
96
- // 处理保存
97
- const handleSave = () => {
98
- setSaveStatus('saving');
99
- setTimeout(() => {
100
- setSaveStatus('success');
101
- setTimeout(() => setSaveStatus('idle'), 2000);
102
- }, 500);
103
- };
104
-
105
- // 处理恢复默认
106
- const handleRestoreDefault = () => {
107
- restoreDefault();
108
- setSaveStatus('success');
109
- setTimeout(() => setSaveStatus('idle'), 2000);
110
- };
111
-
112
- // 获取当前配置
113
- const currentConfig = activeSection === 'system'
114
- ? PROMPT_CONFIG.system[activePrompt as keyof typeof PROMPT_CONFIG.system]
115
- : PROMPT_CONFIG.user[activePrompt as keyof typeof PROMPT_CONFIG.user];
116
-
117
- if (!shouldRender) return null;
118
-
119
- return (
120
- <div
121
- className={`fixed inset-0 z-50 flex flex-col bg-bg-primary transition-all duration-[400ms] ease-out ${isVisible ? 'opacity-100 translate-y-0 scale-100' : 'opacity-0 translate-y-8 scale-95 pointer-events-none'}`}
122
- >
123
- {/* 顶部导航栏 */}
124
- <div className="h-16 bg-bg-secondary border-b border-bg-secondary/50 flex items-center justify-between px-6">
125
- <div className="flex items-center gap-4">
126
- <button
127
- onClick={onClose}
128
- className="p-2 text-text-secondary hover:text-text-primary hover:bg-bg-secondary/50 rounded-lg transition-colors"
129
- title="返回主界面"
130
- >
131
- <svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
132
- <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M15 19l-7-7 7-7" />
133
- </svg>
134
- </button>
135
- <h1 className="text-lg font-medium text-text-primary">提示词管理</h1>
136
- </div>
137
- <div className="flex items-center gap-3">
138
- {saveStatus === 'success' && (
139
- <div className="flex items-center gap-2 text-sm text-green-600 dark:text-green-400">
140
- <svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
141
- <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M5 13l4 4L19 7" />
142
- </svg>
143
- <span>保存成功</span>
144
- </div>
145
- )}
146
- <button
147
- onClick={handleSave}
148
- disabled={saveStatus === 'saving'}
149
- className="px-4 py-2 bg-accent text-white text-sm font-medium rounded-lg hover:bg-accent-hover disabled:opacity-50 disabled:cursor-not-allowed transition-all"
150
- >
151
- {saveStatus === 'saving' ? (
152
- <>
153
- <svg className="animate-spin w-4 h-4 inline mr-2" fill="none" viewBox="0 0 24 24">
154
- <circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4" />
155
- <path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z" />
156
- </svg>
157
- 保存中...
158
- </>
159
- ) : (
160
- '保存'
161
- )}
162
- </button>
163
- </div>
164
- </div>
165
-
166
- {/* 主内容区 */}
167
- <div className="flex-1 flex overflow-hidden">
168
- {/* 侧边栏 */}
169
- <PromptSidebar
170
- activeSection={activeSection}
171
- activePrompt={activePrompt}
172
- onSectionChange={handleSectionChange}
173
- />
174
-
175
- {/* 主编辑区 */}
176
- <div className="flex-1 overflow-y-auto p-8">
177
- <div className="max-w-4xl mx-auto space-y-8">
178
- {/* 当前提示词信息 */}
179
- <div className="bg-bg-secondary/30 rounded-xl p-6">
180
- <h2 className="text-xl font-medium text-text-primary mb-2">
181
- {currentConfig?.label || '提示词编辑'}
182
- </h2>
183
- <p className="text-sm text-text-secondary/70">
184
- {currentConfig?.description || '在此处编辑提示词,它将用于指导 AI 生成和优化动画'}
185
- </p>
186
- </div>
187
-
188
- {/* 输入组件 */}
189
- <div className="space-y-6">
190
- <PromptInput
191
- value={getCurrentPrompt()}
192
- onChange={setCurrentPrompt}
193
- label={currentConfig?.label || '提示词'}
194
- placeholder={currentConfig?.placeholder}
195
- showWordCount
196
- disabled={isLoading}
197
- onSave={handleSave}
198
- onRestoreDefault={handleRestoreDefault}
199
- />
200
-
201
- </div>
202
- </div>
203
- </div>
204
- </div>
205
-
206
- {/* 底部状态栏 */}
207
- <div className="h-10 bg-bg-secondary border-t border-bg-secondary/50 flex items-center justify-between px-6 text-xs text-text-secondary">
208
- <span>提示词管理</span>
209
- <span>字符限制:20000</span>
210
- </div>
211
- </div>
212
- );
213
- }
214
-
 
 
 
 
 
 
 
 
 
 
 
1
+ // 提示词管理主页面组件
2
+ // 包含侧边栏导航和主编辑区的完整布局
3
+
4
+ import { useEffect, useState } from 'react';
5
+ import { PromptSidebar } from './PromptSidebar';
6
+ import { PromptInput } from './PromptInput';
7
+ import { usePrompts } from '../hooks/usePrompts';
8
+
9
+ interface PromptsManagerProps {
10
+ isOpen: boolean;
11
+ onClose: () => void;
12
+ }
13
+
14
+ // 提示词类型配置
15
+ const PROMPT_CONFIG = {
16
+ system: {
17
+ conceptDesigner: {
18
+ label: '概念设计系统提示词',
19
+ placeholder: '输入概念设计阶段的系统提示词...',
20
+ description: '用于指导 AI 理解数学概念并设计动画场景'
21
+ },
22
+ codeGeneration: {
23
+ label: '代码生成系统提示词',
24
+ placeholder: '输入代码生成阶段的系统提示词...',
25
+ description: '用于指导 AI 生成符合规范的 Manim 代码'
26
+ },
27
+ codeRetry: {
28
+ label: '系统重试提示词',
29
+ placeholder: '输入系统重试阶段的提示词...',
30
+ description: '用于指导 AI 在代码失败时进行重试和优化'
31
+ },
32
+ codeEdit: {
33
+ label: 'AI修改系统提示词',
34
+ placeholder: '用于 AI 修改阶段的系统提示词...',
35
+ description: '用于指导 AI 基于现有代码进行修改'
36
+ }
37
+ },
38
+ user: {
39
+ conceptDesigner: {
40
+ label: '概念设计用户提示词',
41
+ placeholder: '输入概念设计阶段的用户提示词...',
42
+ description: '补充说明概念设计的具体求和风格'
43
+ },
44
+ codeGeneration: {
45
+ label: '代码生成用户提示词',
46
+ placeholder: '输入代码生成阶段的用户提示词...',
47
+ description: '补充说明代码生成具体要求和规范'
48
+ },
49
+ codeRetryInitial: {
50
+ label: '代码修复初始重试提示词',
51
+ placeholder: '输入代码修复初始重试阶段的提示词...',
52
+ description: '代码第次失败时的修复指导'
53
+ },
54
+ codeRetryFix: {
55
+ label: '代码修复提示词',
56
+ placeholder: '输入代码修复阶段的提示词...',
57
+ description: '代码第二次失败时的详细修复指导'
58
+ },
59
+ codeEdit: {
60
+ label: 'AI修改用户提示词',
61
+ placeholder: '用于 AI 修改阶段的用户提示词...',
62
+ description: '用于描述修改目标、约束与期望效果'
63
+ }
64
+ }
65
+ };
66
+
67
+ export function PromptsManager({ isOpen, onClose }: PromptsManagerProps) {
68
+ const {
69
+ isLoading,
70
+ activeSection,
71
+ activePrompt,
72
+ setActiveSection,
73
+ setActivePrompt,
74
+ getCurrentPrompt,
75
+ setCurrentPrompt,
76
+ restoreDefault
77
+ } = usePrompts();
78
+
79
+ const [saveStatus, setSaveStatus] = useState<'idle' | 'saving' | 'success' | 'error'>('idle');
80
+
81
+ const TRANSITION_MS = 400;
82
+
83
+ const [shouldRender, setShouldRender] = useState(isOpen);
84
+
85
+ const [isVisible, setIsVisible] = useState(isOpen);
86
+
87
+ useEffect(() => {
88
+ if (isOpen) {
89
+ setShouldRender(true);
90
+ // 延迟一帧再显示,确保初始 opacity-0 生效
91
+ setTimeout(() => setIsVisible(true), 50);
92
+ } else {
93
+ setIsVisible(false);
94
+ const timeout = window.setTimeout(() => setShouldRender(false), 400);
95
+ return () => window.clearTimeout(timeout);
96
+ }
97
+ }, [isOpen]);
98
+
99
+ // 处理侧边栏导航
100
+ const handleSectionChange = (section: string, prompt: string) => {
101
+ setActiveSection(section);
102
+ setActivePrompt(prompt);
103
+ setSaveStatus('idle');
104
+ };
105
+
106
+ // 处理保存
107
+ const handleSave = () => {
108
+ setSaveStatus('saving');
109
+ setTimeout(() => {
110
+ setSaveStatus('success');
111
+ setTimeout(() => setSaveStatus('idle'), 2000);
112
+ }, 500);
113
+ };
114
+
115
+ // 处理恢复默认
116
+ const handleRestoreDefault = () => {
117
+ restoreDefault();
118
+ setSaveStatus('success');
119
+ setTimeout(() => setSaveStatus('idle'), 2000);
120
+ };
121
+
122
+ // 获取当前配置
123
+ const currentConfig = activeSection === 'system'
124
+ ? PROMPT_CONFIG.system[activePrompt as keyof typeof PROMPT_CONFIG.system]
125
+ : PROMPT_CONFIG.user[activePrompt as keyof typeof PROMPT_CONFIG.user];
126
+
127
+ if (!shouldRender) return null;
128
+
129
+ return (
130
+ <div
131
+ className={`fixed inset-0 z-50 flex flex-col bg-bg-primary transition-all duration-[400ms] ease-out ${isVisible ? 'opacity-100 translate-y-0 scale-100' : 'opacity-0 translate-y-8 scale-95 pointer-events-none'}`}
132
+ >
133
+ {/* 顶部导航栏 */}
134
+ <div className="h-16 bg-bg-secondary border-b border-bg-secondary/50 flex items-center justify-between px-6">
135
+ <div className="flex items-center gap-4">
136
+ <button
137
+ onClick={onClose}
138
+ className="p-2 text-text-secondary hover:text-text-primary hover:bg-bg-secondary/50 rounded-lg transition-colors"
139
+ title="返回主界面"
140
+ >
141
+ <svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
142
+ <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M15 19l-7-7 7-7" />
143
+ </svg>
144
+ </button>
145
+ <h1 className="text-lg font-medium text-text-primary">提示词管理</h1>
146
+ </div>
147
+ <div className="flex items-center gap-3">
148
+ {saveStatus === 'success' && (
149
+ <div className="flex items-center gap-2 text-sm text-green-600 dark:text-green-400">
150
+ <svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
151
+ <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M5 13l4 4L19 7" />
152
+ </svg>
153
+ <span>保存成功</span>
154
+ </div>
155
+ )}
156
+ <button
157
+ onClick={handleSave}
158
+ disabled={saveStatus === 'saving'}
159
+ className="px-4 py-2 bg-accent text-white text-sm font-medium rounded-lg hover:bg-accent-hover disabled:opacity-50 disabled:cursor-not-allowed transition-all"
160
+ >
161
+ {saveStatus === 'saving' ? (
162
+ <>
163
+ <svg className="animate-spin w-4 h-4 inline mr-2" fill="none" viewBox="0 0 24 24">
164
+ <circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4" />
165
+ <path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z" />
166
+ </svg>
167
+ 保存中...
168
+ </>
169
+ ) : (
170
+ '保存'
171
+ )}
172
+ </button>
173
+ </div>
174
+ </div>
175
+
176
+ {/* 主内容区 */}
177
+ <div className="flex-1 flex overflow-hidden">
178
+ {/* 侧边栏 */}
179
+ <PromptSidebar
180
+ activeSection={activeSection}
181
+ activePrompt={activePrompt}
182
+ onSectionChange={handleSectionChange}
183
+ />
184
+
185
+ {/* 主编辑区 */}
186
+ <div className="flex-1 overflow-y-auto p-8">
187
+ <div className="max-w-4xl mx-auto space-y-8">
188
+ {/* 当前提示词信息 */}
189
+ <div className="bg-bg-secondary/30 rounded-xl p-6">
190
+ <h2 className="text-xl font-medium text-text-primary mb-2">
191
+ {currentConfig?.label || '提示词编辑'}
192
+ </h2>
193
+ <p className="text-sm text-text-secondary/70">
194
+ {currentConfig?.description || '在此处编辑提示词,它将用于指导 AI 生成和优化动画'}
195
+ </p>
196
+ </div>
197
+
198
+ {/* 输入组件 */}
199
+ <div className="space-y-6">
200
+ <PromptInput
201
+ value={getCurrentPrompt()}
202
+ onChange={setCurrentPrompt}
203
+ label={currentConfig?.label || '提示词'}
204
+ placeholder={currentConfig?.placeholder}
205
+ showWordCount
206
+ disabled={isLoading}
207
+ onSave={handleSave}
208
+ onRestoreDefault={handleRestoreDefault}
209
+ />
210
+
211
+ </div>
212
+ </div>
213
+ </div>
214
+ </div>
215
+
216
+ {/* 底部状态栏 */}
217
+ <div className="h-10 bg-bg-secondary border-t border-bg-secondary/50 flex items-center justify-between px-6 text-xs text-text-secondary">
218
+ <span>提示词管理</span>
219
+ <span>字符限制:20000</span>
220
+ </div>
221
+ </div>
222
+ );
223
+ }
224
+
frontend/src/components/ResultSection.tsx CHANGED
@@ -1,4 +1,4 @@
1
- // 结果展示组件
2
 
3
  import { CodeView } from './CodeView';
4
  import { VideoPreview } from './VideoPreview';
@@ -9,27 +9,66 @@ interface ResultSectionProps {
9
  usedAI: boolean;
10
  renderQuality: string;
11
  generationType: string;
 
 
 
 
12
  }
13
 
14
- export function ResultSection({ code, videoUrl, usedAI, renderQuality, generationType }: ResultSectionProps) {
 
 
 
 
 
 
 
 
 
 
 
 
15
  return (
16
  <div className="w-full max-w-4xl mx-auto space-y-5">
17
- {/* 代码视频预览 */}
18
  <div className="grid grid-cols-1 lg:grid-cols-2 gap-4">
19
  <div className="h-[360px]">
20
- <CodeView code={code} />
21
  </div>
22
  <div className="h-[360px]">
23
  <VideoPreview videoUrl={videoUrl} />
24
  </div>
25
  </div>
26
 
27
- {/* 生成信息 */}
28
  <div className="bg-bg-secondary/30 rounded-xl px-4 py-2.5">
29
  <p className="text-xs text-text-secondary/70">
30
  {generationType}{usedAI ? ' (AI)' : ''} · {renderQuality}
31
  </p>
32
  </div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
33
  </div>
34
  );
35
- }
 
1
+ // 结果展示区域
2
 
3
  import { CodeView } from './CodeView';
4
  import { VideoPreview } from './VideoPreview';
 
9
  usedAI: boolean;
10
  renderQuality: string;
11
  generationType: string;
12
+ onCodeChange?: (code: string) => void;
13
+ onRerender?: () => void;
14
+ onAiModify?: () => void;
15
+ isBusy?: boolean;
16
  }
17
 
18
+ export function ResultSection({
19
+ code,
20
+ videoUrl,
21
+ usedAI,
22
+ renderQuality,
23
+ generationType,
24
+ onCodeChange,
25
+ onRerender,
26
+ onAiModify,
27
+ isBusy = false
28
+ }: ResultSectionProps) {
29
+ const hasActions = onRerender || onAiModify;
30
+
31
  return (
32
  <div className="w-full max-w-4xl mx-auto space-y-5">
33
+ {/* 代码视频预览 */}
34
  <div className="grid grid-cols-1 lg:grid-cols-2 gap-4">
35
  <div className="h-[360px]">
36
+ <CodeView code={code} editable={Boolean(onCodeChange)} onChange={onCodeChange} disabled={isBusy} />
37
  </div>
38
  <div className="h-[360px]">
39
  <VideoPreview videoUrl={videoUrl} />
40
  </div>
41
  </div>
42
 
43
+ {/* 结果信息 */}
44
  <div className="bg-bg-secondary/30 rounded-xl px-4 py-2.5">
45
  <p className="text-xs text-text-secondary/70">
46
  {generationType}{usedAI ? ' (AI)' : ''} · {renderQuality}
47
  </p>
48
  </div>
49
+
50
+ {hasActions && (
51
+ <div className="flex flex-wrap items-center justify-center gap-3">
52
+ {onRerender && (
53
+ <button
54
+ onClick={onRerender}
55
+ disabled={isBusy}
56
+ className="px-5 py-2 text-xs font-medium text-text-secondary/80 hover:text-text-primary bg-bg-secondary/30 hover:bg-bg-secondary/50 rounded-full transition-all disabled:opacity-50 disabled:cursor-not-allowed"
57
+ >
58
+ 重新渲染
59
+ </button>
60
+ )}
61
+ {onAiModify && (
62
+ <button
63
+ onClick={onAiModify}
64
+ disabled={isBusy}
65
+ className="px-5 py-2 text-xs font-medium text-white bg-accent hover:bg-accent-hover rounded-full transition-all disabled:opacity-50 disabled:cursor-not-allowed shadow-sm shadow-accent/20"
66
+ >
67
+ AI修改
68
+ </button>
69
+ )}
70
+ </div>
71
+ )}
72
  </div>
73
  );
74
+ }
frontend/src/hooks/useGeneration.ts CHANGED
@@ -1,10 +1,10 @@
1
  // 生成请求 Hook
2
 
3
  import { useState, useCallback, useRef, useEffect } from 'react';
4
- import { generateAnimation, getJobStatus, cancelJob } from '../lib/api';
5
  import { loadCustomConfig, generateWithCustomApi } from '../lib/custom-ai';
6
  import { loadPrompts } from './usePrompts';
7
- import type { GenerateRequest, JobResult, ProcessingStage, VideoConfig } from '../types/api';
8
 
9
  interface UseGenerationReturn {
10
  status: 'idle' | 'processing' | 'completed' | 'error';
@@ -13,6 +13,8 @@ interface UseGenerationReturn {
13
  jobId: string | null;
14
  stage: ProcessingStage;
15
  generate: (request: GenerateRequest) => Promise<void>;
 
 
16
  reset: () => void;
17
  cancel: () => void;
18
  }
@@ -165,6 +167,56 @@ export function useGeneration(): UseGenerationReturn {
165
  }, POLL_INTERVAL);
166
  }, [requestCancel, updateStage]);
167
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
168
  // 生成动画
169
  const generate = useCallback(async (request: GenerateRequest) => {
170
  setStatus('processing');
@@ -247,6 +299,8 @@ export function useGeneration(): UseGenerationReturn {
247
  jobId,
248
  stage,
249
  generate,
 
 
250
  reset,
251
  cancel,
252
  };
 
1
  // 生成请求 Hook
2
 
3
  import { useState, useCallback, useRef, useEffect } from 'react';
4
+ import { generateAnimation, getJobStatus, cancelJob, modifyAnimation } from '../lib/api';
5
  import { loadCustomConfig, generateWithCustomApi } from '../lib/custom-ai';
6
  import { loadPrompts } from './usePrompts';
7
+ import type { GenerateRequest, JobResult, ProcessingStage, VideoConfig, ModifyRequest } from '../types/api';
8
 
9
  interface UseGenerationReturn {
10
  status: 'idle' | 'processing' | 'completed' | 'error';
 
13
  jobId: string | null;
14
  stage: ProcessingStage;
15
  generate: (request: GenerateRequest) => Promise<void>;
16
+ renderWithCode: (request: GenerateRequest & { code: string }) => Promise<void>;
17
+ modifyWithAI: (request: ModifyRequest) => Promise<void>;
18
  reset: () => void;
19
  cancel: () => void;
20
  }
 
167
  }, POLL_INTERVAL);
168
  }, [requestCancel, updateStage]);
169
 
170
+ // 使用现有代码重新渲染
171
+ const renderWithCode = useCallback(async (request: GenerateRequest & { code: string }) => {
172
+ setStatus('processing');
173
+ setError(null);
174
+ setResult(null);
175
+ setStage('rendering');
176
+ pollCountRef.current = 0;
177
+ abortControllerRef.current = new AbortController();
178
+
179
+ try {
180
+ const promptOverrides = loadPrompts();
181
+ const response = await generateAnimation(
182
+ { ...request, promptOverrides },
183
+ abortControllerRef.current.signal
184
+ );
185
+ startPolling(response.jobId);
186
+ } catch (err) {
187
+ if (err instanceof Error && err.name === 'AbortError') {
188
+ return;
189
+ }
190
+ setStatus('error');
191
+ setError(err instanceof Error ? err.message : '重新渲染失败');
192
+ }
193
+ }, [startPolling]);
194
+
195
+ // AI 修改后渲染
196
+ const modifyWithAI = useCallback(async (request: ModifyRequest) => {
197
+ setStatus('processing');
198
+ setError(null);
199
+ setResult(null);
200
+ setStage('generating');
201
+ pollCountRef.current = 0;
202
+ abortControllerRef.current = new AbortController();
203
+
204
+ try {
205
+ const promptOverrides = loadPrompts();
206
+ const response = await modifyAnimation(
207
+ { ...request, promptOverrides },
208
+ abortControllerRef.current.signal
209
+ );
210
+ startPolling(response.jobId);
211
+ } catch (err) {
212
+ if (err instanceof Error && err.name === 'AbortError') {
213
+ return;
214
+ }
215
+ setStatus('error');
216
+ setError(err instanceof Error ? err.message : 'AI 修改失败');
217
+ }
218
+ }, [startPolling]);
219
+
220
  // 生成动画
221
  const generate = useCallback(async (request: GenerateRequest) => {
222
  setStatus('processing');
 
299
  jobId,
300
  stage,
301
  generate,
302
+ renderWithCode,
303
+ modifyWithAI,
304
  reset,
305
  cancel,
306
  };
frontend/src/hooks/usePrompts.ts CHANGED
@@ -1,173 +1,175 @@
1
- // 提示词管理 Hook
2
- // 负责提示词的状态管理、存储和加载
3
-
4
- import { useState, useEffect, useCallback } from 'react';
5
- import { getPromptDefaults } from '../lib/api';
6
- import type { PromptOverrides } from '../types/api';
7
-
8
- /** 存储在 localStorage 中的键名 */
9
- const PROMPTS_STORAGE_KEY = 'manimcat_prompt_overrides';
10
-
11
- /** 默认提示词配置 */
12
- const DEFAULT_PROMPTS: PromptOverrides = {
13
- system: {
14
- conceptDesigner: '',
15
- codeGeneration: '',
16
- codeRetry: '',
17
- },
18
- user: {
19
- conceptDesigner: '',
20
- codeGeneration: '',
21
- codeRetryInitial: '',
22
- codeRetryFix: '',
23
- },
24
- };
25
-
26
- /** 从 localStorage 加载提示词配置 */
27
- /** Load prompt overrides from localStorage if available. */
28
- export function loadStoredPrompts(): PromptOverrides | null {
29
- try {
30
- const saved = localStorage.getItem(PROMPTS_STORAGE_KEY);
31
- if (saved) {
32
- const parsed = JSON.parse(saved);
33
- return {
34
- system: { ...DEFAULT_PROMPTS.system, ...parsed.system },
35
- user: { ...DEFAULT_PROMPTS.user, ...parsed.user },
36
- };
37
- }
38
- } catch (error) {
39
- console.error('Failed to load prompts:', error);
40
- }
41
- return null;
42
- }
43
-
44
- export function loadPrompts(): PromptOverrides {
45
- return loadStoredPrompts() || DEFAULT_PROMPTS;
46
- }
47
-
48
- /** 保存提示词配置到 localStorage */
49
- export function savePrompts(prompts: PromptOverrides): void {
50
- try {
51
- localStorage.setItem(PROMPTS_STORAGE_KEY, JSON.stringify(prompts));
52
- } catch (error) {
53
- console.error('Failed to save prompts:', error);
54
- }
55
- }
56
-
57
- /** 提示词管理 Hook */
58
- export function usePrompts() {
59
- const [prompts, setPrompts] = useState<PromptOverrides>(DEFAULT_PROMPTS);
60
- const [defaultPrompts, setDefaultPrompts] = useState<PromptOverrides>(DEFAULT_PROMPTS);
61
- const [isLoading, setIsLoading] = useState<boolean>(true);
62
- const [activeSection, setActiveSection] = useState<string>('system');
63
- const [activePrompt, setActivePrompt] = useState<string>('conceptDesigner');
64
-
65
- // localStorage 加载配置
66
- useEffect(() => {
67
- const savedPrompts = loadStoredPrompts();
68
- if (savedPrompts) {
69
- setPrompts(savedPrompts);
70
- }
71
-
72
- let isActive = true;
73
- const loadDefaults = async () => {
74
- setIsLoading(true);
75
- try {
76
- const defaults = await getPromptDefaults();
77
- if (isActive) {
78
- setDefaultPrompts(defaults);
79
- }
80
- } catch (error) {
81
- console.error('Failed to load prompt defaults:', error);
82
- } finally {
83
- if (isActive) {
84
- setIsLoading(false);
85
- }
86
- }
87
- };
88
-
89
- void loadDefaults();
90
- return () => {
91
- isActive = false;
92
- };
93
- }, []);
94
-
95
- // 保存到 localStorage
96
- useEffect(() => {
97
- savePrompts(prompts);
98
- }, [prompts]);
99
-
100
- // 更新提示词
101
- const updatePrompt = useCallback((section: string, type: string, value: string) => {
102
- setPrompts(prev => {
103
- const newPrompts = { ...prev };
104
-
105
- if (section === 'system') {
106
- if (!newPrompts.system) {
107
- newPrompts.system = {};
108
- }
109
- newPrompts.system[type as keyof PromptOverrides['system']] = value;
110
- } else if (section === 'user') {
111
- if (!newPrompts.user) {
112
- newPrompts.user = {};
113
- }
114
- newPrompts.user[type as keyof PromptOverrides['user']] = value;
115
- }
116
-
117
- return newPrompts;
118
- });
119
- }, []);
120
-
121
- // 恢复默认值
122
- const restoreDefault = useCallback(() => {
123
- setPrompts(DEFAULT_PROMPTS);
124
- }, []);
125
-
126
- // 获取当前编辑的提示词
127
- const getDefaultPrompt = useCallback((section: string, type: string) => {
128
- if (section === 'system') {
129
- return defaultPrompts.system?.[type as keyof PromptOverrides['system']] || '';
130
- }
131
- if (section === 'user') {
132
- return defaultPrompts.user?.[type as keyof PromptOverrides['user']] || '';
133
- }
134
- return '';
135
- }, [defaultPrompts]);
136
-
137
- //
138
- const getCurrentPrompt = useCallback(() => {
139
- let current = '';
140
- if (activeSection === 'system') {
141
- current = prompts.system?.[activePrompt as keyof PromptOverrides['system']] || '';
142
- } else if (activeSection === 'user') {
143
- current = prompts.user?.[activePrompt as keyof PromptOverrides['user']] || '';
144
- }
145
-
146
- if (current && current.trim().length > 0) {
147
- return current;
148
- }
149
-
150
- return getDefaultPrompt(activeSection, activePrompt);
151
- }, [prompts, activeSection, activePrompt, getDefaultPrompt]);
152
-
153
- //
154
- const setCurrentPrompt = useCallback((value: string) => {
155
- const defaultValue = getDefaultPrompt(activeSection, activePrompt);
156
- const nextValue = value === defaultValue ? '' : value;
157
- updatePrompt(activeSection, activePrompt, nextValue);
158
- }, [activeSection, activePrompt, getDefaultPrompt, updatePrompt]);
159
-
160
- return {
161
- prompts,
162
- defaultPrompts,
163
- isLoading,
164
- activeSection,
165
- activePrompt,
166
- setActiveSection,
167
- setActivePrompt,
168
- updatePrompt,
169
- restoreDefault,
170
- getCurrentPrompt,
171
- setCurrentPrompt,
172
- };
173
- }
 
 
 
1
+ // 提示词管理 Hook
2
+ // 负责提示词的状态管理、存储和加载
3
+
4
+ import { useState, useEffect, useCallback } from 'react';
5
+ import { getPromptDefaults } from '../lib/api';
6
+ import type { PromptOverrides } from '../types/api';
7
+
8
+ /** 存储在 localStorage 中的键名 */
9
+ const PROMPTS_STORAGE_KEY = 'manimcat_prompt_overrides';
10
+
11
+ /** 默认提示词配置 */
12
+ const DEFAULT_PROMPTS: PromptOverrides = {
13
+ system: {
14
+ conceptDesigner: '',
15
+ codeGeneration: '',
16
+ codeRetry: '',
17
+ codeEdit: '',
18
+ },
19
+ user: {
20
+ conceptDesigner: '',
21
+ codeGeneration: '',
22
+ codeRetryInitial: '',
23
+ codeRetryFix: '',
24
+ codeEdit: '',
25
+ },
26
+ };
27
+
28
+ /** localStorage 加载提示词配置 */
29
+ /** Load prompt overrides from localStorage if available. */
30
+ export function loadStoredPrompts(): PromptOverrides | null {
31
+ try {
32
+ const saved = localStorage.getItem(PROMPTS_STORAGE_KEY);
33
+ if (saved) {
34
+ const parsed = JSON.parse(saved);
35
+ return {
36
+ system: { ...DEFAULT_PROMPTS.system, ...parsed.system },
37
+ user: { ...DEFAULT_PROMPTS.user, ...parsed.user },
38
+ };
39
+ }
40
+ } catch (error) {
41
+ console.error('Failed to load prompts:', error);
42
+ }
43
+ return null;
44
+ }
45
+
46
+ export function loadPrompts(): PromptOverrides {
47
+ return loadStoredPrompts() || DEFAULT_PROMPTS;
48
+ }
49
+
50
+ /** 保存提示词配置到 localStorage */
51
+ export function savePrompts(prompts: PromptOverrides): void {
52
+ try {
53
+ localStorage.setItem(PROMPTS_STORAGE_KEY, JSON.stringify(prompts));
54
+ } catch (error) {
55
+ console.error('Failed to save prompts:', error);
56
+ }
57
+ }
58
+
59
+ /** 提示词管理 Hook */
60
+ export function usePrompts() {
61
+ const [prompts, setPrompts] = useState<PromptOverrides>(DEFAULT_PROMPTS);
62
+ const [defaultPrompts, setDefaultPrompts] = useState<PromptOverrides>(DEFAULT_PROMPTS);
63
+ const [isLoading, setIsLoading] = useState<boolean>(true);
64
+ const [activeSection, setActiveSection] = useState<string>('system');
65
+ const [activePrompt, setActivePrompt] = useState<string>('conceptDesigner');
66
+
67
+ // localStorage 加载配置
68
+ useEffect(() => {
69
+ const savedPrompts = loadStoredPrompts();
70
+ if (savedPrompts) {
71
+ setPrompts(savedPrompts);
72
+ }
73
+
74
+ let isActive = true;
75
+ const loadDefaults = async () => {
76
+ setIsLoading(true);
77
+ try {
78
+ const defaults = await getPromptDefaults();
79
+ if (isActive) {
80
+ setDefaultPrompts(defaults);
81
+ }
82
+ } catch (error) {
83
+ console.error('Failed to load prompt defaults:', error);
84
+ } finally {
85
+ if (isActive) {
86
+ setIsLoading(false);
87
+ }
88
+ }
89
+ };
90
+
91
+ void loadDefaults();
92
+ return () => {
93
+ isActive = false;
94
+ };
95
+ }, []);
96
+
97
+ // 保存到 localStorage
98
+ useEffect(() => {
99
+ savePrompts(prompts);
100
+ }, [prompts]);
101
+
102
+ // 更新提示词
103
+ const updatePrompt = useCallback((section: string, type: string, value: string) => {
104
+ setPrompts(prev => {
105
+ const newPrompts = { ...prev };
106
+
107
+ if (section === 'system') {
108
+ if (!newPrompts.system) {
109
+ newPrompts.system = {};
110
+ }
111
+ newPrompts.system[type as keyof PromptOverrides['system']] = value;
112
+ } else if (section === 'user') {
113
+ if (!newPrompts.user) {
114
+ newPrompts.user = {};
115
+ }
116
+ newPrompts.user[type as keyof PromptOverrides['user']] = value;
117
+ }
118
+
119
+ return newPrompts;
120
+ });
121
+ }, []);
122
+
123
+ // 恢复默认值
124
+ const restoreDefault = useCallback(() => {
125
+ setPrompts(DEFAULT_PROMPTS);
126
+ }, []);
127
+
128
+ // 获取当前编辑的提示词
129
+ const getDefaultPrompt = useCallback((section: string, type: string) => {
130
+ if (section === 'system') {
131
+ return defaultPrompts.system?.[type as keyof PromptOverrides['system']] || '';
132
+ }
133
+ if (section === 'user') {
134
+ return defaultPrompts.user?.[type as keyof PromptOverrides['user']] || '';
135
+ }
136
+ return '';
137
+ }, [defaultPrompts]);
138
+
139
+ //
140
+ const getCurrentPrompt = useCallback(() => {
141
+ let current = '';
142
+ if (activeSection === 'system') {
143
+ current = prompts.system?.[activePrompt as keyof PromptOverrides['system']] || '';
144
+ } else if (activeSection === 'user') {
145
+ current = prompts.user?.[activePrompt as keyof PromptOverrides['user']] || '';
146
+ }
147
+
148
+ if (current && current.trim().length > 0) {
149
+ return current;
150
+ }
151
+
152
+ return getDefaultPrompt(activeSection, activePrompt);
153
+ }, [prompts, activeSection, activePrompt, getDefaultPrompt]);
154
+
155
+ //
156
+ const setCurrentPrompt = useCallback((value: string) => {
157
+ const defaultValue = getDefaultPrompt(activeSection, activePrompt);
158
+ const nextValue = value === defaultValue ? '' : value;
159
+ updatePrompt(activeSection, activePrompt, nextValue);
160
+ }, [activeSection, activePrompt, getDefaultPrompt, updatePrompt]);
161
+
162
+ return {
163
+ prompts,
164
+ defaultPrompts,
165
+ isLoading,
166
+ activeSection,
167
+ activePrompt,
168
+ setActiveSection,
169
+ setActivePrompt,
170
+ updatePrompt,
171
+ restoreDefault,
172
+ getCurrentPrompt,
173
+ setCurrentPrompt,
174
+ };
175
+ }
frontend/src/lib/api.ts CHANGED
@@ -1,6 +1,6 @@
1
  // API 请求函数
2
 
3
- import type { GenerateRequest, GenerateResponse, JobResult, ApiError, VideoConfig, PromptOverrides } from '../types/api';
4
 
5
  const API_BASE = '/api';
6
 
@@ -40,6 +40,25 @@ function getAuthHeaders(): HeadersInit {
40
  /**
41
  * 提交动画生成请求
42
  */
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
43
  export async function generateAnimation(request: GenerateRequest, signal?: AbortSignal): Promise<GenerateResponse> {
44
  // 如果请求中没有 videoConfig,则从设置中加载默认值
45
  const videoConfig = request.videoConfig || loadVideoConfig();
 
1
  // API 请求函数
2
 
3
+ import type { GenerateRequest, GenerateResponse, JobResult, ApiError, VideoConfig, PromptOverrides, ModifyRequest } from '../types/api';
4
 
5
  const API_BASE = '/api';
6
 
 
40
  /**
41
  * 提交动画生成请求
42
  */
43
+ export async function modifyAnimation(request: ModifyRequest, signal?: AbortSignal): Promise<GenerateResponse> {
44
+ const videoConfig = request.videoConfig || loadVideoConfig();
45
+
46
+ const payload = { ...request, videoConfig };
47
+ const response = await fetch(`${API_BASE}/modify`, {
48
+ method: 'POST',
49
+ headers: getAuthHeaders(),
50
+ body: JSON.stringify(payload),
51
+ signal,
52
+ });
53
+
54
+ if (!response.ok) {
55
+ const error: ApiError = await response.json();
56
+ throw new Error(error.error || 'AI 修改失败');
57
+ }
58
+
59
+ return response.json();
60
+ }
61
+
62
  export async function generateAnimation(request: GenerateRequest, signal?: AbortSignal): Promise<GenerateResponse> {
63
  // 如果请求中没有 videoConfig,则从设置中加载默认值
64
  const videoConfig = request.videoConfig || loadVideoConfig();
frontend/src/types/api.ts CHANGED
@@ -1,93 +1,105 @@
1
- // API 类型定义
2
-
3
- /** 视频质量选项 */
4
- export type Quality = 'low' | 'medium' | 'high';
5
-
6
- /** API 配置 */
7
- export interface ApiConfig {
8
- apiUrl: string;
9
- apiKey: string;
10
- model: string;
11
- manimcatApiKey: string;
12
- }
13
-
14
- export interface PromptOverrides {
15
- system?: {
16
- conceptDesigner?: string;
17
- codeGeneration?: string;
18
- codeRetry?: string;
19
- };
20
- user?: {
21
- conceptDesigner?: string;
22
- codeGeneration?: string;
23
- codeRetryInitial?: string;
24
- codeRetryFix?: string;
25
- };
26
- }
27
-
28
- /** 视频配置 */
29
- export interface VideoConfig {
30
- /** 默认质量 */
31
- quality: Quality;
32
- /** 帧率 */
33
- frameRate: number;
34
- /** 超时时间(秒),默认 600 秒(10 分钟) */
35
- timeout?: number;
36
- }
37
-
38
- /** 设置配置 */
39
- export interface SettingsConfig {
40
- api: ApiConfig;
41
- video: VideoConfig;
42
- }
43
-
44
- /** 任务状态 */
45
- export type JobStatus = 'processing' | 'completed' | 'failed';
46
-
47
- /** 处理阶段 */
48
- export type ProcessingStage = 'analyzing' | 'generating' | 'refining' | 'rendering' | 'still-rendering';
49
-
50
- /** 生成请求 */
51
- export interface GenerateRequest {
52
- concept: string;
53
- quality?: Quality;
54
- forceRefresh?: boolean;
55
- /** 预生成的代码(使用自定义 AI 时) */
56
- code?: string;
57
- /** 视频配置 */
58
- videoConfig?: VideoConfig;
59
- /** Prompt overrides */
60
- promptOverrides?: PromptOverrides;
61
- }
62
-
63
- /** 生成响应 */
64
- export interface GenerateResponse {
65
- success: boolean;
66
- jobId: string;
67
- message: string;
68
- status: JobStatus;
69
- }
70
-
71
- /** 任务结果 */
72
- export interface JobResult {
73
- jobId: string;
74
- status: JobStatus;
75
- stage?: ProcessingStage;
76
- message?: string;
77
- success?: boolean;
78
- video_url?: string;
79
- code?: string;
80
- used_ai?: boolean;
81
- render_quality?: string;
82
- generation_type?: string;
83
- render_peak_memory_mb?: number;
84
-
85
- error?: string;
86
- details?: string;
87
- cancel_reason?: string;
88
- }
89
-
90
- /** API 错误 */
91
- export interface ApiError {
92
- error: string;
93
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // API 类型定义
2
+
3
+ /** 视频质量选项 */
4
+ export type Quality = 'low' | 'medium' | 'high';
5
+
6
+ /** API 配置 */
7
+ export interface ApiConfig {
8
+ apiUrl: string;
9
+ apiKey: string;
10
+ model: string;
11
+ manimcatApiKey: string;
12
+ }
13
+
14
+ export interface PromptOverrides {
15
+ system?: {
16
+ conceptDesigner?: string;
17
+ codeGeneration?: string;
18
+ codeRetry?: string;
19
+ codeEdit?: string;
20
+ };
21
+ user?: {
22
+ conceptDesigner?: string;
23
+ codeGeneration?: string;
24
+ codeRetryInitial?: string;
25
+ codeRetryFix?: string;
26
+ codeEdit?: string;
27
+ };
28
+ }
29
+
30
+ /** 视频配置 */
31
+ export interface VideoConfig {
32
+ /** 默认质量 */
33
+ quality: Quality;
34
+ /** 帧率 */
35
+ frameRate: number;
36
+ /** 超时时间(秒),默认 600 秒(10 分钟) */
37
+ timeout?: number;
38
+ }
39
+
40
+ /** 设置配置 */
41
+ export interface SettingsConfig {
42
+ api: ApiConfig;
43
+ video: VideoConfig;
44
+ }
45
+
46
+ /** 任务状态 */
47
+ export type JobStatus = 'processing' | 'completed' | 'failed';
48
+
49
+ /** 处理阶段 */
50
+ export type ProcessingStage = 'analyzing' | 'generating' | 'refining' | 'rendering' | 'still-rendering';
51
+
52
+ /** 生成请求 */
53
+ export interface GenerateRequest {
54
+ concept: string;
55
+ quality?: Quality;
56
+ forceRefresh?: boolean;
57
+ /** 预生成的代码(使用自定义 AI 时) */
58
+ code?: string;
59
+ /** 视频配置 */
60
+ videoConfig?: VideoConfig;
61
+ /** Prompt overrides */
62
+ promptOverrides?: PromptOverrides;
63
+ }
64
+
65
+ /** AI 修改请求 */
66
+ export interface ModifyRequest {
67
+ concept: string;
68
+ quality?: Quality;
69
+ instructions: string;
70
+ code: string;
71
+ videoConfig?: VideoConfig;
72
+ promptOverrides?: PromptOverrides;
73
+ }
74
+
75
+ /** 生成响应 */
76
+ export interface GenerateResponse {
77
+ success: boolean;
78
+ jobId: string;
79
+ message: string;
80
+ status: JobStatus;
81
+ }
82
+
83
+ /** 任务结果 */
84
+ export interface JobResult {
85
+ jobId: string;
86
+ status: JobStatus;
87
+ stage?: ProcessingStage;
88
+ message?: string;
89
+ success?: boolean;
90
+ video_url?: string;
91
+ code?: string;
92
+ used_ai?: boolean;
93
+ render_quality?: string;
94
+ generation_type?: string;
95
+ render_peak_memory_mb?: number;
96
+
97
+ error?: string;
98
+ details?: string;
99
+ cancel_reason?: string;
100
+ }
101
+
102
+ /** API 错误 */
103
+ export interface ApiError {
104
+ error: string;
105
+ }
src/config/bull.ts CHANGED
@@ -1,24 +1,13 @@
1
- /**
2
- * Bull Queue Configuration
3
- * 任务队列配置和初始化
4
- */
5
-
6
- import { default as Bull, type Queue, type QueueOptions } from 'bull'
7
  import Redis from 'ioredis'
8
  import { redisClient, REDIS_KEYS } from './redis'
9
  import { createLogger } from '../utils/logger'
10
 
11
  const logger = createLogger('BullQueue')
12
 
13
- /**
14
- * Bull 队列配置选项
15
- */
16
  const queueOptions: QueueOptions = {
17
  prefix: REDIS_KEYS.QUEUE_PREFIX,
18
- // ... (保持不变)
19
-
20
- // 为 Bull 创建专用的 Redis 客户端
21
- createClient: (type) => {
22
  const REDIS_HOST = process.env.REDIS_HOST || 'localhost'
23
  const REDIS_PORT = parseInt(process.env.REDIS_PORT || '6379', 10)
24
  const REDIS_PASSWORD = process.env.REDIS_PASSWORD || undefined
@@ -29,40 +18,31 @@ const queueOptions: QueueOptions = {
29
  port: REDIS_PORT,
30
  password: REDIS_PASSWORD,
31
  db: REDIS_DB,
32
- maxRetriesPerRequest: null, // Bull 需要
33
- enableReadyCheck: false // Bull 需要
34
  })
35
 
36
  return client
37
  },
38
-
39
- // 默认任务配置
40
  defaultJobOptions: {
41
- attempts: 3, // 失败后重试 3 次
42
  backoff: {
43
- type: 'exponential', // 指数退避
44
- delay: 2000 // 初始延迟 2 秒
45
  },
46
- removeOnComplete: true, // 完成后自动清理
47
- removeOnFail: true, // 失败后自动清理
48
- timeout: 600000 // 任务超时 10 分钟
49
  },
50
-
51
  settings: {
52
- lockDuration: 600000, // 锁定时长 10 分钟
53
- stalledInterval: 30000, // 检查停滞任务间隔 30 秒
54
- maxStalledCount: 1 // 最多标记为停滞 1 次
55
  }
56
  }
57
 
58
- /**
59
- * 视频生成队列
60
- */
61
  export const videoQueue: Queue = new Bull('video-generation', queueOptions)
62
 
63
- /**
64
- * 启动时清理旧任务(防止后端重启后僵尸任务残留)
65
- */
66
  async function cleanupStaleJobs() {
67
  try {
68
  await videoQueue.isReady()
@@ -73,9 +53,6 @@ async function cleanupStaleJobs() {
73
  return
74
  }
75
 
76
- // 使用 clean() 方法移除旧任务
77
- // age=0 表示移除所有任务,不限制时间
78
- // 注意:Bull 的 clean 方法状态参数是 'wait', 'active', 'completed', 'failed', 'delayed'
79
  const [removedWait, removedActive, removedFailed, removedCompleted, removedDelayed] = await Promise.all([
80
  videoQueue.clean(0, 'wait'),
81
  videoQueue.clean(0, 'active'),
@@ -84,40 +61,34 @@ async function cleanupStaleJobs() {
84
  videoQueue.clean(0, 'delayed')
85
  ])
86
 
87
- const totalRemoved = (removedWait || 0) + (removedActive || 0) + (removedFailed || 0) + (removedCompleted || 0) + (removedDelayed || 0)
88
 
89
  if (totalRemoved > 0) {
90
- logger.info(`启动清理: 移除了 ${totalRemoved} 个残留队列任务`, {
91
- wait: removedWait,
92
- active: removedActive,
93
- failed: removedFailed,
94
- completed: removedCompleted,
95
- delayed: removedDelayed
96
  })
97
  }
98
 
99
- // 额外的彻底清理:删除 JobStore 存储的任务结果
100
- // 扫描所有以 'job:result:' 开头的 Key
101
  const resultKeys = await redisClient.keys(`${REDIS_KEYS.JOB_RESULT}*`)
102
  if (resultKeys.length > 0) {
103
  await redisClient.del(...resultKeys)
104
- logger.info(`启动清理: 删除了 ${resultKeys.length} 个历史任务结果缓存`)
105
  }
106
 
107
  if (totalRemoved === 0 && resultKeys.length === 0) {
108
- logger.info('启动检查: 无需清理残留数据')
109
  }
110
  } catch (error: any) {
111
- logger.warn('启动清理任务时遇到警告 (非致命)', { error: error?.message || String(error) })
112
  }
113
  }
114
 
115
- // 启动时执行清理
116
  cleanupStaleJobs()
117
 
118
- /**
119
- * 队列事件监听
120
- */
121
  videoQueue.on('error', (error) => {
122
  logger.error('Queue error', { message: error.message })
123
  })
@@ -130,7 +101,7 @@ videoQueue.on('active', (job) => {
130
  logger.info(`Job ${job.id} started processing`)
131
  })
132
 
133
- videoQueue.on('completed', (job, result) => {
134
  logger.info(`Job ${job.id} completed`)
135
  })
136
 
@@ -146,18 +117,12 @@ videoQueue.on('stalled', (job) => {
146
  logger.warn(`Job ${job.id} stalled`)
147
  })
148
 
149
- /**
150
- * 清理队列(开发调试用)
151
- */
152
  export async function cleanQueue(): Promise<void> {
153
  await videoQueue.clean(0, 'completed')
154
  await videoQueue.clean(0, 'failed')
155
  logger.info('Queue cleaned')
156
  }
157
 
158
- /**
159
- * 获取队列统计信息
160
- */
161
  export async function getQueueStats() {
162
  const [waiting, active, completed, failed, delayed] = await Promise.all([
163
  videoQueue.getWaitingCount(),
@@ -177,24 +142,18 @@ export async function getQueueStats() {
177
  }
178
  }
179
 
180
- /**
181
- * 优雅关闭队列
182
- */
183
- export async function closeQueue(): Promise<void> {
184
- await videoQueue.close()
185
- logger.info('Queue closed gracefully')
186
- }
187
-
188
- /**
189
- * 检查队列健康状态
190
- */
191
  export async function checkQueueHealth(): Promise<boolean> {
192
  try {
193
- const stats = await getQueueStats()
194
- // 如果有太多失败的任务,认为不健康
195
- return stats.failed < 100
196
  } catch (error) {
197
- logger.error('Queue health check failed', { error })
198
  return false
199
  }
200
  }
 
 
 
 
 
 
1
+ import { default as Bull, type Queue, type QueueOptions } from 'bull'
 
 
 
 
 
2
  import Redis from 'ioredis'
3
  import { redisClient, REDIS_KEYS } from './redis'
4
  import { createLogger } from '../utils/logger'
5
 
6
  const logger = createLogger('BullQueue')
7
 
 
 
 
8
  const queueOptions: QueueOptions = {
9
  prefix: REDIS_KEYS.QUEUE_PREFIX,
10
+ createClient: (_type) => {
 
 
 
11
  const REDIS_HOST = process.env.REDIS_HOST || 'localhost'
12
  const REDIS_PORT = parseInt(process.env.REDIS_PORT || '6379', 10)
13
  const REDIS_PASSWORD = process.env.REDIS_PASSWORD || undefined
 
18
  port: REDIS_PORT,
19
  password: REDIS_PASSWORD,
20
  db: REDIS_DB,
21
+ maxRetriesPerRequest: null,
22
+ enableReadyCheck: false
23
  })
24
 
25
  return client
26
  },
 
 
27
  defaultJobOptions: {
28
+ attempts: 3,
29
  backoff: {
30
+ type: 'exponential',
31
+ delay: 2000
32
  },
33
+ removeOnComplete: true,
34
+ removeOnFail: true,
35
+ timeout: 600000
36
  },
 
37
  settings: {
38
+ lockDuration: 600000,
39
+ stalledInterval: 30000,
40
+ maxStalledCount: 1
41
  }
42
  }
43
 
 
 
 
44
  export const videoQueue: Queue = new Bull('video-generation', queueOptions)
45
 
 
 
 
46
  async function cleanupStaleJobs() {
47
  try {
48
  await videoQueue.isReady()
 
53
  return
54
  }
55
 
 
 
 
56
  const [removedWait, removedActive, removedFailed, removedCompleted, removedDelayed] = await Promise.all([
57
  videoQueue.clean(0, 'wait'),
58
  videoQueue.clean(0, 'active'),
 
61
  videoQueue.clean(0, 'delayed')
62
  ])
63
 
64
+ const totalRemoved = removedWait.length + removedActive.length + removedFailed.length + removedCompleted.length + removedDelayed.length
65
 
66
  if (totalRemoved > 0) {
67
+ logger.info(`Startup cleanup: removed ${totalRemoved} queue jobs`, {
68
+ wait: removedWait.length,
69
+ active: removedActive.length,
70
+ failed: removedFailed.length,
71
+ completed: removedCompleted.length,
72
+ delayed: removedDelayed.length
73
  })
74
  }
75
 
 
 
76
  const resultKeys = await redisClient.keys(`${REDIS_KEYS.JOB_RESULT}*`)
77
  if (resultKeys.length > 0) {
78
  await redisClient.del(...resultKeys)
79
+ logger.info(`Startup cleanup: removed ${resultKeys.length} job result entries`)
80
  }
81
 
82
  if (totalRemoved === 0 && resultKeys.length === 0) {
83
+ logger.info('Startup cleanup: no stale data found')
84
  }
85
  } catch (error: any) {
86
+ logger.warn('Startup cleanup warning', { error: error?.message || String(error) })
87
  }
88
  }
89
 
 
90
  cleanupStaleJobs()
91
 
 
 
 
92
  videoQueue.on('error', (error) => {
93
  logger.error('Queue error', { message: error.message })
94
  })
 
101
  logger.info(`Job ${job.id} started processing`)
102
  })
103
 
104
+ videoQueue.on('completed', (job) => {
105
  logger.info(`Job ${job.id} completed`)
106
  })
107
 
 
117
  logger.warn(`Job ${job.id} stalled`)
118
  })
119
 
 
 
 
120
  export async function cleanQueue(): Promise<void> {
121
  await videoQueue.clean(0, 'completed')
122
  await videoQueue.clean(0, 'failed')
123
  logger.info('Queue cleaned')
124
  }
125
 
 
 
 
126
  export async function getQueueStats() {
127
  const [waiting, active, completed, failed, delayed] = await Promise.all([
128
  videoQueue.getWaitingCount(),
 
142
  }
143
  }
144
 
 
 
 
 
 
 
 
 
 
 
 
145
  export async function checkQueueHealth(): Promise<boolean> {
146
  try {
147
+ await videoQueue.isReady()
148
+ await videoQueue.getJobCounts()
149
+ return true
150
  } catch (error) {
151
+ logger.warn('Queue health check failed', { error: String(error) })
152
  return false
153
  }
154
  }
155
+
156
+ export async function closeQueue(): Promise<void> {
157
+ await videoQueue.close()
158
+ logger.info('Queue connection closed')
159
+ }
src/prompts/index.ts CHANGED
@@ -61,7 +61,14 @@ export const SYSTEM_PROMPTS = {
61
 
62
  - **动态更新**:对于涉及数值变化的过程,优先使用 \`ValueTracker\` 和 \`always_redraw\`。
63
  - **公式操作规范**:禁止使用硬编码索引,必须通过 \`substrings_to_isolate\` 配合 \`get_part_by_tex\` 来操作公式的特定部分。
64
- - **坐标系一致性**:所有图形必须通过 \`axes.c2p\` 映射到坐标轴上,严禁脱离坐标系的自由定位。`
 
 
 
 
 
 
 
65
  }
66
 
67
  export const SYSTEM_PROMPT_BASE = SYSTEM_PROMPTS.codeGeneration
@@ -336,6 +343,42 @@ ${brokenCode}
336
  请修复上述代码,只输出修复后的完整 Python 代码,不要任何解释。`
337
  }
338
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
339
  // =====================
340
  // Prompt Builder Utilities
341
  // =====================
 
61
 
62
  - **动态更新**:对于涉及数值变化的过程,优先使用 \`ValueTracker\` 和 \`always_redraw\`。
63
  - **公式操作规范**:禁止使用硬编码索引,必须通过 \`substrings_to_isolate\` 配合 \`get_part_by_tex\` 来操作公式的特定部分。
64
+ - **坐标系一致性**:所有图形必须通过 \`axes.c2p\` 映射到坐标轴上,严禁脱离坐标系的自由定位。`,
65
+
66
+ /** AI 修改时的 system prompt */
67
+ codeEdit: `你是一位 Manim 动画专家,擅长在既有代码基础上进行精准修改。
68
+ 严格遵循提示词规范,确保输出符合 Manim Community Edition (v0.19.2) 的可执行代码。
69
+
70
+ - **保持可运行**:修改后代码必须完整可运行,结构保持为 \`MainScene\`。
71
+ - **只输出代码**:禁止任何解释或 Markdown 包裹。`
72
  }
73
 
74
  export const SYSTEM_PROMPT_BASE = SYSTEM_PROMPTS.codeGeneration
 
343
  请修复上述代码,只输出修复后的完整 Python 代码,不要任何解释。`
344
  }
345
 
346
+ /**
347
+ * 生成 AI 修改时的用户 prompt
348
+ */
349
+ export function generateCodeEditPrompt(
350
+ concept: string,
351
+ instructions: string,
352
+ code: string
353
+ ): string {
354
+ return `## 目标
355
+
356
+ ### 修改信息
357
+
358
+ - **概念**:${concept}
359
+ - **修改意见**:${instructions}
360
+
361
+ ### 输出要求
362
+
363
+ - **仅输出代码**:禁止解释或 Markdown 包裹
364
+ - **锚点协议**:使用 ### START ### 开始,### END ### 结束,仅输出锚点之间的代码
365
+ - **结构规范**:场景类固定为 \`MainScene\`,统一使用 \`from manim import *\`
366
+
367
+ ## 知识库
368
+
369
+ ### API 索引表
370
+
371
+ ${API_INDEX}
372
+
373
+ ## 原始代码
374
+
375
+ \`\`\`python
376
+ ${code}
377
+ \`\`\`
378
+
379
+ 请根据修改意见输出完整的 Manim Python 代码。`;
380
+ }
381
+
382
  // =====================
383
  // Prompt Builder Utilities
384
  // =====================
src/queues/processors/steps/render-step.ts CHANGED
@@ -1,24 +1,17 @@
1
- /**
2
- * 视频渲染步骤
3
- * 执行 Manim 渲染,使用新的重试机制
4
- */
5
-
6
- import * as fs from 'fs'
7
- import * as path from 'path'
8
- import * as os from 'os'
9
- import { executeManimCommand, ManimExecuteOptions } from '../../../utils/manim-executor'
10
- import { findVideoFile } from '../../../utils/file-utils'
11
- import { storeJobStage } from '../../../services/job-store'
12
  import { createLogger } from '../../../utils/logger'
13
  import { cleanManimCode } from '../../../utils/manim-code-cleaner'
14
- import { createRetryContext, executeCodeRetry } from '../../../services/code-retry'
15
- import type { PromptOverrides, VideoJobData, VideoConfig } from '../../../types'
 
 
 
 
16
 
17
  const logger = createLogger('RenderStep')
18
 
19
- /**
20
- * 渲染结果类型
21
- */
22
  export interface RenderResult {
23
  jobId: string
24
  concept: string
@@ -27,37 +20,10 @@ export interface RenderResult {
27
  generationType: string
28
  quality: string
29
  videoUrl: string
30
- renderPeakMemoryMB: number
31
  }
32
-
33
  /**
34
- * 代码生成结果类型
35
- */
36
- export interface GenerationResult {
37
- manimCode: string
38
- usedAI: boolean
39
- generationType: string
40
- sceneDesign?: string // 场景设计方案,用于重试
41
- }
42
-
43
- // 质量对应的分辨率配置
44
- const QUALITY_RESOLUTION: Record<string, { width: number; height: number }> = {
45
- low: { width: 854, height: 480 },
46
- medium: { width: 1280, height: 720 },
47
- high: { width: 1920, height: 1080 }
48
- }
49
-
50
- /**
51
- * 任务结果
52
- */
53
- export interface TaskResult {
54
- success: boolean
55
- source: string
56
- timings: Record<string, number>
57
- }
58
-
59
- /**
60
- * 渲染视频
61
  */
62
  export async function renderVideo(
63
  jobId: string,
@@ -72,12 +38,12 @@ export async function renderVideo(
72
  ): Promise<RenderResult> {
73
  const { manimCode, usedAI, generationType, sceneDesign } = codeResult
74
 
75
- // 应用视频配置
76
  const frameRate = videoConfig?.frameRate || 30
77
 
78
  logger.info('Rendering video', { jobId, quality, usedAI, frameRate })
79
 
80
- // 创建临时目录
81
  const tempDir = path.join(os.tmpdir(), `manim-${jobId}`)
82
  const mediaDir = path.join(tempDir, 'media')
83
  const codeFile = path.join(tempDir, 'scene.py')
@@ -91,7 +57,7 @@ export async function renderVideo(
91
  let lastRenderedCode = manimCode
92
  let lastRenderPeakMemoryMB = 0
93
 
94
- // 渲染函数 - 供重试机制使用
95
  const renderCode = async (code: string): Promise<{
96
  success: boolean
97
  stderr: string
@@ -102,7 +68,7 @@ export async function renderVideo(
102
  lastRenderedCode = cleaned.code
103
 
104
  if (cleaned.changes.length > 0) {
105
- logger.info('Manim 代码已清洗', {
106
  jobId,
107
  changes: cleaned.changes,
108
  originalLength: code.length,
@@ -110,16 +76,16 @@ export async function renderVideo(
110
  })
111
  }
112
 
113
- logger.info('开始渲染代码', {
114
  jobId,
115
  codeLength: cleaned.code.length
116
  })
117
 
118
- // 写入代码文件
119
  fs.writeFileSync(codeFile, cleaned.code, 'utf-8')
120
- logger.info('代码已写入文件', { jobId, codeFile })
121
 
122
- // 执行 manim
123
  const options: ManimExecuteOptions = {
124
  jobId,
125
  quality,
@@ -131,7 +97,7 @@ export async function renderVideo(
131
  const result = await executeManimCommand(codeFile, options)
132
  lastRenderPeakMemoryMB = result.peakMemoryMB
133
 
134
- logger.info('Manim 渲染完成', {
135
  jobId,
136
  success: result.success,
137
  stdoutLength: result.stdout.length,
@@ -139,41 +105,41 @@ export async function renderVideo(
139
  peakMemoryMB: result.peakMemoryMB
140
  })
141
 
142
- // 记录完整输出
143
  if (result.stdout) {
144
- logger.info('Manim stdout 输出', { jobId, stdout: result.stdout })
145
  }
146
 
147
  if (result.stderr) {
148
  if (result.success) {
149
- logger.info('Manim stderr 输出(非错误)', { jobId, stderr: result.stderr })
150
  } else {
151
- logger.error('Manim stderr 输出(错误)', { jobId, stderr: result.stderr })
152
  }
153
  }
154
 
155
  return result
156
  }
157
 
158
- // 决定使用哪种渲染策略
159
  let finalCode = manimCode
160
  let renderResult: { success: boolean; stderr: string; stdout: string; peakMemoryMB: number }
161
 
162
- // 检查是否有场景设计方案(来自两阶段 AI 生成)
163
  const hasSceneDesign = usedAI && !!sceneDesign
164
 
165
  if (hasSceneDesign) {
166
- // 使用新的重试机制:维护完整对话历史
167
  logger.info('Using new code retry mechanism', { jobId, hasSceneDesign })
168
 
169
  await storeJobStage(jobId, 'generating')
170
 
171
  const retryStart = Date.now()
172
 
173
- // 创建重试上下文
174
  const retryContext = createRetryContext(concept, sceneDesign, promptOverrides)
175
 
176
- // 执行重试管理器
177
  const retryManagerResult = await executeCodeRetry(
178
  retryContext,
179
  renderCode,
@@ -191,18 +157,18 @@ export async function renderVideo(
191
  peakMemoryMB: lastRenderPeakMemoryMB
192
  }
193
 
194
- logger.info('代码重试成功', {
195
  jobId,
196
  attempts: retryManagerResult.attempts
197
  })
198
  } else {
199
- // 所有重试均失败
200
  throw new Error(
201
  `Code retry failed after ${retryManagerResult.attempts} attempts: ${retryManagerResult.lastError}`
202
  )
203
  }
204
  } else {
205
- // 非AI生成或无场景设计方案:单次渲染
206
  logger.info('Using single render attempt', {
207
  jobId,
208
  reason: usedAI ? 'no_scene_design' : 'not_ai_generated'
@@ -212,7 +178,7 @@ export async function renderVideo(
212
  renderResult = await renderCode(manimCode)
213
 
214
  if (!renderResult.success) {
215
- logger.error('Manim 渲染失败', {
216
  jobId,
217
  stderrLength: renderResult.stderr.length,
218
  stdoutLength: renderResult.stdout.length,
@@ -225,13 +191,13 @@ export async function renderVideo(
225
  finalCode = lastRenderedCode
226
  }
227
 
228
- // 查找生成的视频文件
229
  const videoPath = findVideoFile(mediaDir, quality, frameRate)
230
  if (!videoPath) {
231
  throw new Error('Video file not found after render')
232
  }
233
 
234
- // 复制到输出目录
235
  const outputFilename = `${jobId}.mp4`
236
  const outputPath = path.join(outputDir, outputFilename)
237
  fs.copyFileSync(videoPath, outputPath)
@@ -249,7 +215,7 @@ export async function renderVideo(
249
  renderPeakMemoryMB: renderResult.peakMemoryMB
250
  }
251
  } finally {
252
- // 清理临时目录
253
  try {
254
  fs.rmSync(tempDir, { recursive: true, force: true })
255
  logger.info('Cleaned up temp dir', { jobId })
@@ -260,7 +226,7 @@ export async function renderVideo(
260
  }
261
 
262
  /**
263
- * 处理预生成代码
264
  */
265
  export async function handlePreGeneratedCode(
266
  jobId: string,
@@ -269,14 +235,14 @@ export async function handlePreGeneratedCode(
269
  preGeneratedCode: string,
270
  timings: Record<string, number>,
271
  jobData: VideoJobData
272
- ): Promise<TaskResult> {
273
  logger.info('Using pre-generated code from frontend', {
274
  jobId,
275
  codeLength: preGeneratedCode.length,
276
  hasCustomApi: !!jobData.customApiConfig
277
  })
278
 
279
- // 直接进入渲染阶段
280
  const renderStart = Date.now()
281
  const renderResult = await renderVideo(jobId, concept, quality, {
282
  manimCode: preGeneratedCode,
@@ -285,10 +251,6 @@ export async function handlePreGeneratedCode(
285
  }, timings, jobData.customApiConfig, jobData.videoConfig, jobData.promptOverrides)
286
  timings.render = Date.now() - renderStart
287
 
288
- // 存储结果
289
- const storeStart = Date.now()
290
- timings.store = Date.now() - storeStart
291
-
292
  logger.info('Job completed (pre-generated code)', { jobId, timings })
293
- return { success: true, source: 'custom-api', timings }
294
  }
 
1
+ import fs from 'fs'
2
+ import os from 'os'
3
+ import path from 'path'
 
 
 
 
 
 
 
 
4
  import { createLogger } from '../../../utils/logger'
5
  import { cleanManimCode } from '../../../utils/manim-code-cleaner'
6
+ import { executeManimCommand, type ManimExecuteOptions } from '../../../utils/manim-executor'
7
+ import { findVideoFile } from '../../../utils/file-utils'
8
+ import { createRetryContext, executeCodeRetry } from '../../../services/code-retry/manager'
9
+ import { storeJobStage } from '../../../services/job-store'
10
+ import type { GenerationResult } from './analysis-step'
11
+ import type { PromptOverrides, VideoConfig, VideoJobData } from '../../../types'
12
 
13
  const logger = createLogger('RenderStep')
14
 
 
 
 
15
  export interface RenderResult {
16
  jobId: string
17
  concept: string
 
20
  generationType: string
21
  quality: string
22
  videoUrl: string
23
+ renderPeakMemoryMB?: number
24
  }
 
25
  /**
26
+ * 娓叉煋瑙嗛
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
27
  */
28
  export async function renderVideo(
29
  jobId: string,
 
38
  ): Promise<RenderResult> {
39
  const { manimCode, usedAI, generationType, sceneDesign } = codeResult
40
 
41
+ // 搴旂敤瑙嗛閰嶇疆
42
  const frameRate = videoConfig?.frameRate || 30
43
 
44
  logger.info('Rendering video', { jobId, quality, usedAI, frameRate })
45
 
46
+ // 鍒涘缓涓存椂鐩綍
47
  const tempDir = path.join(os.tmpdir(), `manim-${jobId}`)
48
  const mediaDir = path.join(tempDir, 'media')
49
  const codeFile = path.join(tempDir, 'scene.py')
 
57
  let lastRenderedCode = manimCode
58
  let lastRenderPeakMemoryMB = 0
59
 
60
+ // 娓叉煋鍑芥暟 - 渚涢噸璇曟満鍒朵娇鐢?
61
  const renderCode = async (code: string): Promise<{
62
  success: boolean
63
  stderr: string
 
68
  lastRenderedCode = cleaned.code
69
 
70
  if (cleaned.changes.length > 0) {
71
+ logger.info('Manim code cleaned', {
72
  jobId,
73
  changes: cleaned.changes,
74
  originalLength: code.length,
 
76
  })
77
  }
78
 
79
+ logger.info('Starting render', {
80
  jobId,
81
  codeLength: cleaned.code.length
82
  })
83
 
84
+ // 鍐欏叆浠g爜鏂囦欢
85
  fs.writeFileSync(codeFile, cleaned.code, 'utf-8')
86
+ logger.info('Code written to file', { jobId, codeFile })
87
 
88
+ // 鎵ц manim
89
  const options: ManimExecuteOptions = {
90
  jobId,
91
  quality,
 
97
  const result = await executeManimCommand(codeFile, options)
98
  lastRenderPeakMemoryMB = result.peakMemoryMB
99
 
100
+ logger.info('Manim 娓叉煋瀹屾垚', {
101
  jobId,
102
  success: result.success,
103
  stdoutLength: result.stdout.length,
 
105
  peakMemoryMB: result.peakMemoryMB
106
  })
107
 
108
+ // 璁板綍瀹屾暣杈撳嚭
109
  if (result.stdout) {
110
+ logger.info('Manim stdout 杈撳嚭', { jobId, stdout: result.stdout })
111
  }
112
 
113
  if (result.stderr) {
114
  if (result.success) {
115
+ logger.info('Manim stderr output (non-error)', { jobId, stderr: result.stderr })
116
  } else {
117
+ logger.error('Manim stderr 杈撳嚭锛堥敊璇級', { jobId, stderr: result.stderr })
118
  }
119
  }
120
 
121
  return result
122
  }
123
 
124
+ // 鍐冲畾浣跨敤鍝娓叉煋绛栫暐
125
  let finalCode = manimCode
126
  let renderResult: { success: boolean; stderr: string; stdout: string; peakMemoryMB: number }
127
 
128
+ // 妫€鏌ユ槸鍚︽湁鍦烘櫙璁捐鏂规锛堟潵鑷袱闃舵 AI 鐢熸垚锛?
129
  const hasSceneDesign = usedAI && !!sceneDesign
130
 
131
  if (hasSceneDesign) {
132
+ // 浣跨敤鏂扮殑閲嶈瘯鏈哄埗锛氱淮鎶ゅ畬鏁村璇濆巻鍙?
133
  logger.info('Using new code retry mechanism', { jobId, hasSceneDesign })
134
 
135
  await storeJobStage(jobId, 'generating')
136
 
137
  const retryStart = Date.now()
138
 
139
+ // 鍒涘缓閲嶈瘯涓婁笅鏂?
140
  const retryContext = createRetryContext(concept, sceneDesign, promptOverrides)
141
 
142
+ // 鎵ц閲嶈瘯绠$悊鍣?
143
  const retryManagerResult = await executeCodeRetry(
144
  retryContext,
145
  renderCode,
 
157
  peakMemoryMB: lastRenderPeakMemoryMB
158
  }
159
 
160
+ logger.info('浠g爜閲嶈瘯鎴愬姛', {
161
  jobId,
162
  attempts: retryManagerResult.attempts
163
  })
164
  } else {
165
+ // 鎵€鏈夐噸璇曞潎澶辫触
166
  throw new Error(
167
  `Code retry failed after ${retryManagerResult.attempts} attempts: ${retryManagerResult.lastError}`
168
  )
169
  }
170
  } else {
171
+ // 闈濧I鐢熸垚鎴栨棤鍦烘櫙璁捐鏂规锛氬崟娆℃覆鏌?
172
  logger.info('Using single render attempt', {
173
  jobId,
174
  reason: usedAI ? 'no_scene_design' : 'not_ai_generated'
 
178
  renderResult = await renderCode(manimCode)
179
 
180
  if (!renderResult.success) {
181
+ logger.error('Manim 娓叉煋澶辫触', {
182
  jobId,
183
  stderrLength: renderResult.stderr.length,
184
  stdoutLength: renderResult.stdout.length,
 
191
  finalCode = lastRenderedCode
192
  }
193
 
194
+ // 鏌ユ壘鐢熸垚鐨勮棰戞枃浠?
195
  const videoPath = findVideoFile(mediaDir, quality, frameRate)
196
  if (!videoPath) {
197
  throw new Error('Video file not found after render')
198
  }
199
 
200
+ // 澶嶅埗鍒拌緭鍑虹洰褰?
201
  const outputFilename = `${jobId}.mp4`
202
  const outputPath = path.join(outputDir, outputFilename)
203
  fs.copyFileSync(videoPath, outputPath)
 
215
  renderPeakMemoryMB: renderResult.peakMemoryMB
216
  }
217
  } finally {
218
+ // 娓呯悊涓存椂鐩綍
219
  try {
220
  fs.rmSync(tempDir, { recursive: true, force: true })
221
  logger.info('Cleaned up temp dir', { jobId })
 
226
  }
227
 
228
  /**
229
+ * 澶勭悊棰勭敓鎴愪唬鐮?
230
  */
231
  export async function handlePreGeneratedCode(
232
  jobId: string,
 
235
  preGeneratedCode: string,
236
  timings: Record<string, number>,
237
  jobData: VideoJobData
238
+ ): Promise<RenderResult> {
239
  logger.info('Using pre-generated code from frontend', {
240
  jobId,
241
  codeLength: preGeneratedCode.length,
242
  hasCustomApi: !!jobData.customApiConfig
243
  })
244
 
245
+ // 鐩存帴杩涘叆娓叉煋闃舵
246
  const renderStart = Date.now()
247
  const renderResult = await renderVideo(jobId, concept, quality, {
248
  manimCode: preGeneratedCode,
 
251
  }, timings, jobData.customApiConfig, jobData.videoConfig, jobData.promptOverrides)
252
  timings.render = Date.now() - renderStart
253
 
 
 
 
 
254
  logger.info('Job completed (pre-generated code)', { jobId, timings })
255
+ return renderResult
256
  }
src/queues/processors/steps/storage-step.ts CHANGED
@@ -16,9 +16,11 @@ const logger = createLogger('StorageStep')
16
  */
17
  export async function storeResult(
18
  renderResult: RenderResult,
19
- _timings: Record<string, number>
 
20
  ): Promise<void> {
21
  const { jobId, concept, manimCode, usedAI, generationType, quality, videoUrl, renderPeakMemoryMB } = renderResult
 
22
 
23
  // 存储到 Redis(用于 API 查询)
24
  await storeJobResult(jobId, {
 
16
  */
17
  export async function storeResult(
18
  renderResult: RenderResult,
19
+ _timings: Record<string, number>,
20
+ options: { skipCache?: boolean } = {}
21
  ): Promise<void> {
22
  const { jobId, concept, manimCode, usedAI, generationType, quality, videoUrl, renderPeakMemoryMB } = renderResult
23
+ const { skipCache = false } = options
24
 
25
  // 存储到 Redis(用于 API 查询)
26
  await storeJobResult(jobId, {
src/queues/processors/video.processor.ts CHANGED
@@ -7,6 +7,7 @@
7
 
8
  import { videoQueue } from '../../config/bull'
9
  import { storeJobResult } from '../../services/job-store'
 
10
  import { ensureJobNotCancelled } from '../../services/job-cancel'
11
  import { clearJobCancelled } from '../../services/job-cancel-store'
12
  import { JobCancelledError } from '../../utils/errors'
@@ -26,9 +27,9 @@ const logger = createLogger('VideoProcessor')
26
  */
27
  videoQueue.process(async (job) => {
28
  const data = job.data as VideoJobData
29
- const { jobId, concept, quality, forceRefresh = false, preGeneratedCode, promptOverrides } = data
30
 
31
- logger.info('Processing video job', { jobId, concept, quality, hasPreGeneratedCode: !!preGeneratedCode })
32
 
33
  // 阶段时长追踪
34
  const timings: Record<string, number> = {}
@@ -38,7 +39,55 @@ videoQueue.process(async (job) => {
38
  // 如果有预生成代码,跳过缓存和 AI 生成阶段,直接渲染
39
  if (preGeneratedCode) {
40
  await ensureJobNotCancelled(jobId, job)
41
- return await handlePreGeneratedCode(jobId, concept, quality, preGeneratedCode, timings, data)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
42
  }
43
 
44
  // Step 1: 检查缓存
 
7
 
8
  import { videoQueue } from '../../config/bull'
9
  import { storeJobResult } from '../../services/job-store'
10
+ import { generateEditedManimCode } from '../../services/code-edit'
11
  import { ensureJobNotCancelled } from '../../services/job-cancel'
12
  import { clearJobCancelled } from '../../services/job-cancel-store'
13
  import { JobCancelledError } from '../../utils/errors'
 
27
  */
28
  videoQueue.process(async (job) => {
29
  const data = job.data as VideoJobData
30
+ const { jobId, concept, quality, forceRefresh = false, preGeneratedCode, editCode, editInstructions, promptOverrides } = data
31
 
32
+ logger.info('Processing video job', { jobId, concept, quality, hasPreGeneratedCode: !!preGeneratedCode, hasEditRequest: !!editInstructions })
33
 
34
  // 阶段时长追踪
35
  const timings: Record<string, number> = {}
 
39
  // 如果有预生成代码,跳过缓存和 AI 生成阶段,直接渲染
40
  if (preGeneratedCode) {
41
  await ensureJobNotCancelled(jobId, job)
42
+ const renderResult = await handlePreGeneratedCode(jobId, concept, quality, preGeneratedCode, timings, data)
43
+
44
+ const storeStart = Date.now()
45
+ await storeResult(renderResult, timings, { skipCache: true })
46
+ timings.store = Date.now() - storeStart
47
+ timings.total = timings.render + timings.store
48
+
49
+ logger.info('Job completed (pre-generated code)', { jobId, timings })
50
+ return { success: true, source: 'pre-generated', timings }
51
+ }
52
+
53
+ // AI 修改流程
54
+ if (editCode && editInstructions) {
55
+ await ensureJobNotCancelled(jobId, job)
56
+ await storeJobStage(jobId, 'generating')
57
+ const editStart = Date.now()
58
+ const editedCode = await generateEditedManimCode(concept, editInstructions, editCode, data.customApiConfig, promptOverrides)
59
+ timings.edit = Date.now() - editStart
60
+
61
+ if (!editedCode) {
62
+ throw new Error('AI 修改未返回有效代码')
63
+ }
64
+
65
+ await ensureJobNotCancelled(jobId, job)
66
+ const renderStart = Date.now()
67
+ const renderResult = await renderVideo(
68
+ jobId,
69
+ concept,
70
+ quality,
71
+ {
72
+ manimCode: editedCode,
73
+ usedAI: true,
74
+ generationType: 'ai-edit'
75
+ },
76
+ timings,
77
+ data.customApiConfig,
78
+ data.videoConfig,
79
+ promptOverrides,
80
+ () => storeJobStage(jobId, 'rendering')
81
+ )
82
+ timings.render = Date.now() - renderStart
83
+
84
+ const storeStart = Date.now()
85
+ await storeResult(renderResult, timings, { skipCache: true })
86
+ timings.store = Date.now() - storeStart
87
+ timings.total = timings.edit + timings.render + timings.store
88
+
89
+ logger.info('Job completed', { jobId, source: 'ai-edit', timings })
90
+ return { success: true, source: 'ai-edit', timings }
91
  }
92
 
93
  // Step 1: 检查缓存
src/routes/generate.route.ts CHANGED
@@ -73,13 +73,15 @@ const bodySchema = z.object({
73
  system: z.object({
74
  conceptDesigner: z.string().max(20000).optional(),
75
  codeGeneration: z.string().max(20000).optional(),
76
- codeRetry: z.string().max(20000).optional()
 
77
  }).optional(),
78
  user: z.object({
79
  conceptDesigner: z.string().max(20000).optional(),
80
  codeGeneration: z.string().max(20000).optional(),
81
  codeRetryInitial: z.string().max(20000).optional(),
82
- codeRetryFix: z.string().max(20000).optional()
 
83
  }).optional()
84
  }).optional(),
85
  /** 视频配置 */
 
73
  system: z.object({
74
  conceptDesigner: z.string().max(20000).optional(),
75
  codeGeneration: z.string().max(20000).optional(),
76
+ codeRetry: z.string().max(20000).optional(),
77
+ codeEdit: z.string().max(20000).optional()
78
  }).optional(),
79
  user: z.object({
80
  conceptDesigner: z.string().max(20000).optional(),
81
  codeGeneration: z.string().max(20000).optional(),
82
  codeRetryInitial: z.string().max(20000).optional(),
83
+ codeRetryFix: z.string().max(20000).optional(),
84
+ codeEdit: z.string().max(20000).optional()
85
  }).optional()
86
  }).optional(),
87
  /** 视频配置 */
src/routes/index.ts CHANGED
@@ -7,6 +7,7 @@
7
 
8
  import express from 'express'
9
  import generateRouter from './generate.route'
 
10
  import jobStatusRouter from './job-status.route'
11
  import jobCancelRouter from './job-cancel.route'
12
  import promptsRouter from './prompts.route'
@@ -20,6 +21,7 @@ router.use(healthRouter)
20
 
21
  // 挂载 API 路由(使用 /api 前缀)
22
  router.use('/api', generateRouter)
 
23
  router.use('/api', jobStatusRouter)
24
  router.use('/api', jobCancelRouter)
25
  router.use('/api', promptsRouter)
 
7
 
8
  import express from 'express'
9
  import generateRouter from './generate.route'
10
+ import modifyRouter from './modify.route'
11
  import jobStatusRouter from './job-status.route'
12
  import jobCancelRouter from './job-cancel.route'
13
  import promptsRouter from './prompts.route'
 
21
 
22
  // 挂载 API 路由(使用 /api 前缀)
23
  router.use('/api', generateRouter)
24
+ router.use('/api', modifyRouter)
25
  router.use('/api', jobStatusRouter)
26
  router.use('/api', jobCancelRouter)
27
  router.use('/api', promptsRouter)
src/routes/modify.route.ts ADDED
@@ -0,0 +1,142 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /**
2
+ * AI 修改路由
3
+ * POST /api/modify - 基于现有代码进行 AI 修改并渲染
4
+ */
5
+
6
+ import express from 'express'
7
+ import { z } from 'zod'
8
+ import { v4 as uuidv4 } from 'uuid'
9
+ import { videoQueue } from '../config/bull'
10
+ import { storeJobStage } from '../services/job-store'
11
+ import { createLogger } from '../utils/logger'
12
+ import { AuthenticationError, ValidationError } from '../utils/errors'
13
+ import { asyncHandler } from '../middlewares/error-handler'
14
+ import { authMiddleware } from '../middlewares/auth.middleware'
15
+ import type { GenerateResponse } from '../types'
16
+
17
+ const router = express.Router()
18
+ const logger = createLogger('ModifyRoute')
19
+
20
+ function extractToken(authHeader: string | string[] | undefined): string {
21
+ if (!authHeader) return ''
22
+
23
+ if (typeof authHeader === 'string') {
24
+ return authHeader.replace(/^Bearer\s+/i, '')
25
+ }
26
+ if (Array.isArray(authHeader)) {
27
+ return authHeader[0]?.replace(/^Bearer\s+/i, '') || ''
28
+ }
29
+ return ''
30
+ }
31
+
32
+ function hasPromptOverrides(promptOverrides: any): boolean {
33
+ if (!promptOverrides) return false
34
+ const system = promptOverrides.system || {}
35
+ const user = promptOverrides.user || {}
36
+ return Object.values(system).some((value) => typeof value === 'string' && value.trim().length > 0) ||
37
+ Object.values(user).some((value) => typeof value === 'string' && value.trim().length > 0)
38
+ }
39
+
40
+ function requirePromptOverrideAuth(req: express.Request): void {
41
+ const manimcatApiKey = process.env.MANIMCAT_API_KEY
42
+ if (!manimcatApiKey) {
43
+ throw new AuthenticationError('Prompt overrides require MANIMCAT_API_KEY to be set.')
44
+ }
45
+
46
+ const token = extractToken(req.headers?.authorization)
47
+ if (!token || token !== manimcatApiKey) {
48
+ throw new AuthenticationError('Prompt overrides require a valid MANIMCAT_API_KEY token.')
49
+ }
50
+ }
51
+
52
+ const bodySchema = z.object({
53
+ concept: z.string().min(1, '概念不能为空'),
54
+ quality: z.enum(['low', 'medium', 'high']).optional().default('low'),
55
+ instructions: z.string().min(1, '修改意见不能为空'),
56
+ code: z.string().min(1, '原始代码不能为空'),
57
+ customApiConfig: z.object({
58
+ apiUrl: z.string(),
59
+ apiKey: z.string(),
60
+ model: z.string()
61
+ }).optional(),
62
+ promptOverrides: z.object({
63
+ system: z.object({
64
+ conceptDesigner: z.string().max(20000).optional(),
65
+ codeGeneration: z.string().max(20000).optional(),
66
+ codeRetry: z.string().max(20000).optional(),
67
+ codeEdit: z.string().max(20000).optional()
68
+ }).optional(),
69
+ user: z.object({
70
+ conceptDesigner: z.string().max(20000).optional(),
71
+ codeGeneration: z.string().max(20000).optional(),
72
+ codeRetryInitial: z.string().max(20000).optional(),
73
+ codeRetryFix: z.string().max(20000).optional(),
74
+ codeEdit: z.string().max(20000).optional()
75
+ }).optional()
76
+ }).optional(),
77
+ videoConfig: z.object({
78
+ quality: z.enum(['low', 'medium', 'high']).optional(),
79
+ frameRate: z.number().int().min(1).max(120).optional(),
80
+ timeout: z.number().optional()
81
+ }).optional()
82
+ })
83
+
84
+ async function handleModifyRequest(req: express.Request, res: express.Response) {
85
+ const parsed = bodySchema.parse(req.body)
86
+ const { concept, quality, instructions, code, customApiConfig, promptOverrides, videoConfig } = parsed
87
+
88
+ if (hasPromptOverrides(promptOverrides)) {
89
+ requirePromptOverrideAuth(req)
90
+ }
91
+
92
+ const sanitizedConcept = concept.trim().replace(/\s+/g, ' ')
93
+ if (sanitizedConcept.length === 0) {
94
+ throw new ValidationError('提供的概念为空', { concept })
95
+ }
96
+
97
+ const sanitizedInstructions = instructions.trim()
98
+ if (!sanitizedInstructions) {
99
+ throw new ValidationError('修改意见不能为空', { instructions })
100
+ }
101
+
102
+ const jobId = uuidv4()
103
+
104
+ logger.info('收到 AI 修改请求', {
105
+ jobId,
106
+ concept: sanitizedConcept,
107
+ quality,
108
+ hasCode: !!code,
109
+ videoConfig
110
+ })
111
+
112
+ await storeJobStage(jobId, 'generating')
113
+
114
+ await videoQueue.add(
115
+ {
116
+ jobId,
117
+ concept: sanitizedConcept,
118
+ quality,
119
+ editCode: code,
120
+ editInstructions: sanitizedInstructions,
121
+ customApiConfig,
122
+ promptOverrides,
123
+ videoConfig,
124
+ timestamp: new Date().toISOString()
125
+ },
126
+ { jobId }
127
+ )
128
+
129
+ const response: GenerateResponse = {
130
+ success: true,
131
+ jobId,
132
+ message: 'AI 修改已开始',
133
+ status: 'processing'
134
+ }
135
+
136
+ res.status(202).json(response)
137
+ }
138
+
139
+ router.post('/modify', authMiddleware, asyncHandler(handleModifyRequest))
140
+
141
+ export default router
142
+
src/routes/prompts.route.ts CHANGED
@@ -1,6 +1,11 @@
1
- import express from 'express'
2
 
3
- import { SYSTEM_PROMPTS, generateConceptDesignerPrompt, generateCodeGenerationPrompt } from '../prompts'
 
 
 
 
 
4
  import { CODE_RETRY_SYSTEM_PROMPT, buildInitialCodePrompt } from '../services/code-retry/prompts'
5
  import { buildRetryFixPrompt } from '../services/code-retry/manager'
6
  import type { PromptOverrides } from '../types'
@@ -12,7 +17,9 @@ const PLACEHOLDERS = {
12
  seed: '{{seed}}',
13
  sceneDesign: '{{sceneDesign}}',
14
  errorMessage: '{{errorMessage}}',
15
- attempt: '{{attempt}}'
 
 
16
  }
17
 
18
  function buildDefaultPromptTemplates(): PromptOverrides {
@@ -20,13 +27,15 @@ function buildDefaultPromptTemplates(): PromptOverrides {
20
  system: {
21
  conceptDesigner: SYSTEM_PROMPTS.conceptDesigner,
22
  codeGeneration: SYSTEM_PROMPTS.codeGeneration,
23
- codeRetry: CODE_RETRY_SYSTEM_PROMPT
 
24
  },
25
  user: {
26
  conceptDesigner: generateConceptDesignerPrompt(PLACEHOLDERS.concept, PLACEHOLDERS.seed),
27
  codeGeneration: generateCodeGenerationPrompt(PLACEHOLDERS.concept, PLACEHOLDERS.seed, PLACEHOLDERS.sceneDesign),
28
  codeRetryInitial: buildInitialCodePrompt(PLACEHOLDERS.concept, PLACEHOLDERS.seed, PLACEHOLDERS.sceneDesign),
29
- codeRetryFix: buildRetryFixPrompt(PLACEHOLDERS.concept, PLACEHOLDERS.errorMessage, PLACEHOLDERS.attempt)
 
30
  }
31
  }
32
  }
 
1
+ import express from 'express'
2
 
3
+ import {
4
+ SYSTEM_PROMPTS,
5
+ generateConceptDesignerPrompt,
6
+ generateCodeGenerationPrompt,
7
+ generateCodeEditPrompt
8
+ } from '../prompts'
9
  import { CODE_RETRY_SYSTEM_PROMPT, buildInitialCodePrompt } from '../services/code-retry/prompts'
10
  import { buildRetryFixPrompt } from '../services/code-retry/manager'
11
  import type { PromptOverrides } from '../types'
 
17
  seed: '{{seed}}',
18
  sceneDesign: '{{sceneDesign}}',
19
  errorMessage: '{{errorMessage}}',
20
+ attempt: '{{attempt}}',
21
+ instructions: '{{instructions}}',
22
+ code: '{{code}}'
23
  }
24
 
25
  function buildDefaultPromptTemplates(): PromptOverrides {
 
27
  system: {
28
  conceptDesigner: SYSTEM_PROMPTS.conceptDesigner,
29
  codeGeneration: SYSTEM_PROMPTS.codeGeneration,
30
+ codeRetry: CODE_RETRY_SYSTEM_PROMPT,
31
+ codeEdit: SYSTEM_PROMPTS.codeEdit
32
  },
33
  user: {
34
  conceptDesigner: generateConceptDesignerPrompt(PLACEHOLDERS.concept, PLACEHOLDERS.seed),
35
  codeGeneration: generateCodeGenerationPrompt(PLACEHOLDERS.concept, PLACEHOLDERS.seed, PLACEHOLDERS.sceneDesign),
36
  codeRetryInitial: buildInitialCodePrompt(PLACEHOLDERS.concept, PLACEHOLDERS.seed, PLACEHOLDERS.sceneDesign),
37
+ codeRetryFix: buildRetryFixPrompt(PLACEHOLDERS.concept, PLACEHOLDERS.errorMessage, PLACEHOLDERS.attempt),
38
+ codeEdit: generateCodeEditPrompt(PLACEHOLDERS.concept, PLACEHOLDERS.instructions, PLACEHOLDERS.code)
39
  }
40
  }
41
  }
src/services/code-edit.ts ADDED
@@ -0,0 +1,134 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import OpenAI from 'openai'
2
+ import { createLogger } from '../utils/logger'
3
+ import { SYSTEM_PROMPTS, generateCodeEditPrompt } from '../prompts'
4
+ import type { CustomApiConfig, PromptOverrides } from '../types'
5
+
6
+ const logger = createLogger('CodeEditService')
7
+
8
+ const OPENAI_MODEL = process.env.OPENAI_MODEL || 'glm-4-flash'
9
+ const CODER_TEMPERATURE = parseFloat(process.env.AI_TEMPERATURE || '0.7')
10
+ const MAX_TOKENS = parseInt(process.env.AI_MAX_TOKENS || '1200', 10)
11
+ const OPENAI_TIMEOUT = parseInt(process.env.OPENAI_TIMEOUT || '600000', 10)
12
+
13
+ const CUSTOM_API_URL = process.env.CUSTOM_API_URL?.trim()
14
+
15
+ let openaiClient: OpenAI | null = null
16
+
17
+ try {
18
+ const baseConfig = {
19
+ timeout: OPENAI_TIMEOUT,
20
+ defaultHeaders: {
21
+ 'User-Agent': 'ManimCat/1.0'
22
+ }
23
+ }
24
+
25
+ if (CUSTOM_API_URL) {
26
+ openaiClient = new OpenAI({
27
+ ...baseConfig,
28
+ baseURL: CUSTOM_API_URL,
29
+ apiKey: process.env.OPENAI_API_KEY
30
+ })
31
+ } else {
32
+ openaiClient = new OpenAI(baseConfig)
33
+ }
34
+ } catch (error) {
35
+ logger.warn('OpenAI 客户端初始化失败', { error })
36
+ }
37
+
38
+ function createCustomClient(config: CustomApiConfig): OpenAI {
39
+ return new OpenAI({
40
+ baseURL: config.apiUrl.trim().replace(/\/+$/, ''),
41
+ apiKey: config.apiKey,
42
+ timeout: OPENAI_TIMEOUT,
43
+ defaultHeaders: {
44
+ 'User-Agent': 'ManimCat/1.0'
45
+ }
46
+ })
47
+ }
48
+
49
+ function applyPromptTemplate(template: string, values: Record<string, string>): string {
50
+ let output = template
51
+ for (const [key, value] of Object.entries(values)) {
52
+ output = output.replace(new RegExp(`{{\\s*${key}\\s*}}`, 'g'), value)
53
+ }
54
+ return output
55
+ }
56
+
57
+ function extractCodeFromResponse(text: string): string {
58
+ if (!text) return ''
59
+ const sanitized = text.replace(/<think>[\s\S]*?<\/think>/gi, '')
60
+ const anchorMatch = sanitized.match(/### START ###([\s\S]*?)### END ###/)
61
+ if (anchorMatch) {
62
+ return anchorMatch[1].trim()
63
+ }
64
+ const codeMatch = sanitized.match(/```(?:python)?\n([\s\S]*?)```/i)
65
+ if (codeMatch) {
66
+ return codeMatch[1].trim()
67
+ }
68
+ return sanitized.trim()
69
+ }
70
+
71
+ export async function generateEditedManimCode(
72
+ concept: string,
73
+ instructions: string,
74
+ code: string,
75
+ customApiConfig?: CustomApiConfig,
76
+ promptOverrides?: PromptOverrides
77
+ ): Promise<string> {
78
+ const client = customApiConfig ? createCustomClient(customApiConfig) : openaiClient
79
+
80
+ if (!client) {
81
+ logger.warn('OpenAI 客户端不可用')
82
+ return ''
83
+ }
84
+
85
+ try {
86
+ const systemPrompt = promptOverrides?.system?.codeEdit || SYSTEM_PROMPTS.codeEdit
87
+ const userPromptOverride = promptOverrides?.user?.codeEdit
88
+ const userPrompt = userPromptOverride
89
+ ? applyPromptTemplate(userPromptOverride, { concept, instructions, code })
90
+ : generateCodeEditPrompt(concept, instructions, code)
91
+
92
+ logger.info('开始 AI 修改代码', { concept })
93
+
94
+ const response = await client.chat.completions.create({
95
+ model: OPENAI_MODEL,
96
+ messages: [
97
+ { role: 'system', content: systemPrompt },
98
+ { role: 'user', content: userPrompt }
99
+ ],
100
+ temperature: CODER_TEMPERATURE,
101
+ max_tokens: MAX_TOKENS
102
+ })
103
+
104
+ const content = response.choices[0]?.message?.content || ''
105
+ if (!content) {
106
+ logger.warn('AI 修改返回空内容')
107
+ return ''
108
+ }
109
+
110
+ const extracted = extractCodeFromResponse(content)
111
+ logger.info('AI 修改完成', { concept, length: extracted.length })
112
+ return extracted
113
+ } catch (error) {
114
+ if (error instanceof OpenAI.APIError) {
115
+ logger.error('AI 修改 API 错误', {
116
+ concept,
117
+ status: error.status,
118
+ code: error.code,
119
+ type: error.type,
120
+ message: error.message
121
+ })
122
+ } else if (error instanceof Error) {
123
+ logger.error('AI 修改失败', { concept, errorName: error.name, errorMessage: error.message })
124
+ } else {
125
+ logger.error('AI 修改失败,未知错误', { concept, error: String(error) })
126
+ }
127
+ return ''
128
+ }
129
+ }
130
+
131
+ export function isCodeEditAvailable(): boolean {
132
+ return openaiClient !== null
133
+ }
134
+
src/services/code-retry/types.ts CHANGED
@@ -1,11 +1,11 @@
1
- /**
2
- * Code Retry Service - 类型定义
3
  */
4
 
5
  import type { CustomApiConfig, PromptOverrides } from '../../types'
6
 
7
  /**
8
- * 对话消息类型
9
  */
10
  export interface ChatMessage {
11
  role: 'system' | 'user' | 'assistant'
@@ -13,18 +13,19 @@ export interface ChatMessage {
13
  }
14
 
15
  /**
16
- * 代码重试上下文
17
- * 维护完整的对话历史
18
  */
19
  export interface CodeRetryContext {
20
  concept: string
21
  sceneDesign: string
22
- originalPrompt: string // 原始写代码的提示词
23
- messages: ChatMessage[] // 完整对话历史
 
24
  }
25
 
26
  /**
27
- * 代码重试选项
28
  */
29
  export interface CodeRetryOptions {
30
  context: CodeRetryContext
@@ -32,7 +33,7 @@ export interface CodeRetryOptions {
32
  }
33
 
34
  /**
35
- * 代码重试结果
36
  */
37
  export interface CodeRetryResult {
38
  success: boolean
@@ -42,7 +43,7 @@ export interface CodeRetryResult {
42
  }
43
 
44
  /**
45
- * 渲染结果
46
  */
47
  export interface RenderResult {
48
  success: boolean
@@ -52,7 +53,7 @@ export interface RenderResult {
52
  }
53
 
54
  /**
55
- * 重试管理器结果
56
  */
57
  export interface RetryManagerResult {
58
  code: string
 
1
+ /**
2
+ * Code Retry Service - 绫诲瀷瀹氫箟
3
  */
4
 
5
  import type { CustomApiConfig, PromptOverrides } from '../../types'
6
 
7
  /**
8
+ * 瀵硅瘽娑堟伅绫诲瀷
9
  */
10
  export interface ChatMessage {
11
  role: 'system' | 'user' | 'assistant'
 
13
  }
14
 
15
  /**
16
+ * 浠g爜閲嶈瘯涓婁笅鏂?
17
+ * 缁存姢瀹屾暣鐨勫璇濆巻鍙?
18
  */
19
  export interface CodeRetryContext {
20
  concept: string
21
  sceneDesign: string
22
+ originalPrompt: string
23
+ messages: ChatMessage[]
24
+ promptOverrides?: PromptOverrides
25
  }
26
 
27
  /**
28
+ * 浠g爜閲嶈瘯閫夐」
29
  */
30
  export interface CodeRetryOptions {
31
  context: CodeRetryContext
 
33
  }
34
 
35
  /**
36
+ * 浠g爜閲嶈瘯缁撴灉
37
  */
38
  export interface CodeRetryResult {
39
  success: boolean
 
43
  }
44
 
45
  /**
46
+ * 娓叉煋缁撴灉
47
  */
48
  export interface RenderResult {
49
  success: boolean
 
53
  }
54
 
55
  /**
56
+ * 閲嶈瘯绠$悊鍣ㄧ粨鏋?
57
  */
58
  export interface RetryManagerResult {
59
  code: string
src/types/index.ts CHANGED
@@ -1,40 +1,40 @@
1
- /**
2
  * Type Definitions
3
- * 全局类型定义
4
  */
5
 
6
  /**
7
- * 视频质量选项
8
  */
9
  export type VideoQuality = 'low' | 'medium' | 'high'
10
 
11
  /**
12
- * 视频配置
13
  */
14
  export interface VideoConfig {
15
- /** 默认质量 */
16
  quality: VideoQuality
17
- /** 帧率 */
18
  frameRate: number
19
  }
20
 
21
  /**
22
- * 任务状态
23
  */
24
  export type JobStatus = 'queued' | 'processing' | 'completed' | 'failed'
25
 
26
  /**
27
- * 处理阶段
28
  */
29
  export type ProcessingStage = 'analyzing' | 'generating' | 'refining' | 'rendering' | 'still-rendering'
30
 
31
  /**
32
- * 生成类型
33
  */
34
  export type GenerationType = 'template' | 'ai' | 'cached'
35
 
36
  /**
37
- * 自定义 API 配置
38
  */
39
  export interface CustomApiConfig {
40
  apiUrl: string
@@ -50,17 +50,19 @@ export interface PromptOverrides {
50
  conceptDesigner?: string
51
  codeGeneration?: string
52
  codeRetry?: string
 
53
  }
54
  user?: {
55
  conceptDesigner?: string
56
  codeGeneration?: string
57
  codeRetryInitial?: string
58
  codeRetryFix?: string
 
59
  }
60
  }
61
 
62
  /**
63
- * 视频生成任务数据
64
  */
65
  export interface VideoJobData {
66
  jobId: string
@@ -68,16 +70,22 @@ export interface VideoJobData {
68
  quality: VideoQuality
69
  forceRefresh?: boolean
70
  timestamp: string
71
- /** 预生成的代码(使用自定义 AI 时) */
72
  preGeneratedCode?: string
73
- /** 自定义 API 配置(用于代码修复) */
 
 
 
 
74
  customApiConfig?: CustomApiConfig
75
- /** 视频配置 */
76
  videoConfig?: VideoConfig
 
 
77
  }
78
 
79
  /**
80
- * 任务结果 - 完成状态
81
  */
82
  export interface CompletedJobResult {
83
  status: 'completed'
@@ -93,7 +101,7 @@ export interface CompletedJobResult {
93
  }
94
 
95
  /**
96
- * 任务结果 - 失败状态
97
  */
98
  export interface FailedJobResult {
99
  status: 'failed'
@@ -106,12 +114,12 @@ export interface FailedJobResult {
106
  }
107
 
108
  /**
109
- * 任务结果联合类型
110
  */
111
  export type JobResult = CompletedJobResult | FailedJobResult
112
 
113
  /**
114
- * 概念缓存数据
115
  */
116
  export interface ConceptCacheData {
117
  jobId: string
@@ -126,7 +134,7 @@ export interface ConceptCacheData {
126
  }
127
 
128
  /**
129
- * API 请求 - 生成视频
130
  */
131
  export interface GenerateRequest {
132
  concept: string
@@ -136,7 +144,20 @@ export interface GenerateRequest {
136
  }
137
 
138
  /**
139
- * API 响应 - 生成视频
 
 
 
 
 
 
 
 
 
 
 
 
 
140
  */
141
  export interface GenerateResponse {
142
  success: boolean
@@ -146,7 +167,7 @@ export interface GenerateResponse {
146
  }
147
 
148
  /**
149
- * API 响应 - 任务状态(处理中)
150
  */
151
  export interface JobStatusProcessingResponse {
152
  status: 'processing' | 'queued'
@@ -156,8 +177,8 @@ export interface JobStatusProcessingResponse {
156
  }
157
 
158
  /**
159
- * API 响应 - 任务状态(完成)
160
- * 与前端 api.ts JobResult 类型兼容
161
  */
162
  export interface JobStatusCompletedResponse {
163
  status: 'completed'
@@ -172,8 +193,8 @@ export interface JobStatusCompletedResponse {
172
  }
173
 
174
  /**
175
- * API 响应 - 任务状态(失败)
176
- * 与前端 api.ts JobResult 类型兼容
177
  */
178
  export interface JobStatusFailedResponse {
179
  status: 'failed'
@@ -185,7 +206,7 @@ export interface JobStatusFailedResponse {
185
  }
186
 
187
  /**
188
- * API 响应 - 任务状态联合类型
189
  */
190
  export type JobStatusResponse =
191
  | JobStatusProcessingResponse
@@ -193,7 +214,7 @@ export type JobStatusResponse =
193
  | JobStatusFailedResponse
194
 
195
  /**
196
- * API 响应 - 健康检查
197
  */
198
  export interface HealthCheckResponse {
199
  status: 'ok' | 'degraded' | 'down'
@@ -214,7 +235,7 @@ export interface HealthCheckResponse {
214
  }
215
 
216
  /**
217
- * API 错误响应
218
  */
219
  export interface ErrorResponse {
220
  error: string
@@ -223,7 +244,7 @@ export interface ErrorResponse {
223
  }
224
 
225
  /**
226
- * Bull 任务进度数据
227
  */
228
  export interface JobProgress {
229
  step: string
@@ -232,7 +253,7 @@ export interface JobProgress {
232
  }
233
 
234
  /**
235
- * Manim 渲染选项
236
  */
237
  export interface ManimRenderOptions {
238
  quality: VideoQuality
@@ -242,9 +263,10 @@ export interface ManimRenderOptions {
242
  }
243
 
244
  /**
245
- * 缓存查询结果
246
  */
247
  export interface CacheCheckResult {
248
  hit: boolean
249
  data?: ConceptCacheData
250
  }
 
 
1
+ /**
2
  * Type Definitions
3
+ * 鍏ㄥ眬绫诲瀷瀹氫箟
4
  */
5
 
6
  /**
7
+ * 瑙嗛璐ㄩ噺閫夐」
8
  */
9
  export type VideoQuality = 'low' | 'medium' | 'high'
10
 
11
  /**
12
+ * 瑙嗛閰嶇疆
13
  */
14
  export interface VideoConfig {
15
+ /** 榛樿璐ㄩ噺 */
16
  quality: VideoQuality
17
+ /** 甯х巼 */
18
  frameRate: number
19
  }
20
 
21
  /**
22
+ * 浠诲姟鐘舵€?
23
  */
24
  export type JobStatus = 'queued' | 'processing' | 'completed' | 'failed'
25
 
26
  /**
27
+ * 澶勭悊闃舵
28
  */
29
  export type ProcessingStage = 'analyzing' | 'generating' | 'refining' | 'rendering' | 'still-rendering'
30
 
31
  /**
32
+ * 鐢熸垚绫诲瀷
33
  */
34
  export type GenerationType = 'template' | 'ai' | 'cached'
35
 
36
  /**
37
+ * 鑷畾涔?API 閰嶇疆
38
  */
39
  export interface CustomApiConfig {
40
  apiUrl: string
 
50
  conceptDesigner?: string
51
  codeGeneration?: string
52
  codeRetry?: string
53
+ codeEdit?: string
54
  }
55
  user?: {
56
  conceptDesigner?: string
57
  codeGeneration?: string
58
  codeRetryInitial?: string
59
  codeRetryFix?: string
60
+ codeEdit?: string
61
  }
62
  }
63
 
64
  /**
65
+ * 瑙嗛鐢熸垚浠诲姟鏁版嵁
66
  */
67
  export interface VideoJobData {
68
  jobId: string
 
70
  quality: VideoQuality
71
  forceRefresh?: boolean
72
  timestamp: string
73
+ /** 棰勭敓鎴愮殑浠g爜锛堜娇鐢ㄨ嚜瀹氫箟 AI 鏃讹級 */
74
  preGeneratedCode?: string
75
+ /** AI 淇敼鏃剁殑鍘熷浠g爜 */
76
+ editCode?: string
77
+ /** AI 淇敼鏃剁殑鐢ㄦ埛鎸囦护 */
78
+ editInstructions?: string
79
+ /** 鑷畾涔?API 閰嶇疆锛堢敤浜庝唬鐮佷慨澶嶏級 */
80
  customApiConfig?: CustomApiConfig
81
+ /** 瑙嗛閰嶇疆 */
82
  videoConfig?: VideoConfig
83
+ /** Prompt 覆盖 */
84
+ promptOverrides?: PromptOverrides
85
  }
86
 
87
  /**
88
+ * 浠诲姟缁撴灉 - 瀹屾垚鐘舵€?
89
  */
90
  export interface CompletedJobResult {
91
  status: 'completed'
 
101
  }
102
 
103
  /**
104
+ * 浠诲姟缁撴灉 - 澶辫触鐘舵€?
105
  */
106
  export interface FailedJobResult {
107
  status: 'failed'
 
114
  }
115
 
116
  /**
117
+ * 浠诲姟缁撴灉鑱斿悎绫诲瀷
118
  */
119
  export type JobResult = CompletedJobResult | FailedJobResult
120
 
121
  /**
122
+ * 姒傚康缂撳瓨鏁版嵁
123
  */
124
  export interface ConceptCacheData {
125
  jobId: string
 
134
  }
135
 
136
  /**
137
+ * API 璇锋眰 - 鐢熸垚瑙嗛
138
  */
139
  export interface GenerateRequest {
140
  concept: string
 
144
  }
145
 
146
  /**
147
+ * API 璇锋眰 - AI 淇敼
148
+ */
149
+ export interface ModifyRequest {
150
+ concept: string
151
+ quality?: VideoQuality
152
+ instructions: string
153
+ code: string
154
+ promptOverrides?: PromptOverrides
155
+ videoConfig?: VideoConfig
156
+ customApiConfig?: CustomApiConfig
157
+ }
158
+
159
+ /**
160
+ * API 鍝嶅簲 - 鐢熸垚瑙嗛
161
  */
162
  export interface GenerateResponse {
163
  success: boolean
 
167
  }
168
 
169
  /**
170
+ * API 鍝嶅簲 - 浠诲姟鐘舵€侊紙澶勭悊涓級
171
  */
172
  export interface JobStatusProcessingResponse {
173
  status: 'processing' | 'queued'
 
177
  }
178
 
179
  /**
180
+ * API 鍝嶅簲 - 浠诲姟鐘舵€侊紙瀹屾垚锛?
181
+ * 涓庡墠绔?api.ts JobResult 绫诲瀷鍏煎
182
  */
183
  export interface JobStatusCompletedResponse {
184
  status: 'completed'
 
193
  }
194
 
195
  /**
196
+ * API 鍝嶅簲 - 浠诲姟鐘舵€侊紙澶辫触锛?
197
+ * 涓庡墠绔?api.ts JobResult 绫诲瀷鍏煎
198
  */
199
  export interface JobStatusFailedResponse {
200
  status: 'failed'
 
206
  }
207
 
208
  /**
209
+ * API 鍝嶅簲 - 浠诲姟鐘舵€佽仈鍚堢被鍨?
210
  */
211
  export type JobStatusResponse =
212
  | JobStatusProcessingResponse
 
214
  | JobStatusFailedResponse
215
 
216
  /**
217
+ * API 鍝嶅簲 - 鍋ュ悍妫€鏌?
218
  */
219
  export interface HealthCheckResponse {
220
  status: 'ok' | 'degraded' | 'down'
 
235
  }
236
 
237
  /**
238
+ * API 閿欒鍝嶅簲
239
  */
240
  export interface ErrorResponse {
241
  error: string
 
244
  }
245
 
246
  /**
247
+ * Bull 浠诲姟杩涘害鏁版嵁
248
  */
249
  export interface JobProgress {
250
  step: string
 
253
  }
254
 
255
  /**
256
+ * Manim 娓叉煋閫夐」
257
  */
258
  export interface ManimRenderOptions {
259
  quality: VideoQuality
 
263
  }
264
 
265
  /**
266
+ * 缂撳瓨鏌ヨ缁撴灉
267
  */
268
  export interface CacheCheckResult {
269
  hit: boolean
270
  data?: ConceptCacheData
271
  }
272
+