claudqunwang Cursor commited on
Commit
a91db77
·
1 Parent(s): fbe1c8a

IST345 course + multi-TA, AI Quiz API doc, courseDirectory and sidebar updates

Browse files
api/store.py CHANGED
@@ -38,12 +38,18 @@ def seed_if_missing() -> None:
38
 
39
  if not COURSE_FILE.exists():
40
  courses = [
 
 
 
 
 
 
41
  CourseDirectoryItem(
42
  id="course_ai_001",
43
  name="Introduction to AI",
44
  instructor=PersonContact(name="Dr. Smith", email="smith@uni.edu"),
45
  teachingAssistant=PersonContact(name="Alice", email="alice@uni.edu"),
46
- ).model_dump()
47
  ]
48
  _write_json(COURSE_FILE, {"items": courses})
49
 
 
38
 
39
  if not COURSE_FILE.exists():
40
  courses = [
41
+ CourseDirectoryItem(
42
+ id="ist345",
43
+ name="IST345",
44
+ instructor=PersonContact(name="Yan Li", email="Yan.Li@cgu.edu"),
45
+ teachingAssistant=PersonContact(name="Kaijie Yu", email="Kaijie.Yu@cgu.edu"),
46
+ ).model_dump(),
47
  CourseDirectoryItem(
48
  id="course_ai_001",
49
  name="Introduction to AI",
50
  instructor=PersonContact(name="Dr. Smith", email="smith@uni.edu"),
51
  teachingAssistant=PersonContact(name="Alice", email="alice@uni.edu"),
52
+ ).model_dump(),
53
  ]
54
  _write_json(COURSE_FILE, {"items": courses})
55
 
docs/AI_QUIZ_API.md ADDED
@@ -0,0 +1,158 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # AI Quiz API 接口文档
2
+
3
+ 用于与前端/其他网页进行 AI Quiz 互动的统一接口规范。所有响应均包含 `meta` 用于模型追踪与排查;题目使用 `question_id` 与 `correct_answers` 便于后端存档与追溯。
4
+
5
+ ---
6
+
7
+ ## 1. 通用:所有响应增加 meta
8
+
9
+ 用于排查模型问题与结果追溯,**所有成功响应**均需包含:
10
+
11
+ ```json
12
+ "meta": {
13
+ "model": "gpt-4.1-mini",
14
+ "model_version": "2024-07",
15
+ "prompt_version": "quiz_v2",
16
+ "temperature": 0.4,
17
+ "tokens_used": 842,
18
+ "latency_ms": 1280
19
+ }
20
+ ```
21
+
22
+ | 字段 | 类型 | 说明 |
23
+ |------|------|------|
24
+ | `model` | string | 模型标识 |
25
+ | `model_version` | string | 模型版本(可选) |
26
+ | `prompt_version` | string | 当前 prompt 版本,便于回溯 |
27
+ | `temperature` | number | 采样温度 |
28
+ | `tokens_used` | number | 本次请求消耗 token 数 |
29
+ | `latency_ms` | number | 请求耗时(毫秒) |
30
+
31
+ ---
32
+
33
+ ## 2. Generate Quiz:题目与答案结构
34
+
35
+ ### 2.1 删除字段
36
+
37
+ - **禁止**使用 `options[].is_correct` 表示正确答案。
38
+
39
+ ### 2.2 新增 / 修改结构
40
+
41
+ 每题必须包含唯一 `question_id`,答案通过 **`correct_answers`** 数组表示,便于后端校验与存档。
42
+
43
+ **题目项示例:**
44
+
45
+ ```json
46
+ {
47
+ "question_id": "ai_q_001",
48
+ "type": "SINGLE_CHOICE",
49
+ "question_text": "What is responsible AI?",
50
+ "options": [
51
+ { "key": "A", "text": "AI that only runs on servers" },
52
+ { "key": "B", "text": "AI designed to be accountable and fair" },
53
+ { "key": "C", "text": "AI that uses no data" }
54
+ ],
55
+ "correct_answers": ["B"]
56
+ }
57
+ ```
58
+
59
+ **答案规则(后端需校验,不合法可重试):**
60
+
61
+ | 题型 | correct_answers 规则 | 备注 |
62
+ |------|----------------------|------|
63
+ | `SINGLE_CHOICE` | 必须 **恰好 1 个** 答案 | 如 `["B"]` |
64
+ | `MULTIPLE_CHOICE` | **≥ 1** 个答案 | 如 `["A","C"]` |
65
+ | `TRUE_FALSE` | 必须 **2 个选项**(True/False) | 答案为其中之一 |
66
+ | `SHORT_ANSWER` | 不适用选项;必须返回 **`explanation`** | 用于评分与反馈 |
67
+
68
+ **SHORT_ANSWER 示例:**
69
+
70
+ ```json
71
+ {
72
+ "question_id": "ai_q_002",
73
+ "type": "SHORT_ANSWER",
74
+ "question_text": "Explain one risk of generative AI in education.",
75
+ "explanation": "Reference answer: e.g. over-reliance may reduce critical thinking."
76
+ }
77
+ ```
78
+
79
+ ---
80
+
81
+ ## 3. 标准错误返回
82
+
83
+ 当 AI 输出不合法、超时或服务失败时,统一使用以下结构(**非 2xx**):
84
+
85
+ ```json
86
+ {
87
+ "code": 422,
88
+ "error": {
89
+ "type": "INVALID_GENERATION",
90
+ "reason": "multiple_correct_answers"
91
+ }
92
+ }
93
+ ```
94
+
95
+ ### 3.1 错误码与 type
96
+
97
+ | HTTP code | error.type | 说明 |
98
+ |-----------|------------|------|
99
+ | **422** | `INVALID_GENERATION` | AI 输出不合法(如单选多答、缺少必填字段等),`reason` 可细化 |
100
+ | **429** | `RATE_LIMIT` | 请求频率超限 |
101
+ | **500** | `MODEL_ERROR` | 模型/上游服务错误 |
102
+ | **504** | `TIMEOUT` | 请求超时 |
103
+
104
+ ### 3.2 reason 示例(422)
105
+
106
+ - `multiple_correct_answers`:单选题返回了多个正确答案
107
+ - `missing_question_id`:缺少 `question_id`
108
+ - `missing_correct_answers`:缺少 `correct_answers` 或不符合题型规则
109
+ - `invalid_short_answer`:SHORT_ANSWER 缺少 `explanation`
110
+
111
+ ---
112
+
113
+ ## 4. Grade Quiz:每题解释
114
+
115
+ 批改接口响应中,**新增** `per_question_feedback`,对每题给出分数与简要理由:
116
+
117
+ ```json
118
+ {
119
+ "overall_score": 75,
120
+ "per_question_feedback": [
121
+ {
122
+ "question_id": "ai_q_001",
123
+ "score": 10,
124
+ "reasoning": "Correct choice; full marks."
125
+ },
126
+ {
127
+ "question_id": "ai_q_002",
128
+ "score": 7,
129
+ "reasoning": "Concept correct but example missing."
130
+ }
131
+ ],
132
+ "meta": {
133
+ "model": "gpt-4.1-mini",
134
+ "model_version": "2024-07",
135
+ "prompt_version": "grade_v1",
136
+ "temperature": 0.2,
137
+ "tokens_used": 520,
138
+ "latency_ms": 890
139
+ }
140
+ }
141
+ ```
142
+
143
+ | 字段 | 类型 | 说明 |
144
+ |------|------|------|
145
+ | `question_id` | string | 对应题目 ID,与 Generate 阶段一致 |
146
+ | `score` | number | 该题得分 |
147
+ | `reasoning` | string | 简短批改理由 |
148
+
149
+ ---
150
+
151
+ ## 5. 课程信息(参考)
152
+
153
+ - **Course**: IST345(来自 syllabus)
154
+ - **Instructor**: Yan Li — Yan.Li@cgu.edu
155
+ - **TA**: Kaijie Yu — Kaijie.Yu@cgu.edu
156
+ - **TA**: Yongjia Sun — Yongjia.Sun@cgu.edu
157
+
158
+ 前端展示 Instructor 与多位 TA 时,可依此配置。
web/src/App.tsx CHANGED
@@ -99,6 +99,7 @@ export interface CourseInfo {
99
  name: string;
100
  instructor: { name: string; email: string };
101
  teachingAssistant: { name: string; email: string };
 
102
  }
103
 
104
  export interface Workspace {
@@ -243,6 +244,16 @@ function App() {
243
  });
244
 
245
  const availableCourses: CourseInfo[] = [
 
 
 
 
 
 
 
 
 
 
246
  {
247
  id: "course1",
248
  name: "Introduction to AI",
 
99
  name: string;
100
  instructor: { name: string; email: string };
101
  teachingAssistant: { name: string; email: string };
102
+ teachingAssistants?: { name: string; email: string }[];
103
  }
104
 
105
  export interface Workspace {
 
244
  });
245
 
246
  const availableCourses: CourseInfo[] = [
247
+ {
248
+ id: "ist345",
249
+ name: "IST345",
250
+ instructor: { name: "Yan Li", email: "Yan.Li@cgu.edu" },
251
+ teachingAssistant: { name: "Kaijie Yu", email: "Kaijie.Yu@cgu.edu" },
252
+ teachingAssistants: [
253
+ { name: "Kaijie Yu", email: "Kaijie.Yu@cgu.edu" },
254
+ { name: "Yongjia Sun", email: "Yongjia.Sun@cgu.edu" },
255
+ ],
256
+ },
257
  {
258
  id: "course1",
259
  name: "Introduction to AI",
web/src/components/sidebar/CourseInfoSection.tsx CHANGED
@@ -125,8 +125,10 @@ export function CourseInfoSection({
125
  const instructorName = (courseInfo as any)?.instructor?.name ?? "N/A";
126
  const instructorEmail = String((courseInfo as any)?.instructor?.email ?? "").trim();
127
 
128
- const taName = (courseInfo as any)?.teachingAssistant?.name ?? "N/A";
129
- const taEmail = String((courseInfo as any)?.teachingAssistant?.email ?? "").trim();
 
 
130
 
131
  return (
132
  <div className="w-full">
@@ -169,25 +171,33 @@ export function CourseInfoSection({
169
  )}
170
  </div>
171
 
172
- {/* TA */}
173
- <div className="text-sm text-muted-foreground">
174
- TA:&nbsp;
175
- {taEmail ? (
176
- <a
177
- href={gmailComposeLink(
178
- taEmail,
179
- `[Clare] Help request for ${courseName}`,
180
- `Hi ${taName},\n\nI need help with ${courseName}:\n\n(Write your question here)\n\nThanks,\n`
181
- )}
182
- target="_blank"
183
- rel="noopener noreferrer"
184
- className="text-primary hover:underline"
185
- >
186
- {taName}
187
- </a>
188
- ) : (
189
- <span className="text-muted-foreground/60">{taName}</span>
190
- )}
 
 
 
 
 
 
 
 
191
  </div>
192
  </div>
193
 
 
125
  const instructorName = (courseInfo as any)?.instructor?.name ?? "N/A";
126
  const instructorEmail = String((courseInfo as any)?.instructor?.email ?? "").trim();
127
 
128
+ const teachingAssistants = (courseInfo as any)?.teachingAssistants as { name?: string; email?: string }[] | undefined;
129
+ const taList = Array.isArray(teachingAssistants) && teachingAssistants.length > 0
130
+ ? teachingAssistants
131
+ : [(courseInfo as any)?.teachingAssistant ?? { name: "N/A", email: "" }];
132
 
133
  return (
134
  <div className="w-full">
 
171
  )}
172
  </div>
173
 
174
+ {/* TA(s) */}
175
+ <div className="text-sm text-muted-foreground space-y-1">
176
+ {taList.map((ta, i) => {
177
+ const name = ta?.name ?? "N/A";
178
+ const email = String(ta?.email ?? "").trim();
179
+ return (
180
+ <div key={i}>
181
+ TA:&nbsp;
182
+ {email ? (
183
+ <a
184
+ href={gmailComposeLink(
185
+ email,
186
+ `[Clare] Help request for ${courseName}`,
187
+ `Hi ${name},\n\nI need help with ${courseName}:\n\n(Write your question here)\n\nThanks,\n`
188
+ )}
189
+ target="_blank"
190
+ rel="noopener noreferrer"
191
+ className="text-primary hover:underline"
192
+ >
193
+ {name}
194
+ </a>
195
+ ) : (
196
+ <span className="text-muted-foreground/60">{name}</span>
197
+ )}
198
+ </div>
199
+ );
200
+ })}
201
  </div>
202
  </div>
203
 
web/src/components/sidebar/LeftSidebar.tsx CHANGED
@@ -259,8 +259,10 @@ export function LeftSidebar(props: Props) {
259
  const instructorName = (courseInfo as any)?.instructor?.name ?? "N/A";
260
  const instructorEmail = String((courseInfo as any)?.instructor?.email ?? "").trim();
261
 
262
- const taName = (courseInfo as any)?.teachingAssistant?.name ?? "N/A";
263
- const taEmail = String((courseInfo as any)?.teachingAssistant?.email ?? "").trim();
 
 
264
 
265
  return (
266
  <div className="h-full w-full flex flex-col min-h-0 bg-background text-foreground">
@@ -421,25 +423,31 @@ export function LeftSidebar(props: Props) {
421
  )}
422
  </div>
423
 
424
- <div className="text-muted-foreground">
425
- TA:&nbsp;
426
- {taEmail ? (
427
- <a
428
- href={gmailComposeLink(
429
- taEmail,
430
- `[Clare] Help request for ${courseName}`,
431
- `Hi ${taName},\n\nI need help with ${courseName}:\n\n(Write your question here)\n\nThanks,\n`
 
 
 
 
 
 
 
 
 
 
 
 
 
432
  )}
433
- target="_blank"
434
- rel="noopener noreferrer"
435
- className="text-primary hover:underline"
436
- >
437
- {taName}
438
- </a>
439
- ) : (
440
- <span className="text-muted-foreground/60">{taName}</span>
441
- )}
442
- </div>
443
  </div>
444
  </div>
445
 
 
259
  const instructorName = (courseInfo as any)?.instructor?.name ?? "N/A";
260
  const instructorEmail = String((courseInfo as any)?.instructor?.email ?? "").trim();
261
 
262
+ const teachingAssistants = (courseInfo as any)?.teachingAssistants as { name?: string; email?: string }[] | undefined;
263
+ const taList = Array.isArray(teachingAssistants) && teachingAssistants.length > 0
264
+ ? teachingAssistants
265
+ : [(courseInfo as any)?.teachingAssistant ?? { name: "N/A", email: "" }];
266
 
267
  return (
268
  <div className="h-full w-full flex flex-col min-h-0 bg-background text-foreground">
 
423
  )}
424
  </div>
425
 
426
+ {taList.map((ta, i) => {
427
+ const name = ta?.name ?? "N/A";
428
+ const email = String(ta?.email ?? "").trim();
429
+ return (
430
+ <div key={i} className="text-muted-foreground">
431
+ TA:&nbsp;
432
+ {email ? (
433
+ <a
434
+ href={gmailComposeLink(
435
+ email,
436
+ `[Clare] Help request for ${courseName}`,
437
+ `Hi ${name},\n\nI need help with ${courseName}:\n\n(Write your question here)\n\nThanks,\n`
438
+ )}
439
+ target="_blank"
440
+ rel="noopener noreferrer"
441
+ className="text-primary hover:underline"
442
+ >
443
+ {name}
444
+ </a>
445
+ ) : (
446
+ <span className="text-muted-foreground/60">{name}</span>
447
  )}
448
+ </div>
449
+ );
450
+ })}
 
 
 
 
 
 
 
451
  </div>
452
  </div>
453
 
web/src/lib/courseDirectory.ts CHANGED
@@ -8,16 +8,26 @@ export type Person = {
8
  export type CourseDirectoryItem = {
9
  id: string;
10
  name: string;
11
- instructor?: Person; // optional: some courses may not have info yet
12
- teachingAssistant?: Person; // optional
 
 
13
  };
14
 
15
  /**
16
- * NOTE:
17
- * - Fill in missing names/emails here once confirmed.
18
- * - Keep emails empty ("") or undefined if you don't have them yet.
19
  */
20
  export const COURSE_DIRECTORY: CourseDirectoryItem[] = [
 
 
 
 
 
 
 
 
 
 
21
  {
22
  id: "intro_ai",
23
  name: "Introduction to AI",
 
8
  export type CourseDirectoryItem = {
9
  id: string;
10
  name: string;
11
+ instructor?: Person;
12
+ teachingAssistant?: Person;
13
+ /** Optional: when multiple TAs (display all) */
14
+ teachingAssistants?: Person[];
15
  };
16
 
17
  /**
18
+ * IST345 from syllabus; Instructor and TAs from course info.
 
 
19
  */
20
  export const COURSE_DIRECTORY: CourseDirectoryItem[] = [
21
+ {
22
+ id: "ist345",
23
+ name: "IST345",
24
+ instructor: { name: "Yan Li", email: "Yan.Li@cgu.edu" },
25
+ teachingAssistant: { name: "Kaijie Yu", email: "Kaijie.Yu@cgu.edu" },
26
+ teachingAssistants: [
27
+ { name: "Kaijie Yu", email: "Kaijie.Yu@cgu.edu" },
28
+ { name: "Yongjia Sun", email: "Yongjia.Sun@cgu.edu" },
29
+ ],
30
+ },
31
  {
32
  id: "intro_ai",
33
  name: "Introduction to AI",