Spaces:
Sleeping
Sleeping
Commit ·
a91db77
1
Parent(s): fbe1c8a
IST345 course + multi-TA, AI Quiz API doc, courseDirectory and sidebar updates
Browse files- api/store.py +7 -1
- docs/AI_QUIZ_API.md +158 -0
- web/src/App.tsx +11 -0
- web/src/components/sidebar/CourseInfoSection.tsx +31 -21
- web/src/components/sidebar/LeftSidebar.tsx +28 -20
- web/src/lib/courseDirectory.ts +15 -5
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
|
| 129 |
-
const
|
|
|
|
|
|
|
| 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 |
-
|
| 175 |
-
|
| 176 |
-
|
| 177 |
-
|
| 178 |
-
|
| 179 |
-
|
| 180 |
-
|
| 181 |
-
|
| 182 |
-
|
| 183 |
-
|
| 184 |
-
|
| 185 |
-
|
| 186 |
-
|
| 187 |
-
|
| 188 |
-
|
| 189 |
-
|
| 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:
|
| 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
|
| 263 |
-
const
|
|
|
|
|
|
|
| 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 |
-
|
| 425 |
-
|
| 426 |
-
|
| 427 |
-
|
| 428 |
-
|
| 429 |
-
|
| 430 |
-
|
| 431 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 432 |
)}
|
| 433 |
-
|
| 434 |
-
|
| 435 |
-
|
| 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:
|
| 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;
|
| 12 |
-
teachingAssistant?: Person;
|
|
|
|
|
|
|
| 13 |
};
|
| 14 |
|
| 15 |
/**
|
| 16 |
-
*
|
| 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",
|