claudqunwang Cursor commited on
Commit
6a91b07
·
1 Parent(s): e8f612b

添加持续对话功能:支持每个功能模块与AI进行多轮对话和互动

Browse files

Co-authored-by: Cursor <cursoragent@cursor.com>

api/courseware/activity_designer.py CHANGED
@@ -2,7 +2,7 @@
2
  """
3
  Activity & Assignment Designer:课堂活动、作业及评分标准(Rubric);从上传资料提取核心知识点。
4
  """
5
- from typing import Optional
6
 
7
  from api.config import client, DEFAULT_MODEL, USE_WEAVIATE
8
  from api.courseware.rag import get_rag_context_with_refs, inject_refs_instruction
@@ -15,6 +15,7 @@ def design_activities_and_assignments(
15
  learning_objectives: Optional[str] = None,
16
  rag_context_override: Optional[str] = None,
17
  max_tokens: int = 2200,
 
18
  ) -> str:
19
  """
20
  设计课堂活动、作业及 Rubric;支持从上传资料提取核心知识点并与目标一致。
@@ -30,13 +31,19 @@ def design_activities_and_assignments(
30
  rag_context=rag_context or "(无检索到知识库摘录,将基于通用教学设计回答。)",
31
  ref_instruction=ref_instruction,
32
  )
 
 
 
 
 
 
 
 
 
33
  try:
34
  resp = client.chat.completions.create(
35
  model=DEFAULT_MODEL,
36
- messages=[
37
- {"role": "system", "content": ACTIVITY_DESIGNER_SYSTEM},
38
- {"role": "user", "content": user_content},
39
- ],
40
  temperature=0.5,
41
  max_tokens=max_tokens,
42
  timeout=90,
 
2
  """
3
  Activity & Assignment Designer:课堂活动、作业及评分标准(Rubric);从上传资料提取核心知识点。
4
  """
5
+ from typing import Optional, List, Tuple
6
 
7
  from api.config import client, DEFAULT_MODEL, USE_WEAVIATE
8
  from api.courseware.rag import get_rag_context_with_refs, inject_refs_instruction
 
15
  learning_objectives: Optional[str] = None,
16
  rag_context_override: Optional[str] = None,
17
  max_tokens: int = 2200,
18
+ history: Optional[list] = None,
19
  ) -> str:
20
  """
21
  设计课堂活动、作业及 Rubric;支持从上传资料提取核心知识点并与目标一致。
 
31
  rag_context=rag_context or "(无检索到知识库摘录,将基于通用教学设计回答。)",
32
  ref_instruction=ref_instruction,
33
  )
34
+ messages = [{"role": "system", "content": ACTIVITY_DESIGNER_SYSTEM}]
35
+ if history:
36
+ for user_msg, assistant_msg in history[-10:]:
37
+ if user_msg:
38
+ messages.append({"role": "user", "content": user_msg})
39
+ if assistant_msg:
40
+ messages.append({"role": "assistant", "content": assistant_msg})
41
+ messages.append({"role": "user", "content": user_content})
42
+
43
  try:
44
  resp = client.chat.completions.create(
45
  model=DEFAULT_MODEL,
46
+ messages=messages,
 
 
 
47
  temperature=0.5,
48
  max_tokens=max_tokens,
49
  timeout=90,
api/courseware/content_generator.py CHANGED
@@ -2,7 +2,7 @@
2
  """
3
  Content Generator:生成 Markdown 详细教案,并导出可用于生成 PPT 的结构化数据。
4
  """
5
- from typing import Optional
6
 
7
  from api.config import client, DEFAULT_MODEL
8
  from api.courseware.rag import get_rag_context_with_refs, inject_refs_instruction
@@ -15,6 +15,7 @@ def generate_lesson_plan_and_ppt_data(
15
  duration: Optional[str] = None,
16
  outline_points: Optional[str] = None,
17
  max_tokens: int = 2800,
 
18
  ) -> str:
19
  """
20
  生成 Markdown 详细教案 + 可用于 PPT 的结构化数据(每页 title、bullets、speaker_notes)。
@@ -29,13 +30,19 @@ def generate_lesson_plan_and_ppt_data(
29
  rag_context=rag_context or "(无检索到知识库摘录。)",
30
  ref_instruction=ref_instruction,
31
  )
 
 
 
 
 
 
 
 
 
32
  try:
33
  resp = client.chat.completions.create(
34
  model=DEFAULT_MODEL,
35
- messages=[
36
- {"role": "system", "content": CONTENT_GENERATOR_SYSTEM},
37
- {"role": "user", "content": user_content},
38
- ],
39
  temperature=0.4,
40
  max_tokens=max_tokens,
41
  timeout=120,
 
2
  """
3
  Content Generator:生成 Markdown 详细教案,并导出可用于生成 PPT 的结构化数据。
4
  """
5
+ from typing import Optional, List, Tuple
6
 
7
  from api.config import client, DEFAULT_MODEL
8
  from api.courseware.rag import get_rag_context_with_refs, inject_refs_instruction
 
15
  duration: Optional[str] = None,
16
  outline_points: Optional[str] = None,
17
  max_tokens: int = 2800,
18
+ history: Optional[list] = None,
19
  ) -> str:
20
  """
21
  生成 Markdown 详细教案 + 可用于 PPT 的结构化数据(每页 title、bullets、speaker_notes)。
 
30
  rag_context=rag_context or "(无检索到知识库摘录。)",
31
  ref_instruction=ref_instruction,
32
  )
33
+ messages = [{"role": "system", "content": CONTENT_GENERATOR_SYSTEM}]
34
+ if history:
35
+ for user_msg, assistant_msg in history[-10:]:
36
+ if user_msg:
37
+ messages.append({"role": "user", "content": user_msg})
38
+ if assistant_msg:
39
+ messages.append({"role": "assistant", "content": assistant_msg})
40
+ messages.append({"role": "user", "content": user_content})
41
+
42
  try:
43
  resp = client.chat.completions.create(
44
  model=DEFAULT_MODEL,
45
+ messages=messages,
 
 
 
46
  temperature=0.4,
47
  max_tokens=max_tokens,
48
  timeout=120,
api/courseware/qa_optimizer.py CHANGED
@@ -2,7 +2,7 @@
2
  """
3
  Course QA Optimizer:基于学生答题数据(Smart Quiz)分析弱点,自动优化后续教学建议。
4
  """
5
- from typing import Optional
6
 
7
  from api.config import client, DEFAULT_MODEL
8
  from api.courseware.rag import get_rag_context_with_refs, inject_refs_instruction
@@ -14,6 +14,7 @@ def optimize_from_quiz_data(
14
  quiz_summary: str,
15
  course_topic: Optional[str] = None,
16
  max_tokens: int = 1500,
 
17
  ) -> str:
18
  """
19
  基于 Smart Quiz 答题数据摘要,分析薄弱点并给出后续教学优化建议。
@@ -27,13 +28,19 @@ def optimize_from_quiz_data(
27
  rag_context=rag_context or "(无检索到知识库摘录。)",
28
  ref_instruction=ref_instruction,
29
  )
 
 
 
 
 
 
 
 
 
30
  try:
31
  resp = client.chat.completions.create(
32
  model=DEFAULT_MODEL,
33
- messages=[
34
- {"role": "system", "content": QA_OPTIMIZER_SYSTEM},
35
- {"role": "user", "content": user_content},
36
- ],
37
  temperature=0.4,
38
  max_tokens=max_tokens,
39
  timeout=90,
 
2
  """
3
  Course QA Optimizer:基于学生答题数据(Smart Quiz)分析弱点,自动优化后续教学建议。
4
  """
5
+ from typing import Optional, List, Tuple
6
 
7
  from api.config import client, DEFAULT_MODEL
8
  from api.courseware.rag import get_rag_context_with_refs, inject_refs_instruction
 
14
  quiz_summary: str,
15
  course_topic: Optional[str] = None,
16
  max_tokens: int = 1500,
17
+ history: Optional[list] = None,
18
  ) -> str:
19
  """
20
  基于 Smart Quiz 答题数据摘要,分析薄弱点并给出后续教学优化建议。
 
28
  rag_context=rag_context or "(无检索到知识库摘录。)",
29
  ref_instruction=ref_instruction,
30
  )
31
+ messages = [{"role": "system", "content": QA_OPTIMIZER_SYSTEM}]
32
+ if history:
33
+ for user_msg, assistant_msg in history[-10:]:
34
+ if user_msg:
35
+ messages.append({"role": "user", "content": user_msg})
36
+ if assistant_msg:
37
+ messages.append({"role": "assistant", "content": assistant_msg})
38
+ messages.append({"role": "user", "content": user_content})
39
+
40
  try:
41
  resp = client.chat.completions.create(
42
  model=DEFAULT_MODEL,
43
+ messages=messages,
 
 
 
44
  temperature=0.4,
45
  max_tokens=max_tokens,
46
  timeout=90,
api/courseware/teaching_copilot.py CHANGED
@@ -2,7 +2,7 @@
2
  """
3
  Teaching Copilot & Student Adaptation:课堂实时辅助,按学生画像(Name, Progress, Behavior)动态调整建议。
4
  """
5
- from typing import Optional, List, Dict, Any
6
 
7
  from api.config import client, DEFAULT_MODEL
8
  from api.courseware.rag import get_rag_context_with_refs, inject_refs_instruction
@@ -27,6 +27,7 @@ def teaching_copilot(
27
  current_content: str,
28
  student_profiles: Optional[List[Dict[str, Any]]] = None,
29
  max_tokens: int = 1200,
 
30
  ) -> str:
31
  """
32
  课堂实时辅助:根据当前授课内容与学生画像给出即时建议与个性化调整。
@@ -41,13 +42,19 @@ def teaching_copilot(
41
  rag_context=rag_context or "(无检索到知识库摘录。)",
42
  ref_instruction=ref_instruction,
43
  )
 
 
 
 
 
 
 
 
 
44
  try:
45
  resp = client.chat.completions.create(
46
  model=DEFAULT_MODEL,
47
- messages=[
48
- {"role": "system", "content": TEACHING_COPILOT_SYSTEM},
49
- {"role": "user", "content": user_content},
50
- ],
51
  temperature=0.5,
52
  max_tokens=max_tokens,
53
  timeout=60,
 
2
  """
3
  Teaching Copilot & Student Adaptation:课堂实时辅助,按学生画像(Name, Progress, Behavior)动态调整建议。
4
  """
5
+ from typing import Optional, List, Dict, Any, Tuple
6
 
7
  from api.config import client, DEFAULT_MODEL
8
  from api.courseware.rag import get_rag_context_with_refs, inject_refs_instruction
 
27
  current_content: str,
28
  student_profiles: Optional[List[Dict[str, Any]]] = None,
29
  max_tokens: int = 1200,
30
+ history: Optional[list] = None,
31
  ) -> str:
32
  """
33
  课堂实时辅助:根据当前授课内容与学生画像给出即时建议与个性化调整。
 
42
  rag_context=rag_context or "(无检索到知识库摘录。)",
43
  ref_instruction=ref_instruction,
44
  )
45
+ messages = [{"role": "system", "content": TEACHING_COPILOT_SYSTEM}]
46
+ if history:
47
+ for user_msg, assistant_msg in history[-10:]:
48
+ if user_msg:
49
+ messages.append({"role": "user", "content": user_msg})
50
+ if assistant_msg:
51
+ messages.append({"role": "assistant", "content": assistant_msg})
52
+ messages.append({"role": "user", "content": user_content})
53
+
54
  try:
55
  resp = client.chat.completions.create(
56
  model=DEFAULT_MODEL,
57
+ messages=messages,
 
 
 
58
  temperature=0.5,
59
  max_tokens=max_tokens,
60
  timeout=60,
api/courseware/vision_builder.py CHANGED
@@ -2,7 +2,7 @@
2
  """
3
  Course Vision & Structure Builder:课程定位、学习目标、层级化知识树。
4
  """
5
- from typing import Optional
6
 
7
  from api.config import client, DEFAULT_MODEL, USE_WEAVIATE
8
  from api.courseware.rag import get_rag_context_with_refs, inject_refs_instruction
@@ -14,6 +14,7 @@ def build_course_vision(
14
  course_info: str,
15
  syllabus: str,
16
  max_tokens: int = 2000,
 
17
  ) -> str:
18
  """
19
  输入:课程基本信息 + 教学大纲。
@@ -28,13 +29,19 @@ def build_course_vision(
28
  rag_context=rag_context or "(无检索到知识库摘录,将基于通用课程设计经验回答。)",
29
  ref_instruction=ref_instruction,
30
  )
 
 
 
 
 
 
 
 
 
31
  try:
32
  resp = client.chat.completions.create(
33
  model=DEFAULT_MODEL,
34
- messages=[
35
- {"role": "system", "content": COURSE_VISION_SYSTEM},
36
- {"role": "user", "content": user_content},
37
- ],
38
  temperature=0.4,
39
  max_tokens=max_tokens,
40
  timeout=90,
 
2
  """
3
  Course Vision & Structure Builder:课程定位、学习目标、层级化知识树。
4
  """
5
+ from typing import Optional, List, Tuple
6
 
7
  from api.config import client, DEFAULT_MODEL, USE_WEAVIATE
8
  from api.courseware.rag import get_rag_context_with_refs, inject_refs_instruction
 
14
  course_info: str,
15
  syllabus: str,
16
  max_tokens: int = 2000,
17
+ history: Optional[list] = None,
18
  ) -> str:
19
  """
20
  输入:课程基本信息 + 教学大纲。
 
29
  rag_context=rag_context or "(无检索到知识库摘录,将基于通用课程设计经验回答。)",
30
  ref_instruction=ref_instruction,
31
  )
32
+ messages = [{"role": "system", "content": COURSE_VISION_SYSTEM}]
33
+ if history:
34
+ for user_msg, assistant_msg in history[-10:]:
35
+ if user_msg:
36
+ messages.append({"role": "user", "content": user_msg})
37
+ if assistant_msg:
38
+ messages.append({"role": "assistant", "content": assistant_msg})
39
+ messages.append({"role": "user", "content": user_content})
40
+
41
  try:
42
  resp = client.chat.completions.create(
43
  model=DEFAULT_MODEL,
44
+ messages=messages,
 
 
 
45
  temperature=0.4,
46
  max_tokens=max_tokens,
47
  timeout=90,
api/routes_courseware.py CHANGED
@@ -21,6 +21,7 @@ router = APIRouter(prefix="/api/courseware", tags=["courseware"])
21
  class VisionRequest(BaseModel):
22
  course_info: str = Field(..., min_length=1, description="课程基本信息")
23
  syllabus: str = Field(..., min_length=1, description="教学大纲或要点")
 
24
 
25
 
26
  class VisionResponse(BaseModel):
@@ -36,6 +37,7 @@ def post_vision(req: VisionRequest):
36
  content = build_course_vision(
37
  course_info=req.course_info.strip(),
38
  syllabus=req.syllabus.strip(),
 
39
  )
40
  return VisionResponse(content=content, weaviate_used=USE_WEAVIATE)
41
  except Exception as e:
@@ -47,6 +49,7 @@ class ActivitiesRequest(BaseModel):
47
  topic: str = Field(..., min_length=1, description="主题/模块")
48
  learning_objectives: Optional[str] = Field(None, description="学习目标(可选)")
49
  rag_context_override: Optional[str] = Field(None, description="覆盖 RAG 上下文(如上传资料摘要)")
 
50
 
51
 
52
  class ActivitiesResponse(BaseModel):
@@ -63,6 +66,7 @@ def post_activities(req: ActivitiesRequest):
63
  topic=req.topic.strip(),
64
  learning_objectives=req.learning_objectives.strip() if req.learning_objectives else None,
65
  rag_context_override=req.rag_context_override.strip() if req.rag_context_override else None,
 
66
  )
67
  return ActivitiesResponse(content=content, weaviate_used=USE_WEAVIATE)
68
  except Exception as e:
@@ -79,6 +83,7 @@ class StudentProfile(BaseModel):
79
  class CopilotRequest(BaseModel):
80
  current_content: str = Field(..., min_length=1, description="当前授课内容/问题")
81
  student_profiles: Optional[List[StudentProfile]] = Field(None, description="学生画像 Name, Progress, Behavior")
 
82
 
83
 
84
  class CopilotResponse(BaseModel):
@@ -95,6 +100,7 @@ def post_copilot(req: CopilotRequest):
95
  content = teaching_copilot(
96
  current_content=req.current_content.strip(),
97
  student_profiles=profiles,
 
98
  )
99
  return CopilotResponse(content=content, weaviate_used=USE_WEAVIATE)
100
  except Exception as e:
@@ -105,6 +111,7 @@ def post_copilot(req: CopilotRequest):
105
  class QAOptimizeRequest(BaseModel):
106
  quiz_summary: str = Field(..., min_length=1, description="学生答题数据摘要(Smart Quiz)")
107
  course_topic: Optional[str] = Field(None, description="相关课程主题/章节")
 
108
 
109
 
110
  class QAOptimizeResponse(BaseModel):
@@ -120,6 +127,7 @@ def post_qa_optimize(req: QAOptimizeRequest):
120
  content = optimize_from_quiz_data(
121
  quiz_summary=req.quiz_summary.strip(),
122
  course_topic=req.course_topic.strip() if req.course_topic else None,
 
123
  )
124
  return QAOptimizeResponse(content=content, weaviate_used=USE_WEAVIATE)
125
  except Exception as e:
@@ -131,6 +139,7 @@ class ContentRequest(BaseModel):
131
  topic: str = Field(..., min_length=1, description="主题/章节")
132
  duration: Optional[str] = Field(None, description="课时/时长建议")
133
  outline_points: Optional[str] = Field(None, description="大纲要点")
 
134
 
135
 
136
  class ContentResponse(BaseModel):
@@ -147,6 +156,7 @@ def post_content(req: ContentRequest):
147
  topic=req.topic.strip(),
148
  duration=req.duration.strip() if req.duration else None,
149
  outline_points=req.outline_points.strip() if req.outline_points else None,
 
150
  )
151
  return ContentResponse(content=content, weaviate_used=USE_WEAVIATE)
152
  except Exception as e:
 
21
  class VisionRequest(BaseModel):
22
  course_info: str = Field(..., min_length=1, description="课程基本信息")
23
  syllabus: str = Field(..., min_length=1, description="教学大纲或要点")
24
+ history: Optional[list] = Field(None, description="对话历史:[(user_msg, assistant_msg), ...]")
25
 
26
 
27
  class VisionResponse(BaseModel):
 
37
  content = build_course_vision(
38
  course_info=req.course_info.strip(),
39
  syllabus=req.syllabus.strip(),
40
+ history=req.history,
41
  )
42
  return VisionResponse(content=content, weaviate_used=USE_WEAVIATE)
43
  except Exception as e:
 
49
  topic: str = Field(..., min_length=1, description="主题/模块")
50
  learning_objectives: Optional[str] = Field(None, description="学习目标(可选)")
51
  rag_context_override: Optional[str] = Field(None, description="覆盖 RAG 上下文(如上传资料摘要)")
52
+ history: Optional[list] = Field(None, description="对话历史:[(user_msg, assistant_msg), ...]")
53
 
54
 
55
  class ActivitiesResponse(BaseModel):
 
66
  topic=req.topic.strip(),
67
  learning_objectives=req.learning_objectives.strip() if req.learning_objectives else None,
68
  rag_context_override=req.rag_context_override.strip() if req.rag_context_override else None,
69
+ history=req.history,
70
  )
71
  return ActivitiesResponse(content=content, weaviate_used=USE_WEAVIATE)
72
  except Exception as e:
 
83
  class CopilotRequest(BaseModel):
84
  current_content: str = Field(..., min_length=1, description="当前授课内容/问题")
85
  student_profiles: Optional[List[StudentProfile]] = Field(None, description="学生画像 Name, Progress, Behavior")
86
+ history: Optional[list] = Field(None, description="对话历史:[(user_msg, assistant_msg), ...]")
87
 
88
 
89
  class CopilotResponse(BaseModel):
 
100
  content = teaching_copilot(
101
  current_content=req.current_content.strip(),
102
  student_profiles=profiles,
103
+ history=req.history,
104
  )
105
  return CopilotResponse(content=content, weaviate_used=USE_WEAVIATE)
106
  except Exception as e:
 
111
  class QAOptimizeRequest(BaseModel):
112
  quiz_summary: str = Field(..., min_length=1, description="学生答题数据摘要(Smart Quiz)")
113
  course_topic: Optional[str] = Field(None, description="相关课程主题/章节")
114
+ history: Optional[list] = Field(None, description="对话历史:[(user_msg, assistant_msg), ...]")
115
 
116
 
117
  class QAOptimizeResponse(BaseModel):
 
127
  content = optimize_from_quiz_data(
128
  quiz_summary=req.quiz_summary.strip(),
129
  course_topic=req.course_topic.strip() if req.course_topic else None,
130
+ history=req.history,
131
  )
132
  return QAOptimizeResponse(content=content, weaviate_used=USE_WEAVIATE)
133
  except Exception as e:
 
139
  topic: str = Field(..., min_length=1, description="主题/章节")
140
  duration: Optional[str] = Field(None, description="课时/时长建议")
141
  outline_points: Optional[str] = Field(None, description="大纲要点")
142
+ history: Optional[list] = Field(None, description="对话历史:[(user_msg, assistant_msg), ...]")
143
 
144
 
145
  class ContentResponse(BaseModel):
 
156
  topic=req.topic.strip(),
157
  duration=req.duration.strip() if req.duration else None,
158
  outline_points=req.outline_points.strip() if req.outline_points else None,
159
+ history=req.history,
160
  )
161
  return ContentResponse(content=content, weaviate_used=USE_WEAVIATE)
162
  except Exception as e:
api/routes_teacher.py CHANGED
@@ -18,6 +18,8 @@ class CourseDescriptionRequest(BaseModel):
18
  topic: str = Field(..., min_length=1, description="课程主题/名称")
19
  outline_hint: str | None = Field(None, description="可选:大纲或要点")
20
  reply_language: str | None = Field(None, description="回复语言:zh=中文, en=English")
 
 
21
 
22
 
23
  class CourseDescriptionResponse(BaseModel):
@@ -34,6 +36,8 @@ def post_course_description(req: CourseDescriptionRequest):
34
  topic=req.topic.strip(),
35
  outline_hint=req.outline_hint.strip() if req.outline_hint else None,
36
  reply_language=req.reply_language.strip() if req.reply_language else None,
 
 
37
  )
38
  return CourseDescriptionResponse(
39
  description=desc,
@@ -48,6 +52,7 @@ class DocSuggestionRequest(BaseModel):
48
  current_doc_excerpt: str | None = Field(None, description="当前已有内容片段")
49
  doc_type: str = Field("讲义/课件", description="文档类型")
50
  reply_language: str | None = Field(None, description="回复语言:zh=中文, en=English")
 
51
 
52
 
53
  class DocSuggestionResponse(BaseModel):
@@ -65,6 +70,7 @@ def post_doc_suggestion(req: DocSuggestionRequest):
65
  current_doc_excerpt=req.current_doc_excerpt.strip() if req.current_doc_excerpt else None,
66
  doc_type=req.doc_type.strip() or "讲义/课件",
67
  reply_language=req.reply_language.strip() if req.reply_language else None,
 
68
  )
69
  return DocSuggestionResponse(
70
  suggestion=suggestion,
@@ -79,6 +85,7 @@ class AssignmentQuestionsRequest(BaseModel):
79
  week_or_module: str | None = Field(None, description="周次/模块")
80
  question_type: str = Field("混合", description="题型:选择题、简答题、开放题、混合")
81
  reply_language: str | None = Field(None, description="回复语言:zh=中文, en=English")
 
82
 
83
 
84
  class AssignmentQuestionsResponse(BaseModel):
@@ -96,6 +103,7 @@ def post_assignment_questions(req: AssignmentQuestionsRequest):
96
  week_or_module=req.week_or_module.strip() if req.week_or_module else None,
97
  question_type=req.question_type.strip() or "混合",
98
  reply_language=req.reply_language.strip() if req.reply_language else None,
 
99
  )
100
  return AssignmentQuestionsResponse(
101
  suggestion=suggestion,
@@ -109,6 +117,7 @@ class AssessmentAnalysisRequest(BaseModel):
109
  assessment_summary: str = Field(..., min_length=1, description="学生表现/评估摘要")
110
  course_topic_hint: str | None = Field(None, description="可选:相关课程主题")
111
  reply_language: str | None = Field(None, description="回复语言:zh=中文, en=English")
 
112
 
113
 
114
  class AssessmentAnalysisResponse(BaseModel):
@@ -125,6 +134,7 @@ def post_assessment_analysis(req: AssessmentAnalysisRequest):
125
  assessment_summary=req.assessment_summary.strip(),
126
  course_topic_hint=req.course_topic_hint.strip() if req.course_topic_hint else None,
127
  reply_language=req.reply_language.strip() if req.reply_language else None,
 
128
  )
129
  return AssessmentAnalysisResponse(
130
  analysis=analysis,
 
18
  topic: str = Field(..., min_length=1, description="课程主题/名称")
19
  outline_hint: str | None = Field(None, description="可选:大纲或要点")
20
  reply_language: str | None = Field(None, description="回复语言:zh=中文, en=English")
21
+ history: list | None = Field(None, description="对话历史:[(user_msg, assistant_msg), ...]")
22
+ userMessage: str | None = Field(None, description="持续对话时的用户消息(可选)")
23
 
24
 
25
  class CourseDescriptionResponse(BaseModel):
 
36
  topic=req.topic.strip(),
37
  outline_hint=req.outline_hint.strip() if req.outline_hint else None,
38
  reply_language=req.reply_language.strip() if req.reply_language else None,
39
+ history=req.history,
40
+ user_message=req.userMessage.strip() if req.userMessage else None,
41
  )
42
  return CourseDescriptionResponse(
43
  description=desc,
 
52
  current_doc_excerpt: str | None = Field(None, description="当前已有内容片段")
53
  doc_type: str = Field("讲义/课件", description="文档类型")
54
  reply_language: str | None = Field(None, description="回复语言:zh=中文, en=English")
55
+ history: list | None = Field(None, description="对话历史:[(user_msg, assistant_msg), ...]")
56
 
57
 
58
  class DocSuggestionResponse(BaseModel):
 
70
  current_doc_excerpt=req.current_doc_excerpt.strip() if req.current_doc_excerpt else None,
71
  doc_type=req.doc_type.strip() or "讲义/课件",
72
  reply_language=req.reply_language.strip() if req.reply_language else None,
73
+ history=req.history,
74
  )
75
  return DocSuggestionResponse(
76
  suggestion=suggestion,
 
85
  week_or_module: str | None = Field(None, description="周次/模块")
86
  question_type: str = Field("混合", description="题型:选择题、简答题、开放题、混合")
87
  reply_language: str | None = Field(None, description="回复语言:zh=中文, en=English")
88
+ history: list | None = Field(None, description="对话历史:[(user_msg, assistant_msg), ...]")
89
 
90
 
91
  class AssignmentQuestionsResponse(BaseModel):
 
103
  week_or_module=req.week_or_module.strip() if req.week_or_module else None,
104
  question_type=req.question_type.strip() or "混合",
105
  reply_language=req.reply_language.strip() if req.reply_language else None,
106
+ history=req.history,
107
  )
108
  return AssignmentQuestionsResponse(
109
  suggestion=suggestion,
 
117
  assessment_summary: str = Field(..., min_length=1, description="学生表现/评估摘要")
118
  course_topic_hint: str | None = Field(None, description="可选:相关课程主题")
119
  reply_language: str | None = Field(None, description="回复语言:zh=中文, en=English")
120
+ history: list | None = Field(None, description="对话历史:[(user_msg, assistant_msg), ...]")
121
 
122
 
123
  class AssessmentAnalysisResponse(BaseModel):
 
134
  assessment_summary=req.assessment_summary.strip(),
135
  course_topic_hint=req.course_topic_hint.strip() if req.course_topic_hint else None,
136
  reply_language=req.reply_language.strip() if req.reply_language else None,
137
+ history=req.history,
138
  )
139
  return AssessmentAnalysisResponse(
140
  analysis=analysis,
api/teacher_agent.py CHANGED
@@ -24,20 +24,39 @@ def _lang_instruction(reply_language: Optional[str]) -> str:
24
  return ""
25
 
26
 
27
- def _call_llm(user_content: str, system_extra: str = "", reply_language: Optional[str] = None, max_tokens: int = 1500) -> str:
 
 
 
 
 
 
 
 
 
28
  system = TEACHER_SYSTEM
29
  lang = _lang_instruction(reply_language)
30
  if lang:
31
  system = system + "\n\n" + lang
32
  if system_extra:
33
  system = system + "\n\n" + system_extra
 
 
 
 
 
 
 
 
 
 
 
 
 
34
  try:
35
  resp = client.chat.completions.create(
36
  model=DEFAULT_MODEL,
37
- messages=[
38
- {"role": "system", "content": system},
39
- {"role": "user", "content": user_content},
40
- ],
41
  temperature=0.6,
42
  max_tokens=max_tokens,
43
  timeout=60,
@@ -52,20 +71,32 @@ def generate_course_description(
52
  topic: str,
53
  outline_hint: Optional[str] = None,
54
  reply_language: Optional[str] = None,
 
 
55
  ) -> str:
56
  """
57
  课程描述生成:根据课程主题(及可选大纲要点)生成一段可用于课程介绍/选课页的描述。
58
  reply_language: "en" | "zh" | None(默认中文)
 
59
  """
60
  rag = retrieve_from_weaviate(topic, top_k=6) if USE_WEAVIATE else ""
61
- user = f"请根据以下信息,生成一段简洁的**课程描述**(约 150–250 字),适合放在课程介绍或选课页面。\n\n"
62
- user += f"**课程主题/名称:** {topic}\n\n"
63
- if outline_hint:
64
- user += f"**大纲或要点(可选):**\n{outline_hint}\n\n"
65
- if rag:
66
- user += "**参考知识库摘录(请据此保持与现有课程内容一致):**\n" + rag[:4000] + "\n\n"
67
- user += "请直接输出课程描述正文,无需重复题目。"
68
- return _call_llm(user, reply_language=reply_language, max_tokens=800)
 
 
 
 
 
 
 
 
 
69
 
70
 
71
  def suggest_course_doc_content(
@@ -73,6 +104,7 @@ def suggest_course_doc_content(
73
  current_doc_excerpt: Optional[str] = None,
74
  doc_type: str = "讲义/课件",
75
  reply_language: Optional[str] = None,
 
76
  ) -> str:
77
  """
78
  课程文档内容建议:针对某一主题或现有文档片段,给出可写入讲义/课件的内容建议。
@@ -85,7 +117,7 @@ def suggest_course_doc_content(
85
  if rag:
86
  user += "**参考知识库摘录:**\n" + rag[:5000] + "\n\n"
87
  user += "请用分点或短段落给出建议,便于教师直接采纳或改写。"
88
- return _call_llm(user, reply_language=reply_language, max_tokens=1200)
89
 
90
 
91
  def suggest_assignments_questions(
@@ -93,6 +125,7 @@ def suggest_assignments_questions(
93
  week_or_module: Optional[str] = None,
94
  question_type: str = "混合",
95
  reply_language: Optional[str] = None,
 
96
  ) -> str:
97
  """
98
  作业和题库生成建议:根据主题(及可选周次/模块)给出作业题、练习题或考试题建议。
@@ -107,13 +140,14 @@ def suggest_assignments_questions(
107
  if rag:
108
  user += "**参考知识库摘录:**\n" + rag[:5000] + "\n\n"
109
  user += "请直接给出建议与示例题,便于教师录入题库或布置作业。"
110
- return _call_llm(user, reply_language=reply_language, max_tokens=1500)
111
 
112
 
113
  def analyze_student_assessment(
114
  assessment_summary: str,
115
  course_topic_hint: Optional[str] = None,
116
  reply_language: Optional[str] = None,
 
117
  ) -> str:
118
  """
119
  学生学习评估分析:根据教师提供的学生表现摘要(如作业/测验得分、常见错误、参与度等),
@@ -129,4 +163,4 @@ def analyze_student_assessment(
129
  if rag:
130
  user += "**参考知识库摘录(可选,用于对齐课程目标):**\n" + rag[:3000] + "\n\n"
131
  user += "控制在 300–500 字。"
132
- return _call_llm(user, reply_language=reply_language, max_tokens=800)
 
24
  return ""
25
 
26
 
27
+ def _call_llm(
28
+ user_content: str,
29
+ system_extra: str = "",
30
+ reply_language: Optional[str] = None,
31
+ max_tokens: int = 1500,
32
+ history: Optional[list] = None,
33
+ ) -> str:
34
+ """
35
+ history: List of tuples [(user_msg, assistant_msg), ...] for conversation context
36
+ """
37
  system = TEACHER_SYSTEM
38
  lang = _lang_instruction(reply_language)
39
  if lang:
40
  system = system + "\n\n" + lang
41
  if system_extra:
42
  system = system + "\n\n" + system_extra
43
+
44
+ messages = [{"role": "system", "content": system}]
45
+
46
+ # Add conversation history if provided
47
+ if history:
48
+ for user_msg, assistant_msg in history[-10:]: # Keep last 10 turns
49
+ if user_msg:
50
+ messages.append({"role": "user", "content": user_msg})
51
+ if assistant_msg:
52
+ messages.append({"role": "assistant", "content": assistant_msg})
53
+
54
+ messages.append({"role": "user", "content": user_content})
55
+
56
  try:
57
  resp = client.chat.completions.create(
58
  model=DEFAULT_MODEL,
59
+ messages=messages,
 
 
 
60
  temperature=0.6,
61
  max_tokens=max_tokens,
62
  timeout=60,
 
71
  topic: str,
72
  outline_hint: Optional[str] = None,
73
  reply_language: Optional[str] = None,
74
+ history: Optional[list] = None,
75
+ user_message: Optional[str] = None,
76
  ) -> str:
77
  """
78
  课程描述生成:根据课程主题(及可选大纲要点)生成一段可用于课程介绍/选课页的描述。
79
  reply_language: "en" | "zh" | None(默认中文)
80
+ user_message: 持续对话时的用户消息(可选)
81
  """
82
  rag = retrieve_from_weaviate(topic, top_k=6) if USE_WEAVIATE else ""
83
+
84
+ if user_message:
85
+ # 持续对话模式:直接使用用户消息
86
+ user = user_message
87
+ if rag:
88
+ user = f"**参考知识库摘录:**\n{rag[:4000]}\n\n---\n\n{user}"
89
+ else:
90
+ # 初始提交模式:使用表单字段
91
+ user = f"请根据以下信息,生成一段简洁的**课程描述**(约 150–250 字),适合放在课程介绍或选课页面。\n\n"
92
+ user += f"**课程主题/名称:** {topic}\n\n"
93
+ if outline_hint:
94
+ user += f"**大纲或要点(可选):**\n{outline_hint}\n\n"
95
+ if rag:
96
+ user += "**参考知识库摘录(请据此保持与现有课程内容一致):**\n" + rag[:4000] + "\n\n"
97
+ user += "请直接输出课程描述正文,无需重复题目。"
98
+
99
+ return _call_llm(user, reply_language=reply_language, max_tokens=800, history=history)
100
 
101
 
102
  def suggest_course_doc_content(
 
104
  current_doc_excerpt: Optional[str] = None,
105
  doc_type: str = "讲义/课件",
106
  reply_language: Optional[str] = None,
107
+ history: Optional[list] = None,
108
  ) -> str:
109
  """
110
  课程文档内容建议:针对某一主题或现有文档片段,给出可写入讲义/课件的内容建议。
 
117
  if rag:
118
  user += "**参考知识库摘录:**\n" + rag[:5000] + "\n\n"
119
  user += "请用分点或短段落给出建议,便于教师直接采纳或改写。"
120
+ return _call_llm(user, reply_language=reply_language, max_tokens=1200, history=history)
121
 
122
 
123
  def suggest_assignments_questions(
 
125
  week_or_module: Optional[str] = None,
126
  question_type: str = "混合",
127
  reply_language: Optional[str] = None,
128
+ history: Optional[list] = None,
129
  ) -> str:
130
  """
131
  作业和题库生成建议:根据主题(及可选周次/模块)给出作业题、练习题或考试题建议。
 
140
  if rag:
141
  user += "**参考知识库摘录:**\n" + rag[:5000] + "\n\n"
142
  user += "请直接给出建议与示例题,便于教师录入题库或布置作业。"
143
+ return _call_llm(user, reply_language=reply_language, max_tokens=1500, history=history)
144
 
145
 
146
  def analyze_student_assessment(
147
  assessment_summary: str,
148
  course_topic_hint: Optional[str] = None,
149
  reply_language: Optional[str] = None,
150
+ history: Optional[list] = None,
151
  ) -> str:
152
  """
153
  学生学习评估分析:根据教师提供的学生表现摘要(如作业/测验得分、常见错误、参与度等),
 
163
  if rag:
164
  user += "**参考知识库摘录(可选,用于对齐课程目标):**\n" + rag[:3000] + "\n\n"
165
  user += "控制在 300–500 字。"
166
+ return _call_llm(user, reply_language=reply_language, max_tokens=800, history=history)
web/src/components/TeacherChatPage.tsx CHANGED
@@ -34,7 +34,7 @@ interface TeacherChatPageProps {
34
  onBack: () => void;
35
  userId: string;
36
  replyLanguage?: "zh" | "en" | "auto";
37
- onSubmit: (inputs: Record<string, string>, files: File[]) => Promise<string>;
38
  }
39
 
40
  const DOC_TYPE_MAP: Record<FileType, string> = {
@@ -60,8 +60,11 @@ export function TeacherChatPage({
60
  const [pendingFiles, setPendingFiles] = useState<File[]>([]);
61
  const [isSubmitting, setIsSubmitting] = useState(false);
62
  const [showFileTypeDialog, setShowFileTypeDialog] = useState(false);
 
 
63
  const fileInputRef = useRef<HTMLInputElement>(null);
64
  const messagesEndRef = useRef<HTMLDivElement>(null);
 
65
 
66
  useEffect(() => {
67
  messagesEndRef.current?.scrollIntoView({ behavior: "smooth" });
@@ -119,45 +122,75 @@ export function TeacherChatPage({
119
  const handleSubmit = async () => {
120
  if (isSubmitting) return;
121
 
122
- // Validate required inputs
123
- const requiredFields: Record<FeatureId, string[]> = {
124
- "course-description": ["topic"],
125
- "doc-suggestion": ["topic"],
126
- "assignment-questions": ["topic"],
127
- "assessment-analysis": ["summary"],
128
- vision: ["courseInfo", "syllabus"],
129
- activities: ["topic"],
130
- copilot: ["content"],
131
- "qa-optimize": ["summary"],
132
- content: ["topic"],
133
- };
 
134
 
135
- const required = requiredFields[featureId] || [];
136
- const missing = required.filter((field) => !inputValues[field]?.trim());
137
 
138
- if (missing.length > 0) {
139
- toast.error(`Please fill in required fields: ${missing.join(", ")}`);
140
- return;
 
141
  }
142
 
143
  setIsSubmitting(true);
144
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
145
  // Add user message
146
  const userMessage: Message = {
147
  id: Date.now().toString(),
148
  role: "user",
149
- content: Object.entries(inputValues)
150
- .filter(([_, v]) => v?.trim())
151
- .map(([k, v]) => `${k}: ${v}`)
152
- .join("\n"),
153
  timestamp: new Date(),
154
- files: uploadedFiles.map((uf) => ({ name: uf.file.name, type: uf.file.type })),
155
  };
156
 
157
  setMessages((prev) => [...prev, userMessage]);
158
 
 
 
 
 
 
 
 
 
159
  try {
160
- const result = await onSubmit(inputValues, uploadedFiles.map((uf) => uf.file));
 
 
 
 
 
 
161
 
162
  // Add assistant message
163
  const assistantMessage: Message = {
@@ -168,6 +201,8 @@ export function TeacherChatPage({
168
  };
169
 
170
  setMessages((prev) => [...prev, assistantMessage]);
 
 
171
  } catch (e: any) {
172
  toast.error(e?.message || "Generation failed");
173
  } finally {
@@ -175,6 +210,11 @@ export function TeacherChatPage({
175
  }
176
  };
177
 
 
 
 
 
 
178
  const getInputFields = () => {
179
  switch (featureId) {
180
  case "course-description":
@@ -510,58 +550,89 @@ export function TeacherChatPage({
510
  </div>
511
 
512
  {/* Right Panel: Chat Messages */}
513
- <div className="flex-1 overflow-auto p-4">
514
- {messages.length === 0 ? (
515
- <div className="h-full flex items-center justify-center text-muted-foreground">
516
- <div className="text-center">
517
- <p className="text-lg mb-2">Start a conversation</p>
518
- <p className="text-sm">Fill in the form and click Generate to begin</p>
 
 
519
  </div>
520
- </div>
521
- ) : (
522
- <div className="space-y-4 max-w-3xl mx-auto">
523
- {messages.map((msg) => (
524
- <div
525
- key={msg.id}
526
- className={`flex gap-3 ${msg.role === "user" ? "justify-end" : "justify-start"}`}
527
- >
528
- {msg.role === "assistant" && (
529
- <div className="w-8 h-8 rounded-full bg-primary/10 flex items-center justify-center flex-shrink-0">
530
- <FileText className="h-4 w-4 text-primary" />
531
- </div>
532
- )}
533
  <div
534
- className={`rounded-lg p-4 max-w-[80%] ${
535
- msg.role === "user"
536
- ? "bg-primary text-primary-foreground"
537
- : "bg-muted text-foreground"
538
- }`}
539
  >
540
- {msg.files && msg.files.length > 0 && (
541
- <div className="mb-2 pb-2 border-b border-border/50">
542
- <p className="text-xs opacity-80 mb-1">Attached files:</p>
543
- <div className="flex flex-wrap gap-1">
544
- {msg.files.map((f, idx) => (
545
- <span key={idx} className="text-xs bg-background/20 px-2 py-0.5 rounded">
546
- {f.name}
547
- </span>
548
- ))}
 
 
 
 
 
 
 
 
 
 
 
 
 
549
  </div>
 
 
 
 
 
 
 
 
 
550
  </div>
551
  )}
552
- <p className="whitespace-pre-wrap">{msg.content}</p>
553
- <p className="text-xs opacity-70 mt-2">
554
- {msg.timestamp.toLocaleTimeString()}
555
- </p>
556
  </div>
557
- {msg.role === "user" && (
558
- <div className="w-8 h-8 rounded-full bg-primary flex items-center justify-center flex-shrink-0">
559
- <span className="text-xs text-primary-foreground">U</span>
560
- </div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
561
  )}
562
- </div>
563
- ))}
564
- <div ref={messagesEndRef} />
565
  </div>
566
  )}
567
  </div>
 
34
  onBack: () => void;
35
  userId: string;
36
  replyLanguage?: "zh" | "en" | "auto";
37
+ onSubmit: (inputs: Record<string, string>, files: File[], history?: Array<[string, string]>) => Promise<string>;
38
  }
39
 
40
  const DOC_TYPE_MAP: Record<FileType, string> = {
 
60
  const [pendingFiles, setPendingFiles] = useState<File[]>([]);
61
  const [isSubmitting, setIsSubmitting] = useState(false);
62
  const [showFileTypeDialog, setShowFileTypeDialog] = useState(false);
63
+ const [chatInput, setChatInput] = useState("");
64
+ const [isInitialSubmit, setIsInitialSubmit] = useState(true);
65
  const fileInputRef = useRef<HTMLInputElement>(null);
66
  const messagesEndRef = useRef<HTMLDivElement>(null);
67
+ const chatInputRef = useRef<HTMLTextAreaElement>(null);
68
 
69
  useEffect(() => {
70
  messagesEndRef.current?.scrollIntoView({ behavior: "smooth" });
 
122
  const handleSubmit = async () => {
123
  if (isSubmitting) return;
124
 
125
+ // Validate required inputs for initial submit
126
+ if (isInitialSubmit) {
127
+ const requiredFields: Record<FeatureId, string[]> = {
128
+ "course-description": ["topic"],
129
+ "doc-suggestion": ["topic"],
130
+ "assignment-questions": ["topic"],
131
+ "assessment-analysis": ["summary"],
132
+ vision: ["courseInfo", "syllabus"],
133
+ activities: ["topic"],
134
+ copilot: ["content"],
135
+ "qa-optimize": ["summary"],
136
+ content: ["topic"],
137
+ };
138
 
139
+ const required = requiredFields[featureId] || [];
140
+ const missing = required.filter((field) => !inputValues[field]?.trim());
141
 
142
+ if (missing.length > 0) {
143
+ toast.error(`Please fill in required fields: ${missing.join(", ")}`);
144
+ return;
145
+ }
146
  }
147
 
148
  setIsSubmitting(true);
149
 
150
+ // Build user message content
151
+ let userContent: string;
152
+ if (isInitialSubmit) {
153
+ // Initial submit: use form inputs
154
+ userContent = Object.entries(inputValues)
155
+ .filter(([_, v]) => v?.trim())
156
+ .map(([k, v]) => `${k}: ${v}`)
157
+ .join("\n");
158
+ } else {
159
+ // Continuous chat: use chat input
160
+ if (!chatInput.trim()) {
161
+ setIsSubmitting(false);
162
+ return;
163
+ }
164
+ userContent = chatInput.trim();
165
+ }
166
+
167
  // Add user message
168
  const userMessage: Message = {
169
  id: Date.now().toString(),
170
  role: "user",
171
+ content: userContent,
 
 
 
172
  timestamp: new Date(),
173
+ files: isInitialSubmit ? uploadedFiles.map((uf) => ({ name: uf.file.name, type: uf.file.type })) : undefined,
174
  };
175
 
176
  setMessages((prev) => [...prev, userMessage]);
177
 
178
+ // Build conversation history (excluding the current message we're about to add)
179
+ const history: Array<[string, string]> = [];
180
+ for (let i = 0; i < messages.length; i++) {
181
+ if (messages[i].role === "user" && i + 1 < messages.length && messages[i + 1].role === "assistant") {
182
+ history.push([messages[i].content, messages[i + 1].content]);
183
+ }
184
+ }
185
+
186
  try {
187
+ // For continuous chat, use chat input as the main message
188
+ // but keep form inputs for context
189
+ const inputsForApi = isInitialSubmit
190
+ ? inputValues
191
+ : { ...inputValues, userMessage: chatInput.trim() };
192
+
193
+ const result = await onSubmit(inputsForApi, uploadedFiles.map((uf) => uf.file), history);
194
 
195
  // Add assistant message
196
  const assistantMessage: Message = {
 
201
  };
202
 
203
  setMessages((prev) => [...prev, assistantMessage]);
204
+ setIsInitialSubmit(false);
205
+ setChatInput("");
206
  } catch (e: any) {
207
  toast.error(e?.message || "Generation failed");
208
  } finally {
 
210
  }
211
  };
212
 
213
+ const handleChatSubmit = async (e: React.FormEvent) => {
214
+ e.preventDefault();
215
+ await handleSubmit();
216
+ };
217
+
218
  const getInputFields = () => {
219
  switch (featureId) {
220
  case "course-description":
 
550
  </div>
551
 
552
  {/* Right Panel: Chat Messages */}
553
+ <div className="flex-1 flex flex-col overflow-hidden">
554
+ <div className="flex-1 overflow-auto p-4">
555
+ {messages.length === 0 ? (
556
+ <div className="h-full flex items-center justify-center text-muted-foreground">
557
+ <div className="text-center">
558
+ <p className="text-lg mb-2">Start a conversation</p>
559
+ <p className="text-sm">Fill in the form and click Generate to begin</p>
560
+ </div>
561
  </div>
562
+ ) : (
563
+ <div className="space-y-4 max-w-3xl mx-auto">
564
+ {messages.map((msg) => (
 
 
 
 
 
 
 
 
 
 
565
  <div
566
+ key={msg.id}
567
+ className={`flex gap-3 ${msg.role === "user" ? "justify-end" : "justify-start"}`}
 
 
 
568
  >
569
+ {msg.role === "assistant" && (
570
+ <div className="w-8 h-8 rounded-full bg-primary/10 flex items-center justify-center flex-shrink-0">
571
+ <FileText className="h-4 w-4 text-primary" />
572
+ </div>
573
+ )}
574
+ <div
575
+ className={`rounded-lg p-4 max-w-[80%] ${
576
+ msg.role === "user"
577
+ ? "bg-primary text-primary-foreground"
578
+ : "bg-muted text-foreground"
579
+ }`}
580
+ >
581
+ {msg.files && msg.files.length > 0 && (
582
+ <div className="mb-2 pb-2 border-b border-border/50">
583
+ <p className="text-xs opacity-80 mb-1">Attached files:</p>
584
+ <div className="flex flex-wrap gap-1">
585
+ {msg.files.map((f, idx) => (
586
+ <span key={idx} className="text-xs bg-background/20 px-2 py-0.5 rounded">
587
+ {f.name}
588
+ </span>
589
+ ))}
590
+ </div>
591
  </div>
592
+ )}
593
+ <p className="whitespace-pre-wrap">{msg.content}</p>
594
+ <p className="text-xs opacity-70 mt-2">
595
+ {msg.timestamp.toLocaleTimeString()}
596
+ </p>
597
+ </div>
598
+ {msg.role === "user" && (
599
+ <div className="w-8 h-8 rounded-full bg-primary flex items-center justify-center flex-shrink-0">
600
+ <span className="text-xs text-primary-foreground">U</span>
601
  </div>
602
  )}
 
 
 
 
603
  </div>
604
+ ))}
605
+ <div ref={messagesEndRef} />
606
+ </div>
607
+ )}
608
+ </div>
609
+
610
+ {/* Chat Input */}
611
+ {messages.length > 0 && (
612
+ <div className="flex-shrink-0 border-t border-border p-4">
613
+ <form onSubmit={handleChatSubmit} className="flex gap-2 max-w-3xl mx-auto">
614
+ <Textarea
615
+ ref={chatInputRef}
616
+ value={chatInput}
617
+ onChange={(e) => setChatInput(e.target.value)}
618
+ placeholder="Continue the conversation..."
619
+ className="flex-1 min-h-[60px] max-h-[200px] resize-none"
620
+ disabled={isSubmitting}
621
+ onKeyDown={(e) => {
622
+ if (e.key === "Enter" && !e.shiftKey) {
623
+ e.preventDefault();
624
+ handleChatSubmit(e);
625
+ }
626
+ }}
627
+ />
628
+ <Button type="submit" disabled={isSubmitting || !chatInput.trim()} size="lg">
629
+ {isSubmitting ? (
630
+ <Loader2 className="h-4 w-4 animate-spin" />
631
+ ) : (
632
+ <Send className="h-4 w-4" />
633
  )}
634
+ </Button>
635
+ </form>
 
636
  </div>
637
  )}
638
  </div>
web/src/components/TeacherDashboard.tsx CHANGED
@@ -128,13 +128,20 @@ export function TeacherDashboard({ user, onBack }: Props) {
128
 
129
  // Handler functions for chat page
130
  const getSubmitHandler = (featureId: FeatureId) => {
131
- return async (inputs: Record<string, string>, files: File[]): Promise<string> => {
 
 
 
 
 
132
  switch (featureId) {
133
  case "course-description": {
134
  const res = await apiTeacherCourseDescription({
135
  topic: inputs.topic || "",
136
  outline_hint: inputs.outline || undefined,
137
  reply_language: replyLangParam,
 
 
138
  });
139
  if (res.weaviate_used) toast.success(getText(uiLanguage, "knowledgeBaseUsed"));
140
  return res.description;
@@ -145,6 +152,7 @@ export function TeacherDashboard({ user, onBack }: Props) {
145
  current_doc_excerpt: inputs.excerpt || undefined,
146
  doc_type: inputs.docType || "Lecture Notes / Slides",
147
  reply_language: replyLangParam,
 
148
  });
149
  if (res.weaviate_used) toast.success(getText(uiLanguage, "knowledgeBaseUsed"));
150
  return res.suggestion;
@@ -155,6 +163,7 @@ export function TeacherDashboard({ user, onBack }: Props) {
155
  week_or_module: inputs.week || undefined,
156
  question_type: inputs.questionType || "Mixed",
157
  reply_language: replyLangParam,
 
158
  });
159
  if (res.weaviate_used) toast.success(getText(uiLanguage, "knowledgeBaseUsed"));
160
  return res.suggestion;
@@ -164,6 +173,7 @@ export function TeacherDashboard({ user, onBack }: Props) {
164
  assessment_summary: inputs.summary || "",
165
  course_topic_hint: inputs.topic || undefined,
166
  reply_language: replyLangParam,
 
167
  });
168
  if (res.weaviate_used) toast.success(getText(uiLanguage, "knowledgeBaseUsed"));
169
  return res.analysis;
@@ -172,6 +182,7 @@ export function TeacherDashboard({ user, onBack }: Props) {
172
  const res = await apiCoursewareVision({
173
  course_info: inputs.courseInfo || "",
174
  syllabus: inputs.syllabus || "",
 
175
  });
176
  if (res.weaviate_used) toast.success(getText(uiLanguage, "knowledgeBaseUsed"));
177
  return res.content;
@@ -180,6 +191,7 @@ export function TeacherDashboard({ user, onBack }: Props) {
180
  const res = await apiCoursewareActivities({
181
  topic: inputs.topic || "",
182
  learning_objectives: inputs.objectives || undefined,
 
183
  });
184
  if (res.weaviate_used) toast.success(getText(uiLanguage, "knowledgeBaseUsed"));
185
  return res.content;
@@ -197,6 +209,7 @@ export function TeacherDashboard({ user, onBack }: Props) {
197
  const res = await apiCoursewareCopilot({
198
  current_content: inputs.content || "",
199
  student_profiles: profiles,
 
200
  });
201
  if (res.weaviate_used) toast.success(getText(uiLanguage, "knowledgeBaseUsed"));
202
  return res.content;
@@ -205,6 +218,7 @@ export function TeacherDashboard({ user, onBack }: Props) {
205
  const res = await apiCoursewareQAOptimize({
206
  quiz_summary: inputs.summary || "",
207
  course_topic: inputs.topic || undefined,
 
208
  });
209
  if (res.weaviate_used) toast.success(getText(uiLanguage, "knowledgeBaseUsed"));
210
  return res.content;
@@ -214,6 +228,7 @@ export function TeacherDashboard({ user, onBack }: Props) {
214
  topic: inputs.topic || "",
215
  duration: inputs.duration || undefined,
216
  outline_points: inputs.outline || undefined,
 
217
  });
218
  if (res.weaviate_used) toast.success(getText(uiLanguage, "knowledgeBaseUsed"));
219
  return res.content;
 
128
 
129
  // Handler functions for chat page
130
  const getSubmitHandler = (featureId: FeatureId) => {
131
+ return async (
132
+ inputs: Record<string, string>,
133
+ files: File[],
134
+ history?: Array<[string, string]>
135
+ ): Promise<string> => {
136
+ const historyParam = history && history.length > 0 ? history : null;
137
  switch (featureId) {
138
  case "course-description": {
139
  const res = await apiTeacherCourseDescription({
140
  topic: inputs.topic || "",
141
  outline_hint: inputs.outline || undefined,
142
  reply_language: replyLangParam,
143
+ history: historyParam,
144
+ userMessage: inputs.userMessage || undefined,
145
  });
146
  if (res.weaviate_used) toast.success(getText(uiLanguage, "knowledgeBaseUsed"));
147
  return res.description;
 
152
  current_doc_excerpt: inputs.excerpt || undefined,
153
  doc_type: inputs.docType || "Lecture Notes / Slides",
154
  reply_language: replyLangParam,
155
+ history: historyParam,
156
  });
157
  if (res.weaviate_used) toast.success(getText(uiLanguage, "knowledgeBaseUsed"));
158
  return res.suggestion;
 
163
  week_or_module: inputs.week || undefined,
164
  question_type: inputs.questionType || "Mixed",
165
  reply_language: replyLangParam,
166
+ history: historyParam,
167
  });
168
  if (res.weaviate_used) toast.success(getText(uiLanguage, "knowledgeBaseUsed"));
169
  return res.suggestion;
 
173
  assessment_summary: inputs.summary || "",
174
  course_topic_hint: inputs.topic || undefined,
175
  reply_language: replyLangParam,
176
+ history: historyParam,
177
  });
178
  if (res.weaviate_used) toast.success(getText(uiLanguage, "knowledgeBaseUsed"));
179
  return res.analysis;
 
182
  const res = await apiCoursewareVision({
183
  course_info: inputs.courseInfo || "",
184
  syllabus: inputs.syllabus || "",
185
+ history: historyParam,
186
  });
187
  if (res.weaviate_used) toast.success(getText(uiLanguage, "knowledgeBaseUsed"));
188
  return res.content;
 
191
  const res = await apiCoursewareActivities({
192
  topic: inputs.topic || "",
193
  learning_objectives: inputs.objectives || undefined,
194
+ history: historyParam,
195
  });
196
  if (res.weaviate_used) toast.success(getText(uiLanguage, "knowledgeBaseUsed"));
197
  return res.content;
 
209
  const res = await apiCoursewareCopilot({
210
  current_content: inputs.content || "",
211
  student_profiles: profiles,
212
+ history: historyParam,
213
  });
214
  if (res.weaviate_used) toast.success(getText(uiLanguage, "knowledgeBaseUsed"));
215
  return res.content;
 
218
  const res = await apiCoursewareQAOptimize({
219
  quiz_summary: inputs.summary || "",
220
  course_topic: inputs.topic || undefined,
221
+ history: historyParam,
222
  });
223
  if (res.weaviate_used) toast.success(getText(uiLanguage, "knowledgeBaseUsed"));
224
  return res.content;
 
228
  topic: inputs.topic || "",
229
  duration: inputs.duration || undefined,
230
  outline_points: inputs.outline || undefined,
231
+ history: historyParam,
232
  });
233
  if (res.weaviate_used) toast.success(getText(uiLanguage, "knowledgeBaseUsed"));
234
  return res.content;
web/src/lib/api.ts CHANGED
@@ -415,6 +415,8 @@ export async function apiTeacherCourseDescription(payload: {
415
  topic: string;
416
  outline_hint?: string | null;
417
  reply_language?: string | null;
 
 
418
  }): Promise<{ description: string; weaviate_used: boolean }> {
419
  const base = getBaseUrl();
420
  const res = await fetchWithTimeout(
@@ -436,6 +438,7 @@ export async function apiTeacherDocSuggestion(payload: {
436
  current_doc_excerpt?: string | null;
437
  doc_type?: string;
438
  reply_language?: string | null;
 
439
  }): Promise<{ suggestion: string; weaviate_used: boolean }> {
440
  const base = getBaseUrl();
441
  const res = await fetchWithTimeout(
@@ -457,6 +460,7 @@ export async function apiTeacherAssignmentQuestions(payload: {
457
  week_or_module?: string | null;
458
  question_type?: string;
459
  reply_language?: string | null;
 
460
  }): Promise<{ suggestion: string; weaviate_used: boolean }> {
461
  const base = getBaseUrl();
462
  const res = await fetchWithTimeout(
@@ -477,6 +481,7 @@ export async function apiTeacherAssessmentAnalysis(payload: {
477
  assessment_summary: string;
478
  course_topic_hint?: string | null;
479
  reply_language?: string | null;
 
480
  }): Promise<{ analysis: string; weaviate_used: boolean }> {
481
  const base = getBaseUrl();
482
  const res = await fetchWithTimeout(
@@ -497,6 +502,7 @@ export async function apiTeacherAssessmentAnalysis(payload: {
497
  export async function apiCoursewareVision(payload: {
498
  course_info: string;
499
  syllabus: string;
 
500
  }): Promise<{ content: string; weaviate_used: boolean }> {
501
  const base = getBaseUrl();
502
  const res = await fetchWithTimeout(
@@ -513,6 +519,7 @@ export async function apiCoursewareActivities(payload: {
513
  topic: string;
514
  learning_objectives?: string | null;
515
  rag_context_override?: string | null;
 
516
  }): Promise<{ content: string; weaviate_used: boolean }> {
517
  const base = getBaseUrl();
518
  const res = await fetchWithTimeout(
@@ -528,6 +535,7 @@ export async function apiCoursewareActivities(payload: {
528
  export async function apiCoursewareCopilot(payload: {
529
  current_content: string;
530
  student_profiles?: Array<{ name?: string; progress?: string; behavior?: string }> | null;
 
531
  }): Promise<{ content: string; weaviate_used: boolean }> {
532
  const base = getBaseUrl();
533
  const res = await fetchWithTimeout(
@@ -543,6 +551,7 @@ export async function apiCoursewareCopilot(payload: {
543
  export async function apiCoursewareQAOptimize(payload: {
544
  quiz_summary: string;
545
  course_topic?: string | null;
 
546
  }): Promise<{ content: string; weaviate_used: boolean }> {
547
  const base = getBaseUrl();
548
  const res = await fetchWithTimeout(
@@ -559,6 +568,7 @@ export async function apiCoursewareContent(payload: {
559
  topic: string;
560
  duration?: string | null;
561
  outline_points?: string | null;
 
562
  }): Promise<{ content: string; weaviate_used: boolean }> {
563
  const base = getBaseUrl();
564
  const res = await fetchWithTimeout(
 
415
  topic: string;
416
  outline_hint?: string | null;
417
  reply_language?: string | null;
418
+ history?: Array<[string, string]> | null;
419
+ userMessage?: string | null;
420
  }): Promise<{ description: string; weaviate_used: boolean }> {
421
  const base = getBaseUrl();
422
  const res = await fetchWithTimeout(
 
438
  current_doc_excerpt?: string | null;
439
  doc_type?: string;
440
  reply_language?: string | null;
441
+ history?: Array<[string, string]> | null;
442
  }): Promise<{ suggestion: string; weaviate_used: boolean }> {
443
  const base = getBaseUrl();
444
  const res = await fetchWithTimeout(
 
460
  week_or_module?: string | null;
461
  question_type?: string;
462
  reply_language?: string | null;
463
+ history?: Array<[string, string]> | null;
464
  }): Promise<{ suggestion: string; weaviate_used: boolean }> {
465
  const base = getBaseUrl();
466
  const res = await fetchWithTimeout(
 
481
  assessment_summary: string;
482
  course_topic_hint?: string | null;
483
  reply_language?: string | null;
484
+ history?: Array<[string, string]> | null;
485
  }): Promise<{ analysis: string; weaviate_used: boolean }> {
486
  const base = getBaseUrl();
487
  const res = await fetchWithTimeout(
 
502
  export async function apiCoursewareVision(payload: {
503
  course_info: string;
504
  syllabus: string;
505
+ history?: Array<[string, string]> | null;
506
  }): Promise<{ content: string; weaviate_used: boolean }> {
507
  const base = getBaseUrl();
508
  const res = await fetchWithTimeout(
 
519
  topic: string;
520
  learning_objectives?: string | null;
521
  rag_context_override?: string | null;
522
+ history?: Array<[string, string]> | null;
523
  }): Promise<{ content: string; weaviate_used: boolean }> {
524
  const base = getBaseUrl();
525
  const res = await fetchWithTimeout(
 
535
  export async function apiCoursewareCopilot(payload: {
536
  current_content: string;
537
  student_profiles?: Array<{ name?: string; progress?: string; behavior?: string }> | null;
538
+ history?: Array<[string, string]> | null;
539
  }): Promise<{ content: string; weaviate_used: boolean }> {
540
  const base = getBaseUrl();
541
  const res = await fetchWithTimeout(
 
551
  export async function apiCoursewareQAOptimize(payload: {
552
  quiz_summary: string;
553
  course_topic?: string | null;
554
+ history?: Array<[string, string]> | null;
555
  }): Promise<{ content: string; weaviate_used: boolean }> {
556
  const base = getBaseUrl();
557
  const res = await fetchWithTimeout(
 
568
  topic: string;
569
  duration?: string | null;
570
  outline_points?: string | null;
571
+ history?: Array<[string, string]> | null;
572
  }): Promise<{ content: string; weaviate_used: boolean }> {
573
  const base = getBaseUrl();
574
  const res = await fetchWithTimeout(