claudqunwang commited on
Commit
98148dc
·
1 Parent(s): b642ebc

Switch AI courseware APIs to snake_case schema

Browse files
api/routes_courseware_ai.py CHANGED
@@ -14,7 +14,7 @@ AI Courseware AI 接口 (Plan / Prepare / Reflect / Improve)。
14
 
15
  import json
16
  import time
17
- from typing import List, Optional, Any
18
 
19
  from fastapi import APIRouter, HTTPException
20
  from pydantic import BaseModel, Field
@@ -112,31 +112,32 @@ def _call_openai_json(
112
 
113
 
114
  class SyllabusContext(BaseModel):
115
- courseName: str
116
- learningOutcome: str
117
- studentLevel: str
118
- teachingFocus: str
119
- courseLength: int = Field(..., ge=1, le=52)
120
 
121
 
122
  class SyllabusGenerateRequest(BaseModel):
123
- requestId: str
124
  context: SyllabusContext
 
125
 
126
 
127
  class WeekSyllabus(BaseModel):
128
- weekNumber: int
129
  title: str
130
- learningObjectives: List[str]
131
  topics: List[str]
132
 
133
 
134
  class SyllabusGenerateData(BaseModel):
135
- courseOverview: str | None = None
136
  assessment: str | None = None
137
- teachingApproach: str | None = None
138
- workloadExpectation: str | None = None
139
- academicIntegrityPolicy: str | None = None
140
  syllabus: List[WeekSyllabus]
141
 
142
 
@@ -149,37 +150,42 @@ class SyllabusGenerateResponse(BaseModel):
149
  def generate_syllabus(req: SyllabusGenerateRequest):
150
  """Phase 1 (Plan):根据课程元数据生成 Weeks/Modules 大纲草案。"""
151
  ctx = req.context
 
152
  sys_prompt = (
153
  "You are an instructional designer. "
154
  "Given course metadata, generate a weekly syllabus and key syllabus header fields as JSON. "
155
- "Output ONLY a JSON object with keys 'courseOverview', 'assessment', 'teachingApproach', "
156
- "'workloadExpectation', 'academicIntegrityPolicy', and 'syllabus', matching the provided schema."
 
157
  )
158
  user_prompt = f"""
159
- Course name: {ctx.courseName}
160
- Learning outcome: {ctx.learningOutcome}
161
- Student level: {ctx.studentLevel}
162
- Teaching focus: {ctx.teachingFocus}
163
- Total weeks/modules: {ctx.courseLength}
 
 
 
164
 
165
  Return JSON:
166
  {{
167
- "courseOverview": "One short paragraph course overview",
168
  "assessment": "Assessment breakdown text...",
169
- "teachingApproach": "How the course will be taught...",
170
- "workloadExpectation": "Expected weekly workload...",
171
- "academicIntegrityPolicy": "Short academic integrity policy...",
172
  "syllabus": [
173
  {{
174
- "weekNumber": 1,
175
  "title": "string",
176
- "learningObjectives": ["string", "..."],
177
  "topics": ["string", "..."]
178
  }},
179
  ...
180
  ]
181
  }}
182
- Make sure the array length equals courseLength ({ctx.courseLength}) and weekNumber starts from 1.
183
  """
184
 
185
  data_dict, meta = _call_openai_json("syllabus.generate", sys_prompt, user_prompt, temperature=0.4)
@@ -195,27 +201,27 @@ Make sure the array length equals courseLength ({ctx.courseLength}) and weekNumb
195
 
196
 
197
  class SyllabusLockedSections(BaseModel):
198
- courseOverview: Optional[str] = None
199
  assessment: Optional[str] = None
200
- teachingApproach: Optional[str] = None
201
- workloadExpectation: Optional[str] = None
202
- academicIntegrityPolicy: Optional[str] = None
 
203
 
204
 
205
  class SyllabusUnlockedSections(BaseModel):
206
- # 目前仅支持对 syllabus 数组进行重写;保留为任意对象数组以兼容前端裁剪字段
207
- syllabus: List[dict] = Field(default_factory=list)
208
 
209
 
210
  class CurrentSyllabus(BaseModel):
211
- lockedSections: SyllabusLockedSections
212
- unlockedSections: SyllabusUnlockedSections
213
 
214
 
215
  class SyllabusPartialRequest(BaseModel):
216
- requestId: str
217
  prompt: str
218
- currentSyllabus: CurrentSyllabus
219
  context: SyllabusContext
220
 
221
 
@@ -236,8 +242,8 @@ class SyllabusPartialResponse(BaseModel):
236
  @router.post("/syllabus/regenerate-partial", response_model=SyllabusPartialResponse)
237
  def regenerate_partial_syllabus(req: SyllabusPartialRequest):
238
  """Phase 1 (Plan):在锁定部分大纲后,根据 Prompt 重写未锁定部分(主要是 syllabus 数组)。"""
239
- locked_sections = req.currentSyllabus.lockedSections.model_dump()
240
- unlocked_sections = req.currentSyllabus.unlockedSections.model_dump()
241
  ctx = req.context
242
 
243
  sys_prompt = (
@@ -252,11 +258,11 @@ User prompt:
252
  {req.prompt}
253
 
254
  Course context:
255
- - courseName: {ctx.courseName}
256
- - learningOutcome: {ctx.learningOutcome}
257
- - studentLevel: {ctx.studentLevel}
258
- - teachingFocus: {ctx.teachingFocus}
259
- - courseLength: {ctx.courseLength}
260
 
261
  Locked sections (MUST remain unchanged, only for your reference):
262
  {json.dumps(locked_sections, ensure_ascii=False)}
@@ -270,9 +276,9 @@ Return JSON:
270
  "proposed_draft": {{
271
  "syllabus": [
272
  {{
273
- "weekNumber": 1,
274
  "title": "string",
275
- "learningObjectives": ["string", "..."],
276
  "topics": ["string", "..."]
277
  }}
278
  ]
@@ -305,26 +311,26 @@ Return JSON:
305
 
306
  class ModuleContext(BaseModel):
307
  title: str
308
- learningObjectives: List[str]
309
  topics: List[str]
310
- durationMinutes: int = Field(..., ge=10, le=600)
311
 
312
 
313
  class FlowGenerateRequest(BaseModel):
314
- requestId: str
315
- moduleContext: ModuleContext
316
- systemPrompts: Optional[List[str]] = None
317
 
318
 
319
  class LessonStep(BaseModel):
320
  type: str
321
  title: str
322
- estimatedDuration: int = Field(..., ge=1)
323
- aiUnderstanding: str
324
- teacherActivity: Optional[str] = None
325
- studentActivity: Optional[str] = None
326
- guidingQuestions: Optional[str] = None
327
- requiredMaterials: Optional[str] = None
328
 
329
 
330
  class FlowGenerateData(BaseModel):
@@ -339,8 +345,8 @@ class FlowGenerateResponse(BaseModel):
339
  @router.post("/flow/generate", response_model=FlowGenerateResponse)
340
  def generate_lesson_flow(req: FlowGenerateRequest):
341
  """Phase 2 (Prepare):为单个 Module 生成完整教学步骤列表。"""
342
- mc = req.moduleContext
343
- extra_system = "\n".join(req.systemPrompts or [])
344
  sys_prompt = (
345
  "You are an AI courseware copilot. "
346
  "Generate a detailed lesson flow for one module as JSON. "
@@ -348,9 +354,9 @@ def generate_lesson_flow(req: FlowGenerateRequest):
348
  )
349
  user_prompt = f"""
350
  Module title: {mc.title}
351
- Learning objectives: {mc.learningObjectives}
352
  Topics: {mc.topics}
353
- Duration (minutes): {mc.durationMinutes}
354
 
355
  Additional long-term system prompts (optional, may be empty):
356
  {extra_system}
@@ -361,17 +367,17 @@ Return JSON:
361
  {{
362
  "type": "Teacher Explanation | Interactive Quiz | Group Discussion | ...",
363
  "title": "string",
364
- "estimatedDuration": 10,
365
- "aiUnderstanding": "short explanation of why this step exists and what it should achieve",
366
- "teacherActivity": "what the teacher should concretely do/say in this step",
367
- "studentActivity": "what students are expected to do",
368
- "guidingQuestions": "- question 1\\n- question 2",
369
- "requiredMaterials": "Projector, handouts, whiteboard, ..."
370
  }},
371
  ...
372
  ]
373
  }}
374
- The total sum of estimatedDuration should be roughly close to durationMinutes ({mc.durationMinutes}).
375
  """
376
 
377
  data_dict, meta = _call_openai_json("flow.generate", sys_prompt, user_prompt, temperature=0.5)
@@ -393,30 +399,30 @@ class SimpleStep(BaseModel):
393
 
394
 
395
  class CurrentFlow(BaseModel):
396
- lockedSteps: List[SimpleStep]
397
- unlockedSteps: List[SimpleStep]
398
 
399
 
400
  class FlowPartialRequest(BaseModel):
401
- requestId: str
402
  prompt: str
403
- currentFlow: CurrentFlow
404
 
405
 
406
  class CopilotProposedStep(BaseModel):
407
  type: str
408
  title: str
409
- estimatedDuration: int
410
- aiUnderstanding: str
411
- teacherActivity: Optional[str] = None
412
- studentActivity: Optional[str] = None
413
- guidingQuestions: Optional[str] = None
414
- requiredMaterials: Optional[str] = None
415
 
416
 
417
  class FlowPartialData(BaseModel):
418
  explanation: str
419
- proposedSteps: List[CopilotProposedStep]
420
 
421
 
422
  class FlowPartialResponse(BaseModel):
@@ -427,8 +433,8 @@ class FlowPartialResponse(BaseModel):
427
  @router.post("/flow/regenerate-partial", response_model=FlowPartialResponse)
428
  def regenerate_partial_flow(req: FlowPartialRequest):
429
  """Phase 2 (Prepare):在锁定部分步骤后,根据 Prompt 重写剩余步骤。"""
430
- locked = [s.model_dump() for s in req.currentFlow.lockedSteps]
431
- unlocked = [s.model_dump() for s in req.currentFlow.unlockedSteps]
432
  sys_prompt = (
433
  "You are an AI copilot for lesson flow editing. "
434
  "Given lockedSteps and unlockedSteps plus a user prompt, "
@@ -446,16 +452,16 @@ unlockedSteps: {unlocked}
446
  Return JSON:
447
  {{
448
  "explanation": "short natural language explanation for the teacher UI bubble",
449
- "proposedSteps": [
450
  {{
451
  "type": "Teacher Explanation | Interactive Quiz | Group Discussion | ...",
452
  "title": "string",
453
- "estimatedDuration": 10,
454
- "aiUnderstanding": "short explanation of this step",
455
- "teacherActivity": "what the teacher should concretely do/say in this step",
456
- "studentActivity": "what students are expected to do",
457
- "guidingQuestions": "- question 1\\n- question 2",
458
- "requiredMaterials": "Projector, handouts, whiteboard, ..."
459
  }}
460
  ]
461
  }}
@@ -464,10 +470,10 @@ Return JSON:
464
  data_dict, meta = _call_openai_json(
465
  "flow.regenerate-partial", sys_prompt, user_prompt, temperature=0.5
466
  )
467
- if "explanation" not in data_dict or "proposedSteps" not in data_dict:
468
  raise HTTPException(
469
  status_code=422,
470
- detail="INVALID_GENERATION: 'explanation' or 'proposedSteps' missing in JSON.",
471
  )
472
 
473
  return FlowPartialResponse(data=FlowPartialData(**data_dict), meta=meta)
@@ -479,17 +485,19 @@ Return JSON:
479
 
480
 
481
  class PlanDetailRequest(BaseModel):
482
- requestId: str
483
- finalizedSteps: List[Any]
484
 
485
 
486
  class LessonSection(BaseModel):
487
  section_id: str
488
  type: str
 
489
  content: str
490
 
491
 
492
  class PlanDetailData(BaseModel):
 
493
  sections: List[LessonSection]
494
 
495
 
@@ -508,19 +516,22 @@ def generate_plan_detail(req: PlanDetailRequest):
508
  )
509
  user_prompt = f"""
510
  Finalized steps (JSON array, opaque to you but describes lesson flow):
511
- {json.dumps(req.finalizedSteps, ensure_ascii=False)}
512
 
513
  Return JSON:
514
  {{
 
515
  "sections": [
516
  {{
517
- "section_id": "sec_obj_01",
518
- "type": "Lesson Objectives",
 
519
  "content": "By the end of this lesson, students will be able to..."
520
  }},
521
  {{
522
- "section_id": "sec_content_01",
523
- "type": "Content",
 
524
  "content": "### 1. Topic heading\\nDetailed explanation..."
525
  }}
526
  ]
@@ -556,54 +567,65 @@ class TeachAnnotation(BaseModel):
556
 
557
  dimension: str # e.g. "TIME" | "DIFFICULTY" | "ENGAGEMENT"
558
  level: str # e.g. "LIMITED" | "ADEQUATE" | "STRONG"
559
- selectedText: Optional[str] = None
560
  feedback: Optional[str] = None
561
 
562
 
563
  class QuizAggregations(BaseModel):
564
- averageScore: Optional[float] = None
565
- lowestTopic: Optional[str] = None
566
 
567
 
568
  class ReflectionRequest(BaseModel):
569
- requestId: str
570
- lessonSteps: List[LessonStepForReflection] = Field(default_factory=list)
571
- teachAnnotations: List[TeachAnnotation] = Field(default_factory=list)
572
- quizAggregations: Optional[QuizAggregations] = None
573
 
574
 
575
- class ReflectionUnderstanding(BaseModel):
576
- status: str
577
- summary: str
578
- bulletPoints: Optional[List[str]] = None
 
579
 
580
 
581
- class ReflectionEngagement(BaseModel):
582
  status: str
583
- summary: Optional[str] = None
 
584
 
585
 
586
- class ReflectionDifficulty(BaseModel):
587
- status: str
588
- challengingTopics: Optional[List[dict]] = None
 
 
589
 
590
 
591
- class ReflectionMisconception(BaseModel):
592
- status: str
593
- issues: Optional[List[dict]] = None
 
594
 
595
 
596
  class NextLessonSuggestion(BaseModel):
597
- actionText: str
598
- deepLinkType: Optional[str] = None
 
 
 
 
 
 
 
 
599
 
600
 
601
  class ReflectionData(BaseModel):
602
- understanding: ReflectionUnderstanding
603
- engagement: ReflectionEngagement
604
- difficulty: ReflectionDifficulty
605
- misconceptions: ReflectionMisconception
606
- nextLessonSuggestions: List[NextLessonSuggestion]
607
 
608
 
609
  class ReflectionResponse(BaseModel):
@@ -614,13 +636,15 @@ class ReflectionResponse(BaseModel):
614
  @router.post("/reflection/generate", response_model=ReflectionResponse)
615
  def generate_reflection(req: ReflectionRequest):
616
  """Phase 4 (Reflect):基于反馈与测验数据生成反思看板。"""
617
- lesson_steps = [s.model_dump() for s in req.lessonSteps]
618
- annotations = [a.model_dump() for a in req.teachAnnotations]
619
- qa = req.quizAggregations.model_dump() if req.quizAggregations else {}
620
  sys_prompt = (
621
  "You are an AI teaching analytics assistant. "
622
- "Given lesson steps, teacher annotations, and quiz aggregations, generate a reflection dashboard as JSON "
623
- "with understanding, engagement, difficulty, misconceptions, and nextLessonSuggestions."
 
 
624
  )
625
  user_prompt = f"""
626
  Lesson steps:
@@ -634,34 +658,59 @@ Quiz aggregations:
634
 
635
  Return JSON:
636
  {{
637
- "understanding": {{
638
- "status": "Low | Medium | High",
639
- "summary": "short text",
640
- "bulletPoints": ["optional", "..."]
641
- }},
642
- "engagement": {{
643
- "status": "Low | Medium | High",
644
- "summary": "optional"
645
- }},
646
- "difficulty": {{
647
- "status": "Low | Medium | High",
648
- "challengingTopics": [{{"topic": "string", "percentStruggle": 45}}]
649
- }},
650
- "misconceptions": {{
651
- "status": "Low | Medium | High",
652
- "issues": [{{"title": "string", "description": "string", "evidence": "string"}}]
653
- }},
654
- "nextLessonSuggestions": [
655
- {{
656
- "actionText": "string",
657
- "deepLinkType": "PREPARE_FLOW_EDIT | REFLECT_DASHBOARD | ..."
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
658
  }}
659
- ]
 
 
 
 
 
 
 
 
 
660
  }}
661
  """
662
 
663
  data_dict, meta = _call_openai_json("reflection.generate", sys_prompt, user_prompt, temperature=0.4)
664
- required = ["understanding", "engagement", "difficulty", "misconceptions", "nextLessonSuggestions"]
665
  if any(k not in data_dict for k in required):
666
  raise HTTPException(
667
  status_code=422,
@@ -677,18 +726,18 @@ Return JSON:
677
 
678
 
679
  class ImprovementRequest(BaseModel):
680
- requestId: str
681
- reflectionReports: List[Any]
682
 
683
 
684
  class ImprovementProposal(BaseModel):
685
  title: str
686
  priority: str
687
- affectedWeeks: Optional[str] = None
688
  evidence: Optional[str] = None
689
- rootCause: Optional[str] = None
690
- proposedSolution: Optional[str] = None
691
- expectedImpact: Optional[str] = None
692
 
693
 
694
  class ImprovementData(BaseModel):
@@ -703,7 +752,7 @@ class ImprovementResponse(BaseModel):
703
  @router.post("/improvement/generate", response_model=ImprovementResponse)
704
  def generate_improvement(req: ImprovementRequest):
705
  """Phase 5 (Improve):基于过往反思报告生成跨期改进提案。"""
706
- reports = json.dumps(req.reflectionReports, ensure_ascii=False)
707
  sys_prompt = (
708
  "You are an AI curriculum improvement assistant. "
709
  "Given past reflection reports, generate high-level improvement proposals as JSON."
@@ -718,11 +767,11 @@ Return JSON:
718
  {{
719
  "title": "Students struggle with gradient descent optimization",
720
  "priority": "High Priority | Medium Priority | Low Priority",
721
- "affectedWeeks": "Week 2, 3",
722
  "evidence": "45% reported difficulty...",
723
- "rootCause": "Introduced too quickly without visual math foundation.",
724
- "proposedSolution": "Split gradient descent into two lessons: (1) Intuition (2) Math.",
725
- "expectedImpact": "Increase scores by 15-20% and reduce frustration."
726
  }}
727
  ]
728
  }}
 
14
 
15
  import json
16
  import time
17
+ from typing import List, Optional, Any, Dict
18
 
19
  from fastapi import APIRouter, HTTPException
20
  from pydantic import BaseModel, Field
 
112
 
113
 
114
  class SyllabusContext(BaseModel):
115
+ course_name: str
116
+ learning_outcome: str
117
+ student_level: str
118
+ teaching_focus: str
119
+ course_length: int = Field(..., ge=1, le=52)
120
 
121
 
122
  class SyllabusGenerateRequest(BaseModel):
123
+ request_id: str
124
  context: SyllabusContext
125
+ system_prompts: Optional[List[str]] = None
126
 
127
 
128
  class WeekSyllabus(BaseModel):
129
+ week_number: int
130
  title: str
131
+ learning_objectives: List[str]
132
  topics: List[str]
133
 
134
 
135
  class SyllabusGenerateData(BaseModel):
136
+ course_overview: str | None = None
137
  assessment: str | None = None
138
+ teaching_approach: str | None = None
139
+ workload_expectation: str | None = None
140
+ academic_integrity_policy: str | None = None
141
  syllabus: List[WeekSyllabus]
142
 
143
 
 
150
  def generate_syllabus(req: SyllabusGenerateRequest):
151
  """Phase 1 (Plan):根据课程元数据生成 Weeks/Modules 大纲草案。"""
152
  ctx = req.context
153
+ extra_system = "\n".join(req.system_prompts or [])
154
  sys_prompt = (
155
  "You are an instructional designer. "
156
  "Given course metadata, generate a weekly syllabus and key syllabus header fields as JSON. "
157
+ "Output ONLY a JSON object using snake_case keys: "
158
+ "'course_overview', 'assessment', 'teaching_approach', "
159
+ "'workload_expectation', 'academic_integrity_policy', and 'syllabus'."
160
  )
161
  user_prompt = f"""
162
+ Course name: {ctx.course_name}
163
+ Learning outcome: {ctx.learning_outcome}
164
+ Student level: {ctx.student_level}
165
+ Teaching focus: {ctx.teaching_focus}
166
+ Total weeks/modules: {ctx.course_length}
167
+
168
+ Additional long-term system prompts (optional, may be empty):
169
+ {extra_system}
170
 
171
  Return JSON:
172
  {{
173
+ "course_overview": "One short paragraph course overview",
174
  "assessment": "Assessment breakdown text...",
175
+ "teaching_approach": "How the course will be taught...",
176
+ "workload_expectation": "Expected weekly workload...",
177
+ "academic_integrity_policy": "Short academic integrity policy...",
178
  "syllabus": [
179
  {{
180
+ "week_number": 1,
181
  "title": "string",
182
+ "learning_objectives": ["string", "..."],
183
  "topics": ["string", "..."]
184
  }},
185
  ...
186
  ]
187
  }}
188
+ Make sure the array length equals course_length ({ctx.course_length}) and week_number starts from 1.
189
  """
190
 
191
  data_dict, meta = _call_openai_json("syllabus.generate", sys_prompt, user_prompt, temperature=0.4)
 
201
 
202
 
203
  class SyllabusLockedSections(BaseModel):
204
+ course_overview: Optional[str] = None
205
  assessment: Optional[str] = None
206
+ teaching_approach: Optional[str] = None
207
+ workload_expectation: Optional[str] = None
208
+ academic_integrity_policy: Optional[str] = None
209
+ syllabus: Optional[List[Dict[str, Any]]] = None
210
 
211
 
212
  class SyllabusUnlockedSections(BaseModel):
213
+ syllabus: List[Dict[str, Any]] = Field(default_factory=list)
 
214
 
215
 
216
  class CurrentSyllabus(BaseModel):
217
+ locked_sections: SyllabusLockedSections
218
+ unlocked_sections: SyllabusUnlockedSections
219
 
220
 
221
  class SyllabusPartialRequest(BaseModel):
222
+ request_id: str
223
  prompt: str
224
+ current_syllabus: CurrentSyllabus
225
  context: SyllabusContext
226
 
227
 
 
242
  @router.post("/syllabus/regenerate-partial", response_model=SyllabusPartialResponse)
243
  def regenerate_partial_syllabus(req: SyllabusPartialRequest):
244
  """Phase 1 (Plan):在锁定部分大纲后,根据 Prompt 重写未锁定部分(主要是 syllabus 数组)。"""
245
+ locked_sections = req.current_syllabus.locked_sections.model_dump()
246
+ unlocked_sections = req.current_syllabus.unlocked_sections.model_dump()
247
  ctx = req.context
248
 
249
  sys_prompt = (
 
258
  {req.prompt}
259
 
260
  Course context:
261
+ - course_name: {ctx.course_name}
262
+ - learning_outcome: {ctx.learning_outcome}
263
+ - student_level: {ctx.student_level}
264
+ - teaching_focus: {ctx.teaching_focus}
265
+ - course_length: {ctx.course_length}
266
 
267
  Locked sections (MUST remain unchanged, only for your reference):
268
  {json.dumps(locked_sections, ensure_ascii=False)}
 
276
  "proposed_draft": {{
277
  "syllabus": [
278
  {{
279
+ "week_number": 1,
280
  "title": "string",
281
+ "learning_objectives": ["string", "..."],
282
  "topics": ["string", "..."]
283
  }}
284
  ]
 
311
 
312
  class ModuleContext(BaseModel):
313
  title: str
314
+ learning_objectives: List[str]
315
  topics: List[str]
316
+ duration_minutes: int = Field(..., ge=10, le=600)
317
 
318
 
319
  class FlowGenerateRequest(BaseModel):
320
+ request_id: str
321
+ module_context: ModuleContext
322
+ system_prompts: Optional[List[str]] = None
323
 
324
 
325
  class LessonStep(BaseModel):
326
  type: str
327
  title: str
328
+ estimated_duration: int = Field(..., ge=1)
329
+ ai_understanding: str
330
+ teacher_activity: Optional[str] = None
331
+ student_activity: Optional[str] = None
332
+ guiding_questions: Optional[str] = None
333
+ required_materials: Optional[str] = None
334
 
335
 
336
  class FlowGenerateData(BaseModel):
 
345
  @router.post("/flow/generate", response_model=FlowGenerateResponse)
346
  def generate_lesson_flow(req: FlowGenerateRequest):
347
  """Phase 2 (Prepare):为单个 Module 生成完整教学步骤列表。"""
348
+ mc = req.module_context
349
+ extra_system = "\n".join(req.system_prompts or [])
350
  sys_prompt = (
351
  "You are an AI courseware copilot. "
352
  "Generate a detailed lesson flow for one module as JSON. "
 
354
  )
355
  user_prompt = f"""
356
  Module title: {mc.title}
357
+ Learning objectives: {mc.learning_objectives}
358
  Topics: {mc.topics}
359
+ Duration (minutes): {mc.duration_minutes}
360
 
361
  Additional long-term system prompts (optional, may be empty):
362
  {extra_system}
 
367
  {{
368
  "type": "Teacher Explanation | Interactive Quiz | Group Discussion | ...",
369
  "title": "string",
370
+ "estimated_duration": 10,
371
+ "ai_understanding": "short explanation of why this step exists and what it should achieve",
372
+ "teacher_activity": "what the teacher should concretely do/say in this step",
373
+ "student_activity": "what students are expected to do",
374
+ "guiding_questions": "- question 1\\n- question 2",
375
+ "required_materials": "Projector, handouts, whiteboard, ..."
376
  }},
377
  ...
378
  ]
379
  }}
380
+ The total sum of estimated_duration should be roughly close to duration_minutes ({mc.duration_minutes}).
381
  """
382
 
383
  data_dict, meta = _call_openai_json("flow.generate", sys_prompt, user_prompt, temperature=0.5)
 
399
 
400
 
401
  class CurrentFlow(BaseModel):
402
+ locked_steps: List[SimpleStep]
403
+ unlocked_steps: List[SimpleStep]
404
 
405
 
406
  class FlowPartialRequest(BaseModel):
407
+ request_id: str
408
  prompt: str
409
+ current_flow: CurrentFlow
410
 
411
 
412
  class CopilotProposedStep(BaseModel):
413
  type: str
414
  title: str
415
+ estimated_duration: int
416
+ ai_understanding: str
417
+ teacher_activity: Optional[str] = None
418
+ student_activity: Optional[str] = None
419
+ guiding_questions: Optional[str] = None
420
+ required_materials: Optional[str] = None
421
 
422
 
423
  class FlowPartialData(BaseModel):
424
  explanation: str
425
+ proposed_steps: List[CopilotProposedStep]
426
 
427
 
428
  class FlowPartialResponse(BaseModel):
 
433
  @router.post("/flow/regenerate-partial", response_model=FlowPartialResponse)
434
  def regenerate_partial_flow(req: FlowPartialRequest):
435
  """Phase 2 (Prepare):在锁定部分步骤后,根据 Prompt 重写剩余步骤。"""
436
+ locked = [s.model_dump() for s in req.current_flow.locked_steps]
437
+ unlocked = [s.model_dump() for s in req.current_flow.unlocked_steps]
438
  sys_prompt = (
439
  "You are an AI copilot for lesson flow editing. "
440
  "Given lockedSteps and unlockedSteps plus a user prompt, "
 
452
  Return JSON:
453
  {{
454
  "explanation": "short natural language explanation for the teacher UI bubble",
455
+ "proposed_steps": [
456
  {{
457
  "type": "Teacher Explanation | Interactive Quiz | Group Discussion | ...",
458
  "title": "string",
459
+ "estimated_duration": 10,
460
+ "ai_understanding": "short explanation of this step",
461
+ "teacher_activity": "what the teacher should concretely do/say in this step",
462
+ "student_activity": "what students are expected to do",
463
+ "guiding_questions": "- question 1\\n- question 2",
464
+ "required_materials": "Projector, handouts, whiteboard, ..."
465
  }}
466
  ]
467
  }}
 
470
  data_dict, meta = _call_openai_json(
471
  "flow.regenerate-partial", sys_prompt, user_prompt, temperature=0.5
472
  )
473
+ if "explanation" not in data_dict or "proposed_steps" not in data_dict:
474
  raise HTTPException(
475
  status_code=422,
476
+ detail="INVALID_GENERATION: 'explanation' or 'proposed_steps' missing in JSON.",
477
  )
478
 
479
  return FlowPartialResponse(data=FlowPartialData(**data_dict), meta=meta)
 
485
 
486
 
487
  class PlanDetailRequest(BaseModel):
488
+ request_id: str
489
+ finalized_steps: List[Any]
490
 
491
 
492
  class LessonSection(BaseModel):
493
  section_id: str
494
  type: str
495
+ title: str
496
  content: str
497
 
498
 
499
  class PlanDetailData(BaseModel):
500
+ course_name: Optional[str] = None
501
  sections: List[LessonSection]
502
 
503
 
 
516
  )
517
  user_prompt = f"""
518
  Finalized steps (JSON array, opaque to you but describes lesson flow):
519
+ {json.dumps(req.finalized_steps, ensure_ascii=False)}
520
 
521
  Return JSON:
522
  {{
523
+ "course_name": "string",
524
  "sections": [
525
  {{
526
+ "section_id": "learning_objectives",
527
+ "type": "Learning Objectives",
528
+ "title": "Learning Objectives",
529
  "content": "By the end of this lesson, students will be able to..."
530
  }},
531
  {{
532
+ "section_id": "lesson_activities",
533
+ "type": "Lesson Activities",
534
+ "title": "Lesson Activities",
535
  "content": "### 1. Topic heading\\nDetailed explanation..."
536
  }}
537
  ]
 
567
 
568
  dimension: str # e.g. "TIME" | "DIFFICULTY" | "ENGAGEMENT"
569
  level: str # e.g. "LIMITED" | "ADEQUATE" | "STRONG"
570
+ selected_text: Optional[str] = None
571
  feedback: Optional[str] = None
572
 
573
 
574
  class QuizAggregations(BaseModel):
575
+ average_score: Optional[float] = None
576
+ lowest_topic: Optional[str] = None
577
 
578
 
579
  class ReflectionRequest(BaseModel):
580
+ request_id: str
581
+ lesson_steps: List[LessonStepForReflection] = Field(default_factory=list)
582
+ teach_annotations: List[TeachAnnotation] = Field(default_factory=list)
583
+ quiz_aggregations: Optional[QuizAggregations] = None
584
 
585
 
586
+ class ReflectionDetails(BaseModel):
587
+ description: Optional[str] = None
588
+ bullet_points: Optional[List[str]] = None
589
+ challenging_topics: Optional[List[str]] = None
590
+ issues: Optional[List[str]] = None
591
 
592
 
593
+ class ReflectionField(BaseModel):
594
  status: str
595
+ summary: str
596
+ details: ReflectionDetails
597
 
598
 
599
+ class UpperSectionFields(BaseModel):
600
+ understanding: ReflectionField
601
+ engagement: ReflectionField
602
+ difficulty: ReflectionField
603
+ misconceptions: ReflectionField
604
 
605
 
606
+ class UpperSection(BaseModel):
607
+ description: Optional[str] = None
608
+ required: bool = True
609
+ fields: UpperSectionFields
610
 
611
 
612
  class NextLessonSuggestion(BaseModel):
613
+ text: str
614
+ deep_link_type: Optional[str] = None
615
+
616
+
617
+ class LowerSection(BaseModel):
618
+ summary: str
619
+ description: Optional[str] = None
620
+ overall_summary: Optional[str] = None
621
+ strengths: List[str]
622
+ next_lesson_suggestions: List[NextLessonSuggestion]
623
 
624
 
625
  class ReflectionData(BaseModel):
626
+ overall_status: str
627
+ upper_section: UpperSection
628
+ lower_section: LowerSection
 
 
629
 
630
 
631
  class ReflectionResponse(BaseModel):
 
636
  @router.post("/reflection/generate", response_model=ReflectionResponse)
637
  def generate_reflection(req: ReflectionRequest):
638
  """Phase 4 (Reflect):基于反馈与测验数据生成反思看板。"""
639
+ lesson_steps = [s.model_dump() for s in req.lesson_steps]
640
+ annotations = [a.model_dump() for a in req.teach_annotations]
641
+ qa = req.quiz_aggregations.model_dump() if req.quiz_aggregations else {}
642
  sys_prompt = (
643
  "You are an AI teaching analytics assistant. "
644
+ "Given lesson steps, teacher annotations, and quiz aggregations, generate a reflection dashboard as JSON. "
645
+ "Use snake_case keys and match this structure: overall_status, upper_section(fields: understanding/engagement/"
646
+ "difficulty/misconceptions), and lower_section(summary/strengths/next_lesson_suggestions). "
647
+ "Output ONLY the JSON object that matches the schema."
648
  )
649
  user_prompt = f"""
650
  Lesson steps:
 
658
 
659
  Return JSON:
660
  {{
661
+ "overall_status": "Overall: Progressing",
662
+ "upper_section": {{
663
+ "description": "One short sentence describing the upper section purpose",
664
+ "required": true,
665
+ "fields": {{
666
+ "understanding": {{
667
+ "status": "Low | Medium | High",
668
+ "summary": "Comprehension of core concepts",
669
+ "details": {{
670
+ "description": "A short paragraph",
671
+ "bullet_points": ["..."]
672
+ }}
673
+ }},
674
+ "engagement": {{
675
+ "status": "Low | Medium | High",
676
+ "summary": "Student participation and motivation",
677
+ "details": {{
678
+ "description": "A short paragraph",
679
+ "bullet_points": ["..."]
680
+ }}
681
+ }},
682
+ "difficulty": {{
683
+ "status": "Low | Medium | High",
684
+ "summary": "Challenging topics and areas",
685
+ "details": {{
686
+ "description": "A short paragraph",
687
+ "challenging_topics": ["Topic A (45% struggle)", "Topic B (38% struggle)"]
688
+ }}
689
+ }},
690
+ "misconceptions": {{
691
+ "status": "Low | Medium | High",
692
+ "summary": "Common misunderstandings identified",
693
+ "details": {{
694
+ "description": "A short paragraph",
695
+ "issues": ["Misconception A — evidence ...", "Misconception B — evidence ..."]
696
+ }}
697
+ }}
698
  }}
699
+ }},
700
+ "lower_section": {{
701
+ "summary": "Lesson 1 Summary",
702
+ "description": "AI-generated overall assessment and recommendations",
703
+ "overall_summary": "One paragraph overall summary",
704
+ "strengths": ["..."],
705
+ "next_lesson_suggestions": [
706
+ {{ "text": "Add visual demos ...", "deep_link_type": "PREPARE_FLOW_EDIT" }}
707
+ ]
708
+ }}
709
  }}
710
  """
711
 
712
  data_dict, meta = _call_openai_json("reflection.generate", sys_prompt, user_prompt, temperature=0.4)
713
+ required = ["overall_status", "upper_section", "lower_section"]
714
  if any(k not in data_dict for k in required):
715
  raise HTTPException(
716
  status_code=422,
 
726
 
727
 
728
  class ImprovementRequest(BaseModel):
729
+ request_id: str
730
+ reflection_reports: List[Any]
731
 
732
 
733
  class ImprovementProposal(BaseModel):
734
  title: str
735
  priority: str
736
+ affected_weeks: Optional[str] = None
737
  evidence: Optional[str] = None
738
+ root_cause: Optional[str] = None
739
+ proposed_solution: Optional[str] = None
740
+ expected_impact: Optional[str] = None
741
 
742
 
743
  class ImprovementData(BaseModel):
 
752
  @router.post("/improvement/generate", response_model=ImprovementResponse)
753
  def generate_improvement(req: ImprovementRequest):
754
  """Phase 5 (Improve):基于过往反思报告生成跨期改进提案。"""
755
+ reports = json.dumps(req.reflection_reports, ensure_ascii=False)
756
  sys_prompt = (
757
  "You are an AI curriculum improvement assistant. "
758
  "Given past reflection reports, generate high-level improvement proposals as JSON."
 
767
  {{
768
  "title": "Students struggle with gradient descent optimization",
769
  "priority": "High Priority | Medium Priority | Low Priority",
770
+ "affected_weeks": "Week 2, 3",
771
  "evidence": "45% reported difficulty...",
772
+ "root_cause": "Introduced too quickly without visual math foundation.",
773
+ "proposed_solution": "Split gradient descent into two lessons: (1) Intuition (2) Math.",
774
+ "expected_impact": "Increase scores by 15-20% and reduce frustration."
775
  }}
776
  ]
777
  }}
docs/AI_COURSEWARE_API_USAGE.md CHANGED
@@ -80,7 +80,7 @@
80
 
81
  前端已经在 `web/src/lib/api.ts` 中提供了 6 个结构化接口的封装函数,可直接在 React 组件中调用。
82
 
83
- #### 3.1 生成大纲草案 `apiAiSyllabusGenerate`
84
 
85
  ```ts
86
  import {
@@ -90,27 +90,27 @@ import {
90
  } from "@/lib/api";
91
 
92
  const req: AiSyllabusGenerateReq = {
93
- requestId: "req_cw_101",
94
  context: {
95
- courseName: "IST 345 Building Generative AI Application",
96
- learningOutcome: "Understand LLMs and build a RAG application",
97
- studentLevel: "BEGINNER",
98
- teachingFocus: "PRACTICE_ORIENTED",
99
- courseLength: 4,
100
  },
101
  };
102
 
103
  const resp: AiSyllabusGenerateResp = await apiAiSyllabusGenerate(req);
104
- // resp.data.courseOverview -> 课程整体简介(段落)
105
  // resp.data.assessment -> 考核方式与比例说明
106
- // resp.data.teachingApproach -> 教学方式描述
107
- // resp.data.workloadExpectation -> 学生每周学习负担预期
108
- // resp.data.academicIntegrityPolicy -> 学术诚信说明(可直接挪用到 Syllabus)
109
  // resp.data.syllabus -> Week 级别数组
110
  // resp.meta -> 模型与性能信息
111
  ```
112
 
113
- #### 3.2 局部重构大纲草案 `apiAiSyllabusRegeneratePartial`
114
 
115
  ```ts
116
  import {
@@ -120,17 +120,17 @@ import {
120
  } from "@/lib/api";
121
 
122
  const req: AiSyllabusPartialReq = {
123
- requestId: "req_cw_101b",
124
  prompt: "Increase the difficulty and add more details on Transformer.",
125
- currentSyllabus: {
126
- lockedSections: {
127
- courseOverview: "This course provides...",
128
  assessment: "Weekly Programming Assignments...",
129
  },
130
- unlockedSections: {
131
  syllabus: [
132
  {
133
- weekNumber: 1,
134
  title: "Introduction to Generative AI",
135
  topics: ["History of AI"],
136
  },
@@ -138,11 +138,11 @@ const req: AiSyllabusPartialReq = {
138
  },
139
  },
140
  context: {
141
- courseName: "IST 345 Building Generative AI Application",
142
- learningOutcome: "Understand LLMs and build a RAG application",
143
- studentLevel: "BEGINNER",
144
- teachingFocus: "PRACTICE_ORIENTED",
145
- courseLength: 4,
146
  },
147
  };
148
 
@@ -151,7 +151,7 @@ const resp: AiSyllabusPartialResp = await apiAiSyllabusRegeneratePartial(req);
151
  // resp.data.proposed_draft.syllabus -> 用于替换未锁定 syllabus 的新大纲
152
  ```
153
 
154
- #### 3.3 生成单个模块的教学流程 `apiAiFlowGenerate`
155
 
156
  ```ts
157
  import {
@@ -161,26 +161,26 @@ import {
161
  } from "@/lib/api";
162
 
163
  const req: AiFlowGenerateReq = {
164
- requestId: "req_cw_102",
165
- moduleContext: {
166
  title: "Introduction to Generative AI",
167
- learningObjectives: ["Understand basic LLM concepts"],
168
  topics: ["History of AI", "Transformer Architecture"],
169
- durationMinutes: 90,
170
  },
171
- systemPrompts: ["(可选) 来自 Improve 阶段的长期提示"],
172
  };
173
 
174
  const resp: AiFlowGenerateResp = await apiAiFlowGenerate(req);
175
- // resp.data.steps[i].estimatedDuration -> 单步预计时长(分钟)
176
- // resp.data.steps[i].aiUnderstanding -> AI 对该步骤教学意图的摘要
177
- // resp.data.steps[i].teacherActivity -> 教师在该步骤的具体行为/讲解要点
178
- // resp.data.steps[i].studentActivity -> 学生在该步骤预期进行的活动
179
- // resp.data.steps[i].guidingQuestions -> 建议的引导提问(Markdown 文本)
180
- // resp.data.steps[i].requiredMaterials -> 所材料/设备清单
181
  ```
182
 
183
- #### 3.4 局部重构教学流程 `apiAiFlowRegeneratePartial`
184
 
185
  ```ts
186
  import {
@@ -190,20 +190,20 @@ import {
190
  } from "@/lib/api";
191
 
192
  const req: AiFlowPartialReq = {
193
- requestId: "req_cw_103",
194
  prompt: "Split the 30-min explanation into two shorter parts with a quiz in between.",
195
- currentFlow: {
196
- lockedSteps: [{ id: "step_1", title: "Welcome", duration: 15 }],
197
- unlockedSteps: [{ id: "step_2", title: "Heavy Explanation", duration: 30 }],
198
  },
199
  };
200
 
201
  const resp: AiFlowPartialResp = await apiAiFlowRegeneratePartial(req);
202
  // resp.data.explanation -> Copilot 气泡文案
203
- // resp.data.proposedSteps -> 用于替换 unlockedSteps 的新步骤
204
  ```
205
 
206
- #### 3.5 教案长文 `apiAiPlanDetailGenerate`
207
 
208
  ```ts
209
  import {
@@ -213,15 +213,16 @@ import {
213
  } from "@/lib/api";
214
 
215
  const req: AiPlanDetailReq = {
216
- requestId: "req_cw_104",
217
- finalizedSteps: stepsArray, // 来自 Flow 阶段确定的完整 steps
218
  };
219
 
220
  const resp: AiPlanDetailResp = await apiAiPlanDetailGenerate(req);
221
- // resp.data.sections -> 多个 sectionLesson Objectives / Content 等,content 支持 Markdown
 
222
  ```
223
 
224
- #### 3.6 反思看板 `apiAiReflectionGenerate`
225
 
226
  ```ts
227
  import {
@@ -231,25 +232,31 @@ import {
231
  } from "@/lib/api";
232
 
233
  const req: AiReflectionReq = {
234
- requestId: "req_cw_105",
235
- teachAnnotations: [
 
 
 
236
  {
237
- category: "NEEDS_MORE_TIME",
238
- selectedText: "Gradient Descent",
 
239
  feedback: "Students were confused by the math",
240
  },
241
  ],
242
- quizAggregations: {
243
- averageScore: 72,
244
- lowestTopic: "Matrix Operations",
245
  },
246
  };
247
 
248
  const resp: AiReflectionResp = await apiAiReflectionGenerate(req);
249
- // resp.data.understanding / engagement / difficulty / misconceptions / nextLessonSuggestions
 
 
250
  ```
251
 
252
- #### 3.7 改进提案 `apiAiImprovementGenerate`
253
 
254
  ```ts
255
  import {
@@ -259,8 +266,8 @@ import {
259
  } from "@/lib/api";
260
 
261
  const req: AiImprovementReq = {
262
- requestId: "req_cw_106",
263
- reflectionReports: previousReflectionReports, // 来自 2.5 的历史数据数组
264
  };
265
 
266
  const resp: AiImprovementResp = await apiAiImprovementGenerate(req);
 
80
 
81
  前端已经在 `web/src/lib/api.ts` 中提供了 6 个结构化接口的封装函数,可直接在 React 组件中调用。
82
 
83
+ #### 3.1 生成大纲草案 `apiAiSyllabusGenerate`(snake_case)
84
 
85
  ```ts
86
  import {
 
90
  } from "@/lib/api";
91
 
92
  const req: AiSyllabusGenerateReq = {
93
+ request_id: "req_cw_101",
94
  context: {
95
+ course_name: "IST 345 Building Generative AI Application",
96
+ learning_outcome: "Understand LLMs and build a RAG application",
97
+ student_level: "BEGINNER",
98
+ teaching_focus: "PRACTICE_ORIENTED",
99
+ course_length: 4,
100
  },
101
  };
102
 
103
  const resp: AiSyllabusGenerateResp = await apiAiSyllabusGenerate(req);
104
+ // resp.data.course_overview -> 课程整体简介(段落)
105
  // resp.data.assessment -> 考核方式与比例说明
106
+ // resp.data.teaching_approach -> 教学方式描述
107
+ // resp.data.workload_expectation -> 学生每周学习负担预期
108
+ // resp.data.academic_integrity_policy -> 学术诚信说明(可直接挪用到 Syllabus)
109
  // resp.data.syllabus -> Week 级别数组
110
  // resp.meta -> 模型与性能信息
111
  ```
112
 
113
+ #### 3.2 局部重构大纲草案 `apiAiSyllabusRegeneratePartial`(snake_case)
114
 
115
  ```ts
116
  import {
 
120
  } from "@/lib/api";
121
 
122
  const req: AiSyllabusPartialReq = {
123
+ request_id: "req_cw_101b",
124
  prompt: "Increase the difficulty and add more details on Transformer.",
125
+ current_syllabus: {
126
+ locked_sections: {
127
+ course_overview: "This course provides...",
128
  assessment: "Weekly Programming Assignments...",
129
  },
130
+ unlocked_sections: {
131
  syllabus: [
132
  {
133
+ week_number: 1,
134
  title: "Introduction to Generative AI",
135
  topics: ["History of AI"],
136
  },
 
138
  },
139
  },
140
  context: {
141
+ course_name: "IST 345 Building Generative AI Application",
142
+ learning_outcome: "Understand LLMs and build a RAG application",
143
+ student_level: "BEGINNER",
144
+ teaching_focus: "PRACTICE_ORIENTED",
145
+ course_length: 4,
146
  },
147
  };
148
 
 
151
  // resp.data.proposed_draft.syllabus -> 用于替换未锁定 syllabus 的新大纲
152
  ```
153
 
154
+ #### 3.3 生成单个模块的教学流程 `apiAiFlowGenerate`(snake_case)
155
 
156
  ```ts
157
  import {
 
161
  } from "@/lib/api";
162
 
163
  const req: AiFlowGenerateReq = {
164
+ request_id: "req_cw_102",
165
+ module_context: {
166
  title: "Introduction to Generative AI",
167
+ learning_objectives: ["Understand basic LLM concepts"],
168
  topics: ["History of AI", "Transformer Architecture"],
169
+ duration_minutes: 90,
170
  },
171
+ system_prompts: ["(可选) 来自 Improve 阶段的长期提示"],
172
  };
173
 
174
  const resp: AiFlowGenerateResp = await apiAiFlowGenerate(req);
175
+ // resp.data.steps[i].estimated_duration -> 单步预计时长(分钟)
176
+ // resp.data.steps[i].ai_understanding -> AI 对该步骤教学意图的摘要
177
+ // resp.data.steps[i].teacher_activity -> 教师在该步骤的具体行为/讲解要点
178
+ // resp.data.steps[i].student_activity -> 学生在该步骤预期进行的活动
179
+ // resp.data.steps[i].guiding_questions -> 建议的引导提问(Markdown 文本)
180
+ // resp.data.steps[i].required_materials -> 所���材料/设备清单
181
  ```
182
 
183
+ #### 3.4 局部重构教学流程 `apiAiFlowRegeneratePartial`(snake_case)
184
 
185
  ```ts
186
  import {
 
190
  } from "@/lib/api";
191
 
192
  const req: AiFlowPartialReq = {
193
+ request_id: "req_cw_103",
194
  prompt: "Split the 30-min explanation into two shorter parts with a quiz in between.",
195
+ current_flow: {
196
+ locked_steps: [{ id: "step_1", title: "Welcome", duration: 15 }],
197
+ unlocked_steps: [{ id: "step_2", title: "Heavy Explanation", duration: 30 }],
198
  },
199
  };
200
 
201
  const resp: AiFlowPartialResp = await apiAiFlowRegeneratePartial(req);
202
  // resp.data.explanation -> Copilot 气泡文案
203
+ // resp.data.proposed_steps -> 用于替换 unlocked_steps 的新步骤
204
  ```
205
 
206
+ #### 3.5 教案长文 `apiAiPlanDetailGenerate`(snake_case)
207
 
208
  ```ts
209
  import {
 
213
  } from "@/lib/api";
214
 
215
  const req: AiPlanDetailReq = {
216
+ request_id: "req_cw_104",
217
+ finalized_steps: stepsArray, // 来自 Flow 阶段确定的完整 steps
218
  };
219
 
220
  const resp: AiPlanDetailResp = await apiAiPlanDetailGenerate(req);
221
+ // resp.data.course_name -> 课程名可选
222
+ // resp.data.sections -> 多个 section(含 section_id/type/title/content),content 支持 Markdown
223
  ```
224
 
225
+ #### 3.6 反思看板 `apiAiReflectionGenerate`(snake_case + 新结构)
226
 
227
  ```ts
228
  import {
 
232
  } from "@/lib/api";
233
 
234
  const req: AiReflectionReq = {
235
+ request_id: "req_cw_105",
236
+ lesson_steps: [
237
+ { step_number: 1, title: "Intro", type: "LECTURE", duration: 15 },
238
+ ],
239
+ teach_annotations: [
240
  {
241
+ dimension: "TIME",
242
+ level: "LIMITED",
243
+ selected_text: "Gradient Descent",
244
  feedback: "Students were confused by the math",
245
  },
246
  ],
247
+ quiz_aggregations: {
248
+ average_score: 72,
249
+ lowest_topic: "Matrix Operations",
250
  },
251
  };
252
 
253
  const resp: AiReflectionResp = await apiAiReflectionGenerate(req);
254
+ // resp.data.overall_status
255
+ // resp.data.upper_section.fields.understanding / engagement / difficulty / misconceptions
256
+ // resp.data.lower_section.strengths / next_lesson_suggestions
257
  ```
258
 
259
+ #### 3.7 改进提案 `apiAiImprovementGenerate`(snake_case)
260
 
261
  ```ts
262
  import {
 
266
  } from "@/lib/api";
267
 
268
  const req: AiImprovementReq = {
269
+ request_id: "req_cw_106",
270
+ reflection_reports: previousReflectionReports, // 来自 2.6 的历史数据数组
271
  };
272
 
273
  const resp: AiImprovementResp = await apiAiImprovementGenerate(req);
web/src/lib/api.ts CHANGED
@@ -594,33 +594,33 @@ export type AiMeta = {
594
 
595
  // 2.1 Generate Syllabus Preview
596
  export type AiSyllabusContext = {
597
- courseName: string;
598
- learningOutcome: string;
599
- studentLevel: string;
600
- teachingFocus: string;
601
- courseLength: number;
602
  };
603
 
604
  export type AiSyllabusGenerateReq = {
605
- requestId: string;
606
  context: AiSyllabusContext;
607
- systemPrompts?: string[] | null;
608
  };
609
 
610
  export type AiWeekSyllabus = {
611
- weekNumber: number;
612
  title: string;
613
- learningObjectives: string[];
614
  topics: string[];
615
  };
616
 
617
  export type AiSyllabusGenerateResp = {
618
  data: {
619
- courseOverview?: string | null;
620
  assessment?: string | null;
621
- teachingApproach?: string | null;
622
- workloadExpectation?: string | null;
623
- academicIntegrityPolicy?: string | null;
624
  syllabus: AiWeekSyllabus[];
625
  };
626
  meta: AiMeta;
@@ -646,11 +646,12 @@ export async function apiAiSyllabusGenerate(
646
 
647
  // 2.2 Regenerate Partial Syllabus
648
  export type AiSyllabusLockedSections = {
649
- courseOverview?: string | null;
650
  assessment?: string | null;
651
- teachingApproach?: string | null;
652
- workloadExpectation?: string | null;
653
- academicIntegrityPolicy?: string | null;
 
654
  };
655
 
656
  export type AiSyllabusUnlockedSections = {
@@ -658,14 +659,14 @@ export type AiSyllabusUnlockedSections = {
658
  };
659
 
660
  export type AiCurrentSyllabus = {
661
- lockedSections: AiSyllabusLockedSections;
662
- unlockedSections: AiSyllabusUnlockedSections;
663
  };
664
 
665
  export type AiSyllabusPartialReq = {
666
- requestId: string;
667
  prompt: string;
668
- currentSyllabus: AiCurrentSyllabus;
669
  context: AiSyllabusContext;
670
  };
671
 
@@ -700,26 +701,26 @@ export async function apiAiSyllabusRegeneratePartial(
700
  // 2.3 Generate Lesson Flow
701
  export type AiModuleContext = {
702
  title: string;
703
- learningObjectives: string[];
704
  topics: string[];
705
- durationMinutes: number;
706
  };
707
 
708
  export type AiFlowGenerateReq = {
709
- requestId: string;
710
- moduleContext: AiModuleContext;
711
- systemPrompts?: string[] | null;
712
  };
713
 
714
  export type AiLessonStep = {
715
  type: string;
716
  title: string;
717
- estimatedDuration: number;
718
- aiUnderstanding: string;
719
- teacherActivity?: string | null;
720
- studentActivity?: string | null;
721
- guidingQuestions?: string | null;
722
- requiredMaterials?: string | null;
723
  };
724
 
725
  export type AiFlowGenerateResp = {
@@ -753,31 +754,31 @@ export type AiSimpleStep = {
753
  };
754
 
755
  export type AiCurrentFlow = {
756
- lockedSteps: AiSimpleStep[];
757
- unlockedSteps: AiSimpleStep[];
758
  };
759
 
760
  export type AiFlowPartialReq = {
761
- requestId: string;
762
  prompt: string;
763
- currentFlow: AiCurrentFlow;
764
  };
765
 
766
  export type AiCopilotProposedStep = {
767
  type: string;
768
  title: string;
769
- estimatedDuration: number;
770
- aiUnderstanding: string;
771
- teacherActivity?: string | null;
772
- studentActivity?: string | null;
773
- guidingQuestions?: string | null;
774
- requiredMaterials?: string | null;
775
  };
776
 
777
  export type AiFlowPartialResp = {
778
  data: {
779
  explanation: string;
780
- proposedSteps: AiCopilotProposedStep[];
781
  };
782
  meta: AiMeta;
783
  };
@@ -802,18 +803,19 @@ export async function apiAiFlowRegeneratePartial(
802
 
803
  // 2.4 Generate/Polish Lesson Plan Detail
804
  export type AiPlanDetailReq = {
805
- requestId: string;
806
- finalizedSteps: any[];
807
  };
808
 
809
  export type AiLessonSection = {
810
  section_id: string;
811
  type: string;
 
812
  content: string;
813
  };
814
 
815
  export type AiPlanDetailResp = {
816
- data: { sections: AiLessonSection[] };
817
  meta: AiMeta;
818
  };
819
 
@@ -839,18 +841,18 @@ export async function apiAiPlanDetailGenerate(
839
  export type AiTeachAnnotation = {
840
  dimension: string;
841
  level: string;
842
- selectedText?: string | null;
843
  feedback?: string | null;
844
  };
845
 
846
  export type AiQuizAggregations = {
847
- averageScore?: number | null;
848
- lowestTopic?: string | null;
849
  };
850
 
851
  export type AiReflectionReq = {
852
- requestId: string;
853
- lessonSteps?: {
854
  step_number: number;
855
  title: string;
856
  type: string;
@@ -859,43 +861,52 @@ export type AiReflectionReq = {
859
  student_activity?: string | null;
860
  guiding_questions?: string | null;
861
  }[];
862
- teachAnnotations: AiTeachAnnotation[];
863
- quizAggregations?: AiQuizAggregations | null;
864
  };
865
 
866
- export type AiReflectionUnderstanding = {
867
- status: string;
868
- summary: string;
869
- bulletPoints?: string[] | null;
 
870
  };
871
 
872
- export type AiReflectionEngagement = {
873
  status: string;
874
- summary?: string | null;
 
875
  };
876
 
877
- export type AiReflectionDifficulty = {
878
- status: string;
879
- challengingTopics?: any[] | null;
 
 
 
 
 
 
880
  };
881
 
882
- export type AiReflectionMisconception = {
883
- status: string;
884
- issues?: any[] | null;
885
  };
886
 
887
- export type AiNextLessonSuggestion = {
888
- actionText: string;
889
- deepLinkType?: string | null;
 
 
 
890
  };
891
 
892
  export type AiReflectionResp = {
893
  data: {
894
- understanding: AiReflectionUnderstanding;
895
- engagement: AiReflectionEngagement;
896
- difficulty: AiReflectionDifficulty;
897
- misconceptions: AiReflectionMisconception;
898
- nextLessonSuggestions: AiNextLessonSuggestion[];
899
  };
900
  meta: AiMeta;
901
  };
@@ -920,18 +931,18 @@ export async function apiAiReflectionGenerate(
920
 
921
  // 2.6 Generate Improvement Proposals
922
  export type AiImprovementReq = {
923
- requestId: string;
924
- reflectionReports: any[];
925
  };
926
 
927
  export type AiImprovementProposal = {
928
  title: string;
929
  priority: string;
930
- affectedWeeks?: string | null;
931
  evidence?: string | null;
932
- rootCause?: string | null;
933
- proposedSolution?: string | null;
934
- expectedImpact?: string | null;
935
  };
936
 
937
  export type AiImprovementResp = {
 
594
 
595
  // 2.1 Generate Syllabus Preview
596
  export type AiSyllabusContext = {
597
+ course_name: string;
598
+ learning_outcome: string;
599
+ student_level: string;
600
+ teaching_focus: string;
601
+ course_length: number;
602
  };
603
 
604
  export type AiSyllabusGenerateReq = {
605
+ request_id: string;
606
  context: AiSyllabusContext;
607
+ system_prompts?: string[] | null;
608
  };
609
 
610
  export type AiWeekSyllabus = {
611
+ week_number: number;
612
  title: string;
613
+ learning_objectives: string[];
614
  topics: string[];
615
  };
616
 
617
  export type AiSyllabusGenerateResp = {
618
  data: {
619
+ course_overview?: string | null;
620
  assessment?: string | null;
621
+ teaching_approach?: string | null;
622
+ workload_expectation?: string | null;
623
+ academic_integrity_policy?: string | null;
624
  syllabus: AiWeekSyllabus[];
625
  };
626
  meta: AiMeta;
 
646
 
647
  // 2.2 Regenerate Partial Syllabus
648
  export type AiSyllabusLockedSections = {
649
+ course_overview?: string | null;
650
  assessment?: string | null;
651
+ teaching_approach?: string | null;
652
+ workload_expectation?: string | null;
653
+ academic_integrity_policy?: string | null;
654
+ syllabus?: Partial<AiWeekSyllabus>[] | null;
655
  };
656
 
657
  export type AiSyllabusUnlockedSections = {
 
659
  };
660
 
661
  export type AiCurrentSyllabus = {
662
+ locked_sections: AiSyllabusLockedSections;
663
+ unlocked_sections: AiSyllabusUnlockedSections;
664
  };
665
 
666
  export type AiSyllabusPartialReq = {
667
+ request_id: string;
668
  prompt: string;
669
+ current_syllabus: AiCurrentSyllabus;
670
  context: AiSyllabusContext;
671
  };
672
 
 
701
  // 2.3 Generate Lesson Flow
702
  export type AiModuleContext = {
703
  title: string;
704
+ learning_objectives: string[];
705
  topics: string[];
706
+ duration_minutes: number;
707
  };
708
 
709
  export type AiFlowGenerateReq = {
710
+ request_id: string;
711
+ module_context: AiModuleContext;
712
+ system_prompts?: string[] | null;
713
  };
714
 
715
  export type AiLessonStep = {
716
  type: string;
717
  title: string;
718
+ estimated_duration: number;
719
+ ai_understanding: string;
720
+ teacher_activity?: string | null;
721
+ student_activity?: string | null;
722
+ guiding_questions?: string | null;
723
+ required_materials?: string | null;
724
  };
725
 
726
  export type AiFlowGenerateResp = {
 
754
  };
755
 
756
  export type AiCurrentFlow = {
757
+ locked_steps: AiSimpleStep[];
758
+ unlocked_steps: AiSimpleStep[];
759
  };
760
 
761
  export type AiFlowPartialReq = {
762
+ request_id: string;
763
  prompt: string;
764
+ current_flow: AiCurrentFlow;
765
  };
766
 
767
  export type AiCopilotProposedStep = {
768
  type: string;
769
  title: string;
770
+ estimated_duration: number;
771
+ ai_understanding: string;
772
+ teacher_activity?: string | null;
773
+ student_activity?: string | null;
774
+ guiding_questions?: string | null;
775
+ required_materials?: string | null;
776
  };
777
 
778
  export type AiFlowPartialResp = {
779
  data: {
780
  explanation: string;
781
+ proposed_steps: AiCopilotProposedStep[];
782
  };
783
  meta: AiMeta;
784
  };
 
803
 
804
  // 2.4 Generate/Polish Lesson Plan Detail
805
  export type AiPlanDetailReq = {
806
+ request_id: string;
807
+ finalized_steps: any[];
808
  };
809
 
810
  export type AiLessonSection = {
811
  section_id: string;
812
  type: string;
813
+ title: string;
814
  content: string;
815
  };
816
 
817
  export type AiPlanDetailResp = {
818
+ data: { course_name?: string | null; sections: AiLessonSection[] };
819
  meta: AiMeta;
820
  };
821
 
 
841
  export type AiTeachAnnotation = {
842
  dimension: string;
843
  level: string;
844
+ selected_text?: string | null;
845
  feedback?: string | null;
846
  };
847
 
848
  export type AiQuizAggregations = {
849
+ average_score?: number | null;
850
+ lowest_topic?: string | null;
851
  };
852
 
853
  export type AiReflectionReq = {
854
+ request_id: string;
855
+ lesson_steps?: {
856
  step_number: number;
857
  title: string;
858
  type: string;
 
861
  student_activity?: string | null;
862
  guiding_questions?: string | null;
863
  }[];
864
+ teach_annotations: AiTeachAnnotation[];
865
+ quiz_aggregations?: AiQuizAggregations | null;
866
  };
867
 
868
+ export type AiReflectionDetails = {
869
+ description?: string | null;
870
+ bullet_points?: string[] | null;
871
+ challenging_topics?: string[] | null;
872
+ issues?: string[] | null;
873
  };
874
 
875
+ export type AiReflectionField = {
876
  status: string;
877
+ summary: string;
878
+ details: AiReflectionDetails;
879
  };
880
 
881
+ export type AiUpperSection = {
882
+ description?: string | null;
883
+ required: boolean;
884
+ fields: {
885
+ understanding: AiReflectionField;
886
+ engagement: AiReflectionField;
887
+ difficulty: AiReflectionField;
888
+ misconceptions: AiReflectionField;
889
+ };
890
  };
891
 
892
+ export type AiNextLessonSuggestion = {
893
+ text: string;
894
+ deep_link_type?: string | null;
895
  };
896
 
897
+ export type AiLowerSection = {
898
+ summary: string;
899
+ description?: string | null;
900
+ overall_summary?: string | null;
901
+ strengths: string[];
902
+ next_lesson_suggestions: AiNextLessonSuggestion[];
903
  };
904
 
905
  export type AiReflectionResp = {
906
  data: {
907
+ overall_status: string;
908
+ upper_section: AiUpperSection;
909
+ lower_section: AiLowerSection;
 
 
910
  };
911
  meta: AiMeta;
912
  };
 
931
 
932
  // 2.6 Generate Improvement Proposals
933
  export type AiImprovementReq = {
934
+ request_id: string;
935
+ reflection_reports: any[];
936
  };
937
 
938
  export type AiImprovementProposal = {
939
  title: string;
940
  priority: string;
941
+ affected_weeks?: string | null;
942
  evidence?: string | null;
943
+ root_cause?: string | null;
944
+ proposed_solution?: string | null;
945
+ expected_impact?: string | null;
946
  };
947
 
948
  export type AiImprovementResp = {