SarahXia0405 commited on
Commit
7b43c09
·
verified ·
1 Parent(s): 96891a5

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +984 -30
app.py CHANGED
@@ -1,37 +1,991 @@
1
- rag_context = retrieve_relevant_chunks(message, rag_chunks or [])
 
 
 
 
2
 
3
- # 如果完全检索不到任何与 Module 10 文档相关的内容,就认定为 out-of-scope
4
- if (not rag_context) and doc_type_val in ["Syllabus", "Reading", "Module 10"]:
5
- out_msg = (
6
- "For this experiment, I'm only allowed to discuss **Module 10 – "
7
- "Responsible AI (Alto, 2024, Chapter 12)** and the uploaded Module 10 materials.\n\n"
8
- "Your question seems to be outside this scope. Could you please rephrase it "
9
- "to focus on Responsible AI topics (risks, mitigation layers, regulations, etc.)?"
10
- )
11
- new_history = (chat_history or []) + [[message, out_msg]]
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
12
 
13
- # 也可以顺便记一条“out_of_scope”日志
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
14
  try:
15
- log_event(
16
- {
17
- "experiment_id": "RESP_AI_W10",
18
- "student_id": user_id_val or "ANON",
19
- "event_type": "out_of_scope",
20
- "timestamp": time.time(),
21
- "latency_ms": 0,
22
- "question": message,
23
- "answer": out_msg,
24
- "model_name": model_name_val,
25
- "language": resolved_lang,
26
- "learning_mode": mode_val,
27
- }
28
  )
 
 
 
 
29
  except Exception as e:
30
- print("log_event error:", e)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
31
 
32
- new_status = render_session_status(
33
- mode_val or "Concept Explainer",
34
- weaknesses or [],
35
- cognitive_state or {"confusion": 0, "mastery": 0},
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
36
  )
37
- return "", new_history, weaknesses, cognitive_state, new_status
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import time
3
+ import base64
4
+ import requests
5
+ from typing import List, Dict, Tuple, Optional
6
 
7
+ import gradio as gr
8
+
9
+ from config import (
10
+ DEFAULT_MODEL,
11
+ DEFAULT_COURSE_TOPICS,
12
+ LEARNING_MODES,
13
+ DOC_TYPES,
14
+ )
15
+ from clare_core import (
16
+ update_weaknesses_from_message,
17
+ update_cognitive_state_from_message,
18
+ render_session_status,
19
+ find_similar_past_question,
20
+ detect_language,
21
+ chat_with_clare,
22
+ export_conversation,
23
+ generate_quiz_from_history,
24
+ get_empty_input_prompt,
25
+ summarize_conversation,
26
+ )
27
+ from rag_engine import (
28
+ build_rag_chunks_from_file,
29
+ retrieve_relevant_chunks,
30
+ )
31
+ from syllabus_utils import extract_course_topics_from_file
32
+
33
+ # ================== Assets ==================
34
+ HANBRIDGE_LOGO_PATH = "hanbridge_logo.png"
35
+ CLARE_LOGO_PATH = "clare_mascot.png"
36
+ CLARE_RUN_PATH = "Clare_Run.png"
37
+ CLARE_READING_PATH = "Clare_reading.png" # 确保存在
38
+
39
+ # ================== Base64 Helper ==================
40
+ def image_to_base64(image_path: str) -> str:
41
+ if not os.path.exists(image_path):
42
+ return ""
43
+ with open(image_path, "rb") as img_file:
44
+ encoded_string = base64.b64encode(img_file.read()).decode("utf-8")
45
+ if image_path.lower().endswith(".png"):
46
+ mime = "image/png"
47
+ elif image_path.lower().endswith((".jpg", ".jpeg")):
48
+ mime = "image/jpeg"
49
+ else:
50
+ mime = "image/png"
51
+ return f"data:{mime};base64,{encoded_string}"
52
+
53
+ # ================== User Guide Content ==================
54
+ USER_GUIDE_SECTIONS = {
55
+ "getting_started": """
56
+ Welcome to **Clare — Your Personalized AI Tutor**.
57
+
58
+ For this controlled experiment, Clare is already pre-loaded with:
59
+
60
+ 📘 **Module 10 Reading – Responsible AI (Alto, 2024, Chapter 12)**
61
+
62
+ You do **NOT** need to upload any materials.
63
+ You may optionally upload extra files, but Clare will always include the Module 10 reading as core context.
64
+
65
+ **To begin:**
66
+ 1. Log in with your **Student Name + Email/ID** on the right.
67
+ 2. Select your **Learning Mode** on the left.
68
+ 3. (Optional) Upload additional Module 10 slides / notes at the top.
69
+ 4. Ask Clare any question about **Module 10 – Responsible AI**.
70
+ """,
71
+ "mode_definition": """
72
+ Clare offers different teaching modes to match how you prefer to learn.
73
+
74
+ ### Concept Explainer
75
+ Clear, structured explanations with examples — ideal for learning new topics.
76
+
77
+ ### Socratic Tutor
78
+ Clare asks guiding questions instead of giving direct answers.
79
+ Helps you build reasoning and problem-solving skills.
80
+
81
+ ### Exam Prep / Quiz
82
+ Generates short practice questions aligned with your course week.
83
+ Useful for self-testing and preparing for exams.
84
+
85
+ ### Assignment Helper
86
+ Helps you interpret assignment prompts, plan structure, and understand requirements.
87
+ ❗ Clare does **not** produce full assignment answers (academic integrity).
88
+
89
+ ### Quick Summary
90
+ Gives brief summaries of slides, reading materials, or long questions.
91
+ """,
92
+ "how_clare_works": """
93
+ Clare combines **course context + learning science + AI reasoning** to generate answers.
94
+
95
+ For this experiment, Clare always includes:
96
+
97
+ - Module 10 Reading – Responsible AI (Alto, 2024, Chapter 12)
98
+ - Any additional Module 10 files you upload
99
+
100
+ Clare uses:
101
+ - **Learning Mode**: tone, depth, and interaction style.
102
+ - **Reinforcement model**: may prioritize concepts you’re likely to forget.
103
+ - **Responsible AI principles**: avoids harmful output and preserves academic integrity.
104
+ """,
105
+ "memory_line": """
106
+ **Memory Line** is a visualization of your *learning reinforcement cycle*.
107
+
108
+ Based on the **forgetting-curve model**, Clare organizes your review topics into:
109
+ - **T+0 (Current Week)** – new concepts
110
+ - **T+7** – first spaced review
111
+ - **T+14** – reinforcement review
112
+ - **T+30** – long-term consolidation
113
+
114
+ In this experiment, Memory Line should be interpreted as your **Module 10** reinforcement status.
115
+ """,
116
+ "learning_progress": """
117
+ The Learning Progress Report highlights:
118
+ - **Concepts mastered**
119
+ - **Concepts in progress**
120
+ - **Concepts due for review**
121
+ - Your recent **micro-quiz results**
122
+ - Suggested **next-step topics**
123
+ """,
124
+ "how_files": """
125
+ Your uploaded materials help Clare:
126
+
127
+ - Align explanations with your exact course (here: **Module 10 – Responsible AI**)
128
+ - Use terminology consistent with your professor
129
+ - Improve factual accuracy
130
+
131
+ 🔒 **Privacy**
132
+ - Files are used only within your session
133
+ - They are not kept as permanent training data
134
+
135
+ Accepted formats: **.docx / .pdf / .pptx**
136
+
137
+ For this experiment, Clare is **already pre-loaded** with the Module 10 reading. Uploads are optional.
138
+ """,
139
+ "micro_quiz": """
140
+ The **Micro-Quiz** function provides a:
141
+
142
+ - 1-minute self-check
143
+ - 1–3 questions about **Module 10 – Responsible AI**
144
+ - Instant feedback inside the main chat
145
+
146
+ **How it works:**
147
+ 1. Click “Let’s Try (Micro-Quiz)” on the right.
148
+ 2. Clare will send the **first quiz question** in the main chat.
149
+ 3. Type your answer in the chat box.
150
+ 4. Clare will:
151
+ - Judge correctness
152
+ - Give a brief explanation
153
+ - Ask if you want another question
154
+ 5. You can continue or say “stop” at any time.
155
+ """,
156
+ "summarization": """
157
+ Clare can summarize:
158
+
159
+ - Module 10 reading
160
+ - Uploaded slides / notes
161
+ - Long conversation threads
162
+ """,
163
+ "export_conversation": """
164
+ You can export your chat session for:
165
+
166
+ - Study review
167
+ - Exam preparation
168
+ - Saving important explanations
169
+
170
+ Export format: **Markdown / plain text**.
171
+ """,
172
+ "faq": """
173
+ **Q: Does Clare give assignment answers?**
174
+ No. Clare assists with understanding and planning but does **not** generate full solutions.
175
+
176
+ **Q: Does Clare replace lectures or TA office hours?**
177
+ No. Clare supplements your learning by providing on-demand guidance.
178
+
179
+ **Q: What languages does Clare support?**
180
+ Currently: English & 简体中文.
181
+ """
182
+ }
183
+
184
+ # ================== CSS 样式表 ==================
185
+ CUSTOM_CSS = """
186
+ /* --- Main Header --- */
187
+ .header-container { padding: 10px 20px; background-color: #ffffff; border-bottom: 2px solid #f3f4f6; margin-bottom: 15px; display: flex; align-items: center; }
188
 
189
+ /* --- Sidebar Login Panel --- */
190
+ .login-panel {
191
+ background-color: #e5e7eb;
192
+ padding: 15px;
193
+ border-radius: 8px;
194
+ text-align: center;
195
+ margin-bottom: 20px;
196
+ }
197
+ .login-panel img {
198
+ display: block;
199
+ margin: 0 auto 10px auto;
200
+ height: 80px;
201
+ object-fit: contain;
202
+ }
203
+ .login-main-btn {
204
+ background-color: #ffffff !important;
205
+ color: #000 !important;
206
+ border: 1px solid #000 !important;
207
+ font-weight: bold !important;
208
+ }
209
+ .logout-btn {
210
+ background-color: #6b2828 !important;
211
+ color: #fff !important;
212
+ border: none !important;
213
+ font-weight: bold !important;
214
+ }
215
+
216
+ /* User Guide */
217
+ .main-user-guide { border: none !important; background: transparent !important; box-shadow: none !important; }
218
+ .main-user-guide > .label-wrap { border: none !important; background: transparent !important; padding: 10px 0 !important; }
219
+ .main-user-guide > .label-wrap span { font-size: 1.3rem !important; font-weight: 800 !important; color: #111827 !important; }
220
+
221
+ .clean-accordion { border: none !important; background: transparent !important; box-shadow: none !important; margin-bottom: 0px !important; padding: 0 !important; border-radius: 0 !important; }
222
+ .clean-accordion > .label-wrap { padding: 8px 5px !important; border: none !important; background: transparent !important; border-bottom: 1px solid #e5e7eb !important; }
223
+ .clean-accordion > .label-wrap span { font-size: 0.9rem !important; font-weight: 500 !important; color: #374151 !important; }
224
+ .clean-accordion > .label-wrap:hover { background-color: #f9fafb !important; }
225
+
226
+ /* Action Buttons */
227
+ .action-btn { font-weight: bold !important; font-size: 0.9rem !important; position: relative; overflow: visible !important; }
228
+ .action-btn:hover::before { content: "See User Guide for details"; position: absolute; bottom: 110%; left: 50%; transform: translateX(-50%); background-color: #333; color: #fff; padding: 5px 10px; border-radius: 5px; font-size: 12px; white-space: nowrap; z-index: 1000; pointer-events: none; opacity: 0; animation: fadeIn 0.2s forwards; }
229
+ .action-btn:hover::after { content: ""; position: absolute; bottom: 100%; left: 50%; margin-left: -5px; border-width: 5px; border-style: solid; border-color: #333 transparent transparent transparent; opacity: 0; animation: fadeIn 0.2s forwards; }
230
+
231
+ /* Tooltips & Memory Line */
232
+ .html-tooltip { border-bottom: 1px dashed #999; cursor: help; position: relative; }
233
+ .html-tooltip:hover::before { content: attr(data-tooltip); position: absolute; bottom: 120%; left: 0; background-color: #333; color: #fff; padding: 5px 8px; border-radius: 4px; font-size: 11px; white-space: nowrap; z-index: 100; pointer-events: none; }
234
+ .memory-line-box { border: 1px solid #e5e7eb; padding: 12px; border-radius: 8px; background-color: #f9fafb; height: 100%; display: flex; flex-direction: column; justify-content: space-between; }
235
+
236
+ /* Results Box Style */
237
+ .result-box { border: 1px solid #e5e7eb; background: #ffffff; padding: 10px; border-radius: 8px; height: 100%; }
238
+ .result-box .prose { font-size: 0.9rem; }
239
+
240
+ @keyframes fadeIn { to { opacity: 1; } }
241
+ """
242
+
243
+ # ========== Preload Module 10 PDF ==========
244
+ MODULE10_PATH = "module10_responsible_ai.pdf"
245
+ MODULE10_DOC_TYPE = "Reading"
246
+
247
+ preloaded_topics: List[str] = []
248
+ preloaded_chunks: List[Dict] = []
249
+
250
+ if os.path.exists(MODULE10_PATH):
251
  try:
252
+ preloaded_topics = extract_course_topics_from_file(
253
+ MODULE10_PATH, MODULE10_DOC_TYPE
 
 
 
 
 
 
 
 
 
 
 
254
  )
255
+ preloaded_chunks = build_rag_chunks_from_file(
256
+ MODULE10_PATH, MODULE10_DOC_TYPE
257
+ )
258
+ print("Module 10 PDF preloaded successfully.")
259
  except Exception as e:
260
+ print("Module 10 preload failed:", e)
261
+ else:
262
+ print("Module 10 PDF not found at path:", MODULE10_PATH)
263
+
264
+ # ===== Google Sheet logging ====
265
+ GSHEET_WEBHOOK_URL = os.getenv("GSHEET_WEBHOOK_URL")
266
+ GSHEET_API_KEY = os.getenv("GSHEET_API_KEY")
267
+
268
+
269
+ def log_event(data: Dict):
270
+ """
271
+ 把日志发到 Google Sheet(通过 Apps Script Webhook)
272
+ """
273
+ if not GSHEET_WEBHOOK_URL:
274
+ print("Google Sheet webhook not configured, skip logging")
275
+ return
276
+
277
+ payload = data.copy()
278
+ payload["api_key"] = GSHEET_API_KEY or ""
279
+
280
+ try:
281
+ resp = requests.post(GSHEET_WEBHOOK_URL, json=payload, timeout=3)
282
+ if not resp.ok:
283
+ print("GSheet log failed:", resp.status_code, resp.text)
284
+ except Exception as e:
285
+ print("GSheet log exception:", e)
286
+
287
+
288
+ # ================== Gradio App ==================
289
+ with gr.Blocks(
290
+ title="Clare – Hanbridge AI Teaching Assistant", css=CUSTOM_CSS
291
+ ) as demo:
292
+
293
+ # 全局状态(预加载 Module 10 作为基础)
294
+ course_outline_state = gr.State(preloaded_topics or DEFAULT_COURSE_TOPICS)
295
+ weakness_state = gr.State([])
296
+ cognitive_state_state = gr.State({"confusion": 0, "mastery": 0})
297
+ rag_chunks_state = gr.State(preloaded_chunks or [])
298
+
299
+ # 用户状态(登录)
300
+ user_name_state = gr.State("")
301
+ user_id_state = gr.State("")
302
+
303
+ # --- Header ---
304
+ with gr.Row(elem_classes="header-container"):
305
+ with gr.Column(scale=2):
306
+ gr.HTML(
307
+ f"""
308
+ <div style="display:flex; align-items:center; gap: 20px;">
309
+ <img src="{image_to_base64(CLARE_LOGO_PATH)}" style="height: 75px; object-fit: contain;">
310
+ <div style="display:flex; flex-direction:column;">
311
+ <div style="font-size: 32px; font-weight: 800; line-height: 1.1; color: #000;">
312
+ Clare
313
+ <span style="font-size: 18px; font-weight: 600; margin-left: 10px;">Your Personalized AI Tutor</span>
314
+ </div>
315
+ <div style="font-size: 14px; font-style: italic; color: #333; margin-top: 4px;">
316
+ Personalized guidance, review, and intelligent reinforcement
317
+ </div>
318
+ </div>
319
+ </div>
320
+ """
321
+ )
322
+
323
+ with gr.Column(scale=1):
324
+ gr.HTML(
325
+ f"""
326
+ <div style="display:flex; justify-content:flex-end; margin-bottom: 5px;">
327
+ <img src="{image_to_base64(HANBRIDGE_LOGO_PATH)}" style="height: 55px; object-fit: contain;">
328
+ </div>
329
+ """
330
+ )
331
+
332
+ # --- Main Layout ---
333
+ with gr.Row():
334
+
335
+ # === Left Sidebar ===
336
+ with gr.Column(scale=1, min_width=200):
337
+ clear_btn = gr.Button(
338
+ "Reset Conversation", variant="stop", interactive=False
339
+ )
340
+
341
+ gr.Markdown("### Model Settings")
342
+ model_name = gr.Textbox(
343
+ label="Model",
344
+ value="gpt-4.1-mini",
345
+ interactive=False,
346
+ lines=1,
347
+ )
348
+ language_preference = gr.Radio(
349
+ choices=["Auto", "English", "简体中文"],
350
+ value="Auto",
351
+ label="Language",
352
+ interactive=False,
353
+ )
354
+
355
+ learning_mode = gr.Radio(
356
+ choices=LEARNING_MODES,
357
+ value="Concept Explainer",
358
+ label="Learning Mode",
359
+ info="See User Guide for mode definition details.",
360
+ interactive=False,
361
+ )
362
+
363
+ # User Guide
364
+ with gr.Accordion(
365
+ "User Guide", open=True, elem_classes="main-user-guide"
366
+ ):
367
+ with gr.Accordion(
368
+ "Getting Started",
369
+ open=False,
370
+ elem_classes="clean-accordion",
371
+ ):
372
+ gr.Markdown(USER_GUIDE_SECTIONS["getting_started"])
373
+ with gr.Accordion(
374
+ "Mode Definition",
375
+ open=False,
376
+ elem_classes="clean-accordion",
377
+ ):
378
+ gr.Markdown(USER_GUIDE_SECTIONS["mode_definition"])
379
+ with gr.Accordion(
380
+ "How Clare Works",
381
+ open=False,
382
+ elem_classes="clean-accordion",
383
+ ):
384
+ gr.Markdown(USER_GUIDE_SECTIONS["how_clare_works"])
385
+ with gr.Accordion(
386
+ "What is Memory Line",
387
+ open=False,
388
+ elem_classes="clean-accordion",
389
+ ):
390
+ gr.Markdown(USER_GUIDE_SECTIONS["memory_line"])
391
+ with gr.Accordion(
392
+ "Learning Progress Report",
393
+ open=False,
394
+ elem_classes="clean-accordion",
395
+ ):
396
+ gr.Markdown(USER_GUIDE_SECTIONS["learning_progress"])
397
+ with gr.Accordion(
398
+ "How Clare Uses Your Files",
399
+ open=False,
400
+ elem_classes="clean-accordion",
401
+ ):
402
+ gr.Markdown(USER_GUIDE_SECTIONS["how_files"])
403
+ with gr.Accordion(
404
+ "Micro-Quiz", open=False, elem_classes="clean-accordion"
405
+ ):
406
+ gr.Markdown(USER_GUIDE_SECTIONS["micro_quiz"])
407
+ with gr.Accordion(
408
+ "Summarization",
409
+ open=False,
410
+ elem_classes="clean-accordion",
411
+ ):
412
+ gr.Markdown(USER_GUIDE_SECTIONS["summarization"])
413
+ with gr.Accordion(
414
+ "Export Conversation",
415
+ open=False,
416
+ elem_classes="clean-accordion",
417
+ ):
418
+ gr.Markdown(USER_GUIDE_SECTIONS["export_conversation"])
419
+ with gr.Accordion(
420
+ "FAQ", open=False, elem_classes="clean-accordion"
421
+ ):
422
+ gr.Markdown(USER_GUIDE_SECTIONS["faq"])
423
+
424
+ gr.Markdown("---")
425
+ gr.Button("System Settings", size="sm", variant="secondary", interactive=False)
426
+
427
+ gr.HTML(
428
+ """
429
+ <div style="font-size: 11px; color: #9ca3af; margin-top: 15px; text-align: left;">
430
+ © 2025 Made by <a href="https://www.linkedin.com/in/qinghua-xia-479199252/" target="_blank" style="color: #6b7280; text-decoration: underline;">Sarah Xia</a>
431
+ </div>
432
+ """
433
+ )
434
 
435
+ # === Center Main ===
436
+ with gr.Column(scale=3):
437
+
438
+ # --- Top Control Row ---
439
+ with gr.Row():
440
+ with gr.Column(scale=2):
441
+ syllabus_file = gr.File(
442
+ file_types=[".docx", ".pdf", ".pptx"],
443
+ file_count="single",
444
+ height=160,
445
+ label="Upload additional Module 10 file (.docx/.pdf/.pptx) — optional",
446
+ interactive=False,
447
+ )
448
+ with gr.Column(scale=1):
449
+ doc_type = gr.Dropdown(
450
+ choices=DOC_TYPES,
451
+ value="Reading",
452
+ label="File type",
453
+ container=True,
454
+ interactive=False,
455
+ )
456
+ gr.HTML("<div style='height:5px'></div>")
457
+ docs_btn = gr.Button(
458
+ "📂 Loaded Docs", size="sm", variant="secondary", interactive=False
459
+ )
460
+ with gr.Column(scale=2):
461
+ with gr.Group(elem_classes="memory-line-box"):
462
+ gr.HTML(
463
+ f"""
464
+ <div style="font-weight:bold; font-size:14px; margin-bottom:5px;">
465
+ <span class="html-tooltip" data-tooltip="See User Guide for explanation">Memory Line</span>
466
+ </div>
467
+ <div style="position: relative; height: 35px; margin-top: 10px; margin-bottom: 5px;">
468
+ <div style="position: absolute; bottom: 5px; left: 0; width: 100%; height: 8px; background-color: #e5e7eb; border-radius: 4px;"></div>
469
+ <div style="position: absolute; bottom: 5px; left: 0; width: 40%; height: 8px; background-color: #8B1A1A; border-radius: 4px 0 0 4px;"></div>
470
+ <img src="{image_to_base64(CLARE_RUN_PATH)}" style="position: absolute; left: 36%; bottom: 8px; height: 35px; z-index: 10;">
471
+ </div>
472
+ <div style="display:flex; justify-content:space-between; align-items:center;">
473
+ <div style="font-size: 12px; color: #666;">Next Review: T+7</div>
474
+ <div style="font-size: 12px; color: #004a99; text-decoration:underline; cursor:pointer;">Report ⬇️</div>
475
+ </div>
476
+ """
477
+ )
478
+ review_btn = gr.Button(
479
+ "Review Now", size="sm", variant="primary", interactive=False
480
+ )
481
+ session_status = gr.Markdown(visible=False)
482
+
483
+ # Chat instruction(主聊天框上方提示 Module10 已预加载)
484
+ gr.Markdown(
485
+ """
486
+ <div style="background-color:#f9fafb; padding:10px; border-radius:5px; margin-top:10px; font-size:0.9em; color:#555;">
487
+ ✦ <b>Instruction:</b> This prototype is <b>pre-loaded</b> with <b>Module 10 – Responsible AI (Alto, 2024, Chapter 12)</b>.<br>
488
+ ✦ You do <b>not</b> need to upload files (uploads are optional).<br>
489
+ ✦ Please log in on the right before chatting with Clare.
490
+ </div>
491
+ """
492
+ )
493
+ chatbot = gr.Chatbot(
494
+ label="",
495
+ height=450,
496
+ avatar_images=(None, CLARE_LOGO_PATH),
497
+ show_label=False,
498
+ bubble_full_width=False,
499
+ )
500
+ user_input = gr.Textbox(
501
+ label="Your Input",
502
+ placeholder="Please log in on the right before asking Clare anything...",
503
+ show_label=False,
504
+ container=True,
505
+ autofocus=False,
506
+ interactive=False,
507
+ )
508
+
509
+ # === Right Sidebar ===
510
+ with gr.Column(scale=1, min_width=180):
511
+ with gr.Group(elem_classes="login-panel"):
512
+ gr.HTML(f"<img src='{image_to_base64(CLARE_READING_PATH)}'>")
513
+
514
+ with gr.Group(visible=True) as login_state_1:
515
+ login_start_btn = gr.Button(
516
+ "Student Login", elem_classes="login-main-btn"
517
+ )
518
+
519
+ with gr.Group(visible=False) as login_state_2:
520
+ name_input = gr.Textbox(
521
+ label="Student Name", placeholder="Name", container=True
522
+ )
523
+ id_input = gr.Textbox(
524
+ label="Email/ID", placeholder="ID", container=True
525
+ )
526
+ login_confirm_btn = gr.Button(
527
+ "Enter", variant="primary", size="sm"
528
+ )
529
+
530
+ with gr.Group(visible=False) as login_state_3:
531
+ student_info_html = gr.HTML()
532
+ logout_btn = gr.Button(
533
+ "Log out", elem_classes="logout-btn", size="sm"
534
+ )
535
+
536
+ gr.Markdown("### Actions")
537
+ export_btn = gr.Button(
538
+ "Export Conversation", size="sm", elem_classes="action-btn", interactive=False
539
+ )
540
+ quiz_btn = gr.Button(
541
+ "Let's Try (Micro-Quiz)", size="sm", elem_classes="action-btn", interactive=False
542
+ )
543
+ summary_btn = gr.Button(
544
+ "Summarization", size="sm", elem_classes="action-btn", interactive=False
545
+ )
546
+
547
+ gr.Markdown("### Results")
548
+ with gr.Group(elem_classes="result-box"):
549
+ result_display = gr.Markdown(
550
+ value="Results (export / summary) will appear here...",
551
+ label="Generated Content",
552
+ )
553
+
554
+ # ================== Login Flow ==================
555
+
556
+ def show_inputs():
557
+ return {
558
+ login_state_1: gr.update(visible=False),
559
+ login_state_2: gr.update(visible=True),
560
+ login_state_3: gr.update(visible=False),
561
+ }
562
+
563
+ login_start_btn.click(
564
+ show_inputs, outputs=[login_state_1, login_state_2, login_state_3]
565
+ )
566
+
567
+ def confirm_login(name, id_val):
568
+ if not name or not id_val:
569
+ return {
570
+ login_state_1: gr.update(),
571
+ login_state_2: gr.update(),
572
+ login_state_3: gr.update(),
573
+ student_info_html: gr.update(
574
+ value="<p style='color:red; font-size:12px;'>Please enter both Name and Email/ID to start.</p>"
575
+ ),
576
+ user_name_state: gr.update(),
577
+ user_id_state: gr.update(),
578
+ user_input: gr.update(interactive=False),
579
+ clear_btn: gr.update(interactive=False),
580
+ export_btn: gr.update(interactive=False),
581
+ quiz_btn: gr.update(interactive=False),
582
+ summary_btn: gr.update(interactive=False),
583
+ syllabus_file: gr.update(interactive=False),
584
+ doc_type: gr.update(interactive=False),
585
+ review_btn: gr.update(interactive=False),
586
+ language_preference: gr.update(interactive=False),
587
+ learning_mode: gr.update(interactive=False),
588
+ model_name: gr.update(interactive=False),
589
+ docs_btn: gr.update(interactive=False),
590
+ }
591
+
592
+ info_html = f"""
593
+ <div style="margin-bottom:10px;">
594
+ <div style="font-weight:bold; font-size:16px;">{name}</div>
595
+ <div style="color:#666; font-size:12px;">{id_val}</div>
596
+ </div>
597
+ """
598
+ return {
599
+ login_state_1: gr.update(visible=False),
600
+ login_state_2: gr.update(visible=False),
601
+ login_state_3: gr.update(visible=True),
602
+ student_info_html: gr.update(value=info_html),
603
+ user_name_state: name,
604
+ user_id_state: id_val,
605
+ user_input: gr.update(
606
+ interactive=True,
607
+ placeholder="Ask about Module 10 concepts, Responsible AI, or let Clare test you...",
608
+ ),
609
+ clear_btn: gr.update(interactive=True),
610
+ export_btn: gr.update(interactive=True),
611
+ quiz_btn: gr.update(interactive=True),
612
+ summary_btn: gr.update(interactive=True),
613
+ syllabus_file: gr.update(interactive=True),
614
+ doc_type: gr.update(interactive=True),
615
+ review_btn: gr.update(interactive=True),
616
+ language_preference: gr.update(interactive=True),
617
+ learning_mode: gr.update(interactive=True),
618
+ model_name: gr.update(interactive=False),
619
+ docs_btn: gr.update(interactive=True),
620
+ }
621
+
622
+ login_confirm_btn.click(
623
+ confirm_login,
624
+ inputs=[name_input, id_input],
625
+ outputs=[
626
+ login_state_1,
627
+ login_state_2,
628
+ login_state_3,
629
+ student_info_html,
630
+ user_name_state,
631
+ user_id_state,
632
+ user_input,
633
+ clear_btn,
634
+ export_btn,
635
+ quiz_btn,
636
+ summary_btn,
637
+ syllabus_file,
638
+ doc_type,
639
+ review_btn,
640
+ language_preference,
641
+ learning_mode,
642
+ model_name,
643
+ docs_btn,
644
+ ],
645
  )
646
+
647
+ def logout():
648
+ return {
649
+ login_state_1: gr.update(visible=True),
650
+ login_state_2: gr.update(visible=False),
651
+ login_state_3: gr.update(visible=False),
652
+ name_input: gr.update(value=""),
653
+ id_input: gr.update(value=""),
654
+ user_name_state: "",
655
+ user_id_state: "",
656
+ student_info_html: gr.update(value=""),
657
+ user_input: gr.update(
658
+ value="",
659
+ interactive=False,
660
+ placeholder="Please log in on the right before asking Clare anything...",
661
+ ),
662
+ clear_btn: gr.update(interactive=False),
663
+ export_btn: gr.update(interactive=False),
664
+ quiz_btn: gr.update(interactive=False),
665
+ summary_btn: gr.update(interactive=False),
666
+ syllabus_file: gr.update(interactive=False),
667
+ doc_type: gr.update(interactive=False),
668
+ review_btn: gr.update(interactive=False),
669
+ language_preference: gr.update(interactive=False),
670
+ learning_mode: gr.update(interactive=False),
671
+ docs_btn: gr.update(interactive=False),
672
+ }
673
+
674
+ logout_btn.click(
675
+ logout,
676
+ outputs=[
677
+ login_state_1,
678
+ login_state_2,
679
+ login_state_3,
680
+ name_input,
681
+ id_input,
682
+ user_name_state,
683
+ user_id_state,
684
+ student_info_html,
685
+ user_input,
686
+ clear_btn,
687
+ export_btn,
688
+ quiz_btn,
689
+ summary_btn,
690
+ syllabus_file,
691
+ doc_type,
692
+ review_btn,
693
+ language_preference,
694
+ learning_mode,
695
+ docs_btn,
696
+ ],
697
+ )
698
+
699
+ # ================== Main Logic ==================
700
+
701
+ def update_course_and_rag(file, doc_type_val):
702
+ """
703
+ 上传的文件会在 Module10 预加载的基础上「追加」,而不是替换。
704
+ """
705
+ local_topics = preloaded_topics or []
706
+ local_chunks = preloaded_chunks or []
707
+
708
+ if file is not None:
709
+ try:
710
+ topics = extract_course_topics_from_file(file, doc_type_val)
711
+ except Exception:
712
+ topics = []
713
+ try:
714
+ chunks = build_rag_chunks_from_file(file, doc_type_val)
715
+ except Exception:
716
+ chunks = []
717
+
718
+ local_topics = (preloaded_topics or []) + (topics or [])
719
+ local_chunks = (preloaded_chunks or []) + (chunks or [])
720
+
721
+ status_md = (
722
+ f"✅ **Loaded Module 10 base reading + uploaded {doc_type_val} file.**\n\n"
723
+ "Both will be used for explanations and quizzes."
724
+ )
725
+ else:
726
+ status_md = (
727
+ "�� **Using pre-loaded Module 10 reading only.**\n\n"
728
+ "You may optionally upload additional Module 10 materials."
729
+ )
730
+
731
+ return local_topics, local_chunks, status_md
732
+
733
+ syllabus_file.change(
734
+ update_course_and_rag,
735
+ [syllabus_file, doc_type],
736
+ [course_outline_state, rag_chunks_state, session_status],
737
+ )
738
+
739
+ def show_loaded_docs(doc_type_val):
740
+ gr.Info(
741
+ f"For this experiment, Clare always includes the pre-loaded Module 10 reading.\n"
742
+ f"Additional uploaded {doc_type_val} files will be used as supplementary context.",
743
+ title="Loaded Documents",
744
+ )
745
+
746
+ docs_btn.click(show_loaded_docs, inputs=[doc_type])
747
+
748
+ def respond(
749
+ message,
750
+ chat_history,
751
+ course_outline,
752
+ weaknesses,
753
+ cognitive_state,
754
+ rag_chunks,
755
+ model_name_val,
756
+ lang_pref,
757
+ mode_val,
758
+ doc_type_val,
759
+ user_id_val,
760
+ ):
761
+ # 双保险:没登录就提示
762
+ if not user_id_val:
763
+ out_msg = (
764
+ "🔒 Please log in with your Student Name and Email/ID on the right "
765
+ "before using Clare."
766
+ )
767
+ new_history = (chat_history or []) + [[message, out_msg]]
768
+ new_status = render_session_status(
769
+ mode_val or "Concept Explainer",
770
+ weaknesses or [],
771
+ cognitive_state or {"confusion": 0, "mastery": 0},
772
+ )
773
+ return "", new_history, weaknesses, cognitive_state, new_status
774
+
775
+ resolved_lang = detect_language(message or "", lang_pref)
776
+
777
+ if not message or not message.strip():
778
+ new_status = render_session_status(
779
+ mode_val or "Concept Explainer",
780
+ weaknesses or [],
781
+ cognitive_state or {"confusion": 0, "mastery": 0},
782
+ )
783
+ return "", chat_history, weaknesses, cognitive_state, new_status
784
+
785
+ weaknesses = update_weaknesses_from_message(message, weaknesses or [])
786
+ cognitive_state = update_cognitive_state_from_message(message, cognitive_state)
787
+
788
+ # RAG 始终基于 (预加载 Module10 + 可选上传)
789
+ rag_context = retrieve_relevant_chunks(message, rag_chunks or [])
790
+
791
+ start_ts = time.time()
792
+ answer, new_history = chat_with_clare(
793
+ message=message,
794
+ history=chat_history,
795
+ model_name=model_name_val,
796
+ language_preference=resolved_lang,
797
+ learning_mode=mode_val,
798
+ doc_type=doc_type_val,
799
+ course_outline=course_outline,
800
+ weaknesses=weaknesses,
801
+ cognitive_state=cognitive_state,
802
+ rag_context=rag_context,
803
+ )
804
+ end_ts = time.time()
805
+ latency_ms = (end_ts - start_ts) * 1000.0
806
+
807
+ student_id = user_id_val or "ANON"
808
+ experiment_id = "RESP_AI_W10"
809
+
810
+ try:
811
+ log_event(
812
+ {
813
+ "experiment_id": experiment_id,
814
+ "student_id": student_id,
815
+ "event_type": "chat_turn",
816
+ "timestamp": end_ts,
817
+ "latency_ms": latency_ms,
818
+ "question": message,
819
+ "answer": answer,
820
+ "model_name": model_name_val,
821
+ "language": resolved_lang,
822
+ "learning_mode": mode_val,
823
+ }
824
+ )
825
+ except Exception as e:
826
+ print("log_event error:", e)
827
+
828
+ new_status = render_session_status(mode_val, weaknesses, cognitive_state)
829
+ return "", new_history, weaknesses, cognitive_state, new_status
830
+
831
+ user_input.submit(
832
+ respond,
833
+ [
834
+ user_input,
835
+ chatbot,
836
+ course_outline_state,
837
+ weakness_state,
838
+ cognitive_state_state,
839
+ rag_chunks_state,
840
+ model_name,
841
+ language_preference,
842
+ learning_mode,
843
+ doc_type,
844
+ user_id_state,
845
+ ],
846
+ [user_input, chatbot, weakness_state, cognitive_state_state, session_status],
847
+ )
848
+
849
+ # ===== Micro-Quiz: 直接在主 Chatbot 里进行 =====
850
+ def start_micro_quiz(
851
+ chat_history,
852
+ course_outline,
853
+ weaknesses,
854
+ cognitive_state,
855
+ rag_chunks,
856
+ model_name_val,
857
+ lang_pref,
858
+ mode_val,
859
+ doc_type_val,
860
+ user_id_val,
861
+ ):
862
+ if not user_id_val:
863
+ gr.Info("Please log in first to start a micro-quiz.", title="Login required")
864
+ return (
865
+ chat_history,
866
+ weaknesses,
867
+ cognitive_state,
868
+ render_session_status(
869
+ mode_val or "Concept Explainer",
870
+ weaknesses or [],
871
+ cognitive_state or {"confusion": 0, "mastery": 0},
872
+ ),
873
+ )
874
+
875
+ quiz_instruction = (
876
+ "We are running a short micro-quiz session based ONLY on **Module 10 – "
877
+ "Responsible AI (Alto, 2024, Chapter 12)** and the pre-loaded materials.\n\n"
878
+ "From now on, please:\n"
879
+ "1) Ask me ONE quiz question at a time, based on Module 10 concepts "
880
+ "(Responsible AI definition, risk types, mitigation layers, EU AI Act, etc.).\n"
881
+ "2) Do NOT reveal the answer when you ask the question.\n"
882
+ "3) When I reply, grade my answer, explain briefly, and then ask if I want another question.\n"
883
+ "4) Continue this pattern until I explicitly say to stop.\n\n"
884
+ "Please now send the FIRST quiz question only."
885
+ )
886
+
887
+ resolved_lang = lang_pref
888
+
889
+ start_ts = time.time()
890
+ answer, new_history = chat_with_clare(
891
+ message=quiz_instruction,
892
+ history=chat_history,
893
+ model_name=model_name_val,
894
+ language_preference=resolved_lang,
895
+ learning_mode=mode_val,
896
+ doc_type=doc_type_val,
897
+ course_outline=course_outline,
898
+ weaknesses=weaknesses,
899
+ cognitive_state=cognitive_state,
900
+ rag_context=retrieve_relevant_chunks(
901
+ "Module 10 quiz", rag_chunks or []
902
+ ),
903
+ )
904
+ end_ts = time.time()
905
+ latency_ms = (end_ts - start_ts) * 1000.0
906
+
907
+ student_id = user_id_val or "ANON"
908
+ experiment_id = "RESP_AI_W10"
909
+
910
+ try:
911
+ log_event(
912
+ {
913
+ "experiment_id": experiment_id,
914
+ "student_id": student_id,
915
+ "event_type": "micro_quiz_start",
916
+ "timestamp": end_ts,
917
+ "latency_ms": latency_ms,
918
+ "question": quiz_instruction,
919
+ "answer": answer,
920
+ "model_name": model_name_val,
921
+ "language": resolved_lang,
922
+ "learning_mode": mode_val,
923
+ }
924
+ )
925
+ except Exception as e:
926
+ print("log_event error:", e)
927
+
928
+ new_status = render_session_status(mode_val, weaknesses, cognitive_state)
929
+ return new_history, weaknesses, cognitive_state, new_status
930
+
931
+ quiz_btn.click(
932
+ start_micro_quiz,
933
+ [
934
+ chatbot,
935
+ course_outline_state,
936
+ weakness_state,
937
+ cognitive_state_state,
938
+ rag_chunks_state,
939
+ model_name,
940
+ language_preference,
941
+ learning_mode,
942
+ doc_type,
943
+ user_id_state,
944
+ ],
945
+ [chatbot, weakness_state, cognitive_state_state, session_status],
946
+ )
947
+
948
+ # ===== Export / Summary =====
949
+ export_btn.click(
950
+ lambda h, c, m, w, cog: export_conversation(h, c, m, w, cog),
951
+ [chatbot, course_outline_state, learning_mode, weakness_state, cognitive_state_state],
952
+ [result_display],
953
+ )
954
+
955
+ summary_btn.click(
956
+ lambda h, c, w, cog, m, l: summarize_conversation(
957
+ h, c, w, cog, m, l
958
+ ),
959
+ [
960
+ chatbot,
961
+ course_outline_state,
962
+ weakness_state,
963
+ cognitive_state_state,
964
+ model_name,
965
+ language_preference,
966
+ ],
967
+ [result_display],
968
+ )
969
+
970
+ # ===== Reset Conversation =====
971
+ def clear_all():
972
+ empty_state = {"confusion": 0, "mastery": 0}
973
+ default_status = render_session_status("Concept Explainer", [], empty_state)
974
+ return [], [], empty_state, [], "", default_status
975
+
976
+ clear_btn.click(
977
+ clear_all,
978
+ None,
979
+ [
980
+ chatbot,
981
+ weakness_state,
982
+ cognitive_state_state,
983
+ rag_chunks_state,
984
+ result_display,
985
+ session_status,
986
+ ],
987
+ queue=False,
988
+ )
989
+
990
+ if __name__ == "__main__":
991
+ demo.launch()