Spaces:
Sleeping
Sleeping
| import os | |
| import time | |
| import base64 | |
| from collections import defaultdict | |
| from typing import List, Dict | |
| import gradio as gr | |
| from langsmith import Client # LangSmith 客户端 | |
| from config import ( | |
| DEFAULT_MODEL, | |
| DEFAULT_COURSE_TOPICS, | |
| LEARNING_MODES, | |
| DOC_TYPES, | |
| ) | |
| from clare_core import ( | |
| update_weaknesses_from_message, | |
| update_cognitive_state_from_message, | |
| render_session_status, | |
| find_similar_past_question, | |
| detect_language, | |
| chat_with_clare, | |
| export_conversation, | |
| generate_quiz_from_history, | |
| get_empty_input_prompt, | |
| summarize_conversation, | |
| ) | |
| from rag_engine import ( | |
| build_rag_chunks_from_file, | |
| retrieve_relevant_chunks, | |
| ) | |
| from syllabus_utils import extract_course_topics_from_file | |
| # ================== Assets ================== | |
| CLARE_LOGO_PATH = "clare_mascot.png" | |
| CLARE_RUN_PATH = "Clare_Run.png" | |
| CLARE_READING_PATH = "Clare_reading.png" # 确保存在 | |
| # ================== Base64 Helper ================== | |
| def image_to_base64(image_path: str) -> str: | |
| if not os.path.exists(image_path): | |
| return "" | |
| with open(image_path, "rb") as img_file: | |
| encoded_string = base64.b64encode(img_file.read()).decode("utf-8") | |
| if image_path.lower().endswith(".png"): | |
| mime = "image/png" | |
| elif image_path.lower().endswith((".jpg", ".jpeg")): | |
| mime = "image/jpeg" | |
| else: | |
| mime = "image/png" | |
| return f"data:{mime};base64,{encoded_string}" | |
| # ================== User Guide Content ================== | |
| USER_GUIDE_SECTIONS = { | |
| "getting_started": """ | |
| Welcome to **Clare — Your Personalized AI Tutor**. | |
| For this controlled experiment, Clare is already pre-loaded with: | |
| 📘 **Module 10 Reading – Responsible AI (Alto, 2024, Chapter 12)** | |
| You do **NOT** need to upload any materials. | |
| You may optionally upload extra files, but Clare will always include the Module 10 reading as core context. | |
| **To begin:** | |
| 1. Log in with your **Student Name + Email/ID** on the right. | |
| 2. Select your **Learning Mode** on the left. | |
| 3. (Optional) Upload additional Module 10 slides / notes at the top. | |
| 4. Ask Clare any question about **Module 10 – Responsible AI**. | |
| """, | |
| "mode_definition": """ | |
| Clare offers different teaching modes to match how you prefer to learn. | |
| ### Concept Explainer | |
| Clear, structured explanations with examples — ideal for learning new topics. | |
| ### Socratic Tutor | |
| Clare asks guiding questions instead of giving direct answers. | |
| Helps you build reasoning and problem-solving skills. | |
| ### Exam Prep / Quiz | |
| Generates short practice questions aligned with your course week. | |
| Useful for self-testing and preparing for exams. | |
| ### Assignment Helper | |
| Helps you interpret assignment prompts, plan structure, and understand requirements. | |
| ❗ Clare does **not** produce full assignment answers (academic integrity). | |
| ### Quick Summary | |
| Gives brief summaries of slides, reading materials, or long questions. | |
| """, | |
| "how_clare_works": """ | |
| Clare combines **course context + learning science + AI reasoning** to generate answers. | |
| For this experiment, Clare always includes: | |
| - Module 10 Reading – Responsible AI (Alto, 2024, Chapter 12) | |
| - Any additional Module 10 files you upload | |
| Clare uses: | |
| - **Learning Mode**: tone, depth, and interaction style. | |
| - **Reinforcement model**: may prioritize concepts you’re likely to forget. | |
| - **Responsible AI principles**: avoids harmful output and preserves academic integrity. | |
| """, | |
| "memory_line": """ | |
| **Memory Line** is a visualization of your *learning reinforcement cycle*. | |
| Based on the **forgetting-curve model**, Clare organizes your review topics into: | |
| - **T+0 (Current Week)** – new concepts | |
| - **T+7** – first spaced review | |
| - **T+14** – reinforcement review | |
| - **T+30** – long-term consolidation | |
| In this experiment, Memory Line should be interpreted as your **Module 10** reinforcement status. | |
| """, | |
| "learning_progress": """ | |
| The Learning Progress Report highlights: | |
| - **Concepts mastered** | |
| - **Concepts in progress** | |
| - **Concepts due for review** | |
| - Your recent **micro-quiz results** | |
| - Suggested **next-step topics** | |
| """, | |
| "how_files": """ | |
| Your uploaded materials help Clare: | |
| - Align explanations with your exact course (here: **Module 10 – Responsible AI**) | |
| - Use terminology consistent with your professor | |
| - Improve factual accuracy | |
| 🔒 **Privacy** | |
| - Files are used only within your session | |
| - They are not kept as permanent training data | |
| Accepted formats: **.docx / .pdf / .pptx** | |
| For this experiment, Clare is **already pre-loaded** with the Module 10 reading. Uploads are optional. | |
| """, | |
| "micro_quiz": """ | |
| The **Micro-Quiz** function provides a: | |
| - 1-minute self-check | |
| - 1–3 questions about **Module 10 – Responsible AI** | |
| - Instant feedback inside the main chat | |
| **How it works:** | |
| 1. Click “Let’s Try (Micro-Quiz)” on the right. | |
| 2. Clare will send the **first quiz question** in the main chat. | |
| 3. Type your answer in the chat box. | |
| 4. Clare will: | |
| - Judge correctness | |
| - Give a brief explanation | |
| - Ask if you want another question | |
| 5. You can continue or say “stop” at any time. | |
| """, | |
| "summarization": """ | |
| Clare can summarize: | |
| - Module 10 reading | |
| - Uploaded slides / notes | |
| - Long conversation threads | |
| """, | |
| "export_conversation": """ | |
| You can export your chat session for: | |
| - Study review | |
| - Exam preparation | |
| - Saving important explanations | |
| Export format: **Markdown / plain text**. | |
| """, | |
| "faq": """ | |
| **Q: Does Clare give assignment answers?** | |
| No. Clare assists with understanding and planning but does **not** generate full solutions. | |
| **Q: Does Clare replace lectures or TA office hours?** | |
| No. Clare supplements your learning by providing on-demand guidance. | |
| **Q: What languages does Clare support?** | |
| Currently: English & 简体中文. | |
| """ | |
| } | |
| # ================== CSS 样式表 ================== | |
| CUSTOM_CSS = """ | |
| /* --- Main Header --- */ | |
| .header-container { padding: 10px 20px; background-color: #ffffff; border-bottom: 2px solid #f3f4f6; margin-bottom: 15px; display: flex; align-items: center; } | |
| /* --- Sidebar Login Panel --- */ | |
| .login-panel { | |
| background-color: #e5e7eb; | |
| padding: 15px; | |
| border-radius: 8px; | |
| text-align: center; | |
| margin-bottom: 20px; | |
| } | |
| .login-panel img { | |
| display: block; | |
| margin: 0 auto 10px auto; | |
| height: 80px; | |
| object-fit: contain; | |
| } | |
| .login-main-btn { | |
| background-color: #ffffff !important; | |
| color: #000 !important; | |
| border: 1px solid #000 !important; | |
| font-weight: bold !important; | |
| } | |
| .logout-btn { | |
| background-color: #6b2828 !important; | |
| color: #fff !important; | |
| border: none !important; | |
| font-weight: bold !important; | |
| } | |
| /* User Guide */ | |
| .main-user-guide { border: none !important; background: transparent !important; box-shadow: none !important; } | |
| .main-user-guide > .label-wrap { border: none !important; background: transparent !important; padding: 10px 0 !important; } | |
| .main-user-guide > .label-wrap span { font-size: 1.3rem !important; font-weight: 800 !important; color: #111827 !important; } | |
| .clean-accordion { border: none !important; background: transparent !important; box-shadow: none !important; margin-bottom: 0px !important; padding: 0 !important; border-radius: 0 !important; } | |
| .clean-accordion > .label-wrap { padding: 8px 5px !important; border: none !important; background: transparent !important; border-bottom: 1px solid #e5e7eb !important; } | |
| .clean-accordion > .label-wrap span { font-size: 0.9rem !important; font-weight: 500 !important; color: #374151 !important; } | |
| .clean-accordion > .label-wrap:hover { background-color: #f9fafb !important; } | |
| /* Action Buttons */ | |
| .action-btn { font-weight: bold !important; font-size: 0.9rem !important; position: relative; overflow: visible !important; } | |
| .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; } | |
| .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; } | |
| /* Tooltips & Memory Line */ | |
| .html-tooltip { border-bottom: 1px dashed #999; cursor: help; position: relative; } | |
| .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; } | |
| .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; } | |
| /* Results Box Style */ | |
| .result-box { border: 1px solid #e5e7eb; background: #ffffff; padding: 10px; border-radius: 8px; height: 100%; } | |
| .result-box .prose { font-size: 0.9rem; } | |
| @keyframes fadeIn { to { opacity: 1; } } | |
| """ | |
| # ========== Preload Module 10 PDF ========== | |
| MODULE10_PATH = "module10_responsible_ai.pdf" | |
| MODULE10_DOC_TYPE = "Literature Review / Paper" | |
| preloaded_topics: List[str] = [] | |
| preloaded_chunks: List[Dict] = [] | |
| if os.path.exists(MODULE10_PATH): | |
| try: | |
| preloaded_topics = extract_course_topics_from_file( | |
| MODULE10_PATH, MODULE10_DOC_TYPE | |
| ) | |
| preloaded_chunks = build_rag_chunks_from_file( | |
| MODULE10_PATH, MODULE10_DOC_TYPE | |
| ) | |
| print("Module 10 PDF preloaded successfully.") | |
| except Exception as e: | |
| print("Module 10 preload failed:", e) | |
| else: | |
| print("Module 10 PDF not found at path:", MODULE10_PATH) | |
| # ===== LangSmith logging ===== | |
| ls_client = Client() | |
| LS_DATASET_NAME = "clare_user_events" | |
| def log_event(data: Dict): | |
| """ | |
| 把日志写入 LangSmith Dataset (clare_user_events) | |
| """ | |
| try: | |
| inputs = { | |
| "question": data.get("question"), | |
| "student_id": data.get("student_id"), | |
| } | |
| # ✅ event_type 等字段作为 metadata,这样在 Dataset 列表里能直接看到 / 过滤 | |
| metadata = {k: v for k, v in data.items() if k not in ("question", "answer")} | |
| ls_client.create_example( | |
| inputs=inputs, | |
| outputs={"answer": data.get("answer")}, | |
| metadata=metadata, | |
| dataset_name=LS_DATASET_NAME, | |
| ) | |
| except Exception as e: | |
| print("LangSmith log failed:", e) | |
| # ===== Reference Formatting Helper ===== | |
| def format_references( | |
| rag_chunks: List[Dict], max_files: int = 2, max_sections_per_file: int = 3 | |
| ) -> str: | |
| if not rag_chunks: | |
| return "" | |
| refs_by_file: Dict[str, List[str]] = defaultdict(list) | |
| for chunk in rag_chunks: | |
| file_name = chunk.get("source_file") or "module10_responsible_ai.pdf" | |
| section = chunk.get("section") or "Related section" | |
| if section not in refs_by_file[file_name]: | |
| refs_by_file[file_name].append(section) | |
| if not refs_by_file: | |
| return "" | |
| lines = ["**References (RAG context used):**"] | |
| for i, (file_name, sections) in enumerate(refs_by_file.items()): | |
| if i >= max_files: | |
| break | |
| short_sections = sections[:max_sections_per_file] | |
| if short_sections: | |
| section_str = "; ".join(short_sections) | |
| lines.append(f"- *{file_name}* — {section_str}") | |
| else: | |
| lines.append(f"- *{file_name}*") | |
| if len(lines) == 1: | |
| return "" | |
| return "\n".join(lines) | |
| def is_academic_query(message: str) -> bool: | |
| if not message: | |
| return False | |
| m = message.strip().lower() | |
| if not m: | |
| return False | |
| m = " ".join(m.split()) | |
| smalltalk_tokens = { | |
| "hi", "hello", "hey", "yo", | |
| "thanks", "thank", "thank you", | |
| "ok", "okay", | |
| "bye", "goodbye", "see you", | |
| "haha", "lol" | |
| } | |
| tokens = m.split() | |
| if "?" not in m and all(t in smalltalk_tokens for t in tokens): | |
| return False | |
| meta_phrases = [ | |
| "who are you", | |
| "what are you", | |
| "what is your name", | |
| "introduce yourself", | |
| "tell me about yourself", | |
| "what can you do", | |
| "how can you help", | |
| "how do you help", | |
| "how do i use", | |
| "how to use this", | |
| "what is this app", | |
| "what is this tool", | |
| "what is clare", | |
| "who is clare", | |
| ] | |
| if any(p in m for p in meta_phrases): | |
| return False | |
| if len(tokens) <= 2 and "?" not in m: | |
| return False | |
| return True | |
| # ================== Gradio App ================== | |
| with gr.Blocks( | |
| title="Clare – Hanbridge AI Teaching Assistant", css=CUSTOM_CSS | |
| ) as demo: | |
| # 全局状态 | |
| course_outline_state = gr.State(preloaded_topics or DEFAULT_COURSE_TOPICS) | |
| weakness_state = gr.State([]) | |
| cognitive_state_state = gr.State({"confusion": 0, "mastery": 0}) | |
| rag_chunks_state = gr.State(preloaded_chunks or []) | |
| last_question_state = gr.State("") | |
| last_answer_state = gr.State("") | |
| user_name_state = gr.State("") | |
| user_id_state = gr.State("") | |
| # ✅ 当前“最近一次回答”是否已经被点赞/点踩(只允许一次) | |
| feedback_used_state = gr.State(False) | |
| # --- Header --- | |
| with gr.Row(elem_classes="header-container"): | |
| with gr.Column(scale=3): | |
| gr.HTML( | |
| f""" | |
| <div style="display:flex; align-items:center; gap: 20px;"> | |
| <img src="{image_to_base64(CLARE_LOGO_PATH)}" style="height: 75px; object-fit: contain;"> | |
| <div style="display:flex; flex-direction:column;"> | |
| <div style="font-size: 32px; font-weight: 800; line-height: 1.1; color: #000;"> | |
| Clare | |
| <span style="font-size: 18px; font-weight: 600; margin-left: 10px;">Your Personalized AI Tutor</span> | |
| </div> | |
| <div style="font-size: 14px; font-style: italic; color: #333; margin-top: 4px;"> | |
| Personalized guidance, review, and intelligent reinforcement | |
| </div> | |
| </div> | |
| </div> | |
| """ | |
| ) | |
| # --- Main Layout --- | |
| with gr.Row(): | |
| # === Left Sidebar === | |
| with gr.Column(scale=1, min_width=200): | |
| clear_btn = gr.Button( | |
| "Reset Conversation", variant="stop", interactive=False | |
| ) | |
| gr.Markdown("### Model Settings") | |
| model_name = gr.Textbox( | |
| label="Model", | |
| value="gpt-4.1-mini", | |
| interactive=False, | |
| lines=1, | |
| ) | |
| language_preference = gr.Radio( | |
| choices=["Auto", "English", "简体中文"], | |
| value="Auto", | |
| label="Language", | |
| interactive=False, | |
| ) | |
| learning_mode = gr.Radio( | |
| choices=LEARNING_MODES, | |
| value="Concept Explainer", | |
| label="Learning Mode", | |
| info="See User Guide for mode definition details.", | |
| interactive=False, | |
| ) | |
| with gr.Accordion( | |
| "User Guide", open=True, elem_classes="main-user-guide" | |
| ): | |
| with gr.Accordion( | |
| "Getting Started", | |
| open=False, | |
| elem_classes="clean-accordion", | |
| ): | |
| gr.Markdown(USER_GUIDE_SECTIONS["getting_started"]) | |
| with gr.Accordion( | |
| "Mode Definition", | |
| open=False, | |
| elem_classes="clean-accordion", | |
| ): | |
| gr.Markdown(USER_GUIDE_SECTIONS["mode_definition"]) | |
| with gr.Accordion( | |
| "How Clare Works", | |
| open=False, | |
| elem_classes="clean-accordion", | |
| ): | |
| gr.Markdown(USER_GUIDE_SECTIONS["how_clare_works"]) | |
| with gr.Accordion( | |
| "What is Memory Line", | |
| open=False, | |
| elem_classes="clean-accordion", | |
| ): | |
| gr.Markdown(USER_GUIDE_SECTIONS["memory_line"]) | |
| with gr.Accordion( | |
| "Learning Progress Report", | |
| open=False, | |
| elem_classes="clean-accordion", | |
| ): | |
| gr.Markdown(USER_GUIDE_SECTIONS["learning_progress"]) | |
| with gr.Accordion( | |
| "How Clare Uses Your Files", | |
| open=False, | |
| elem_classes="clean-accordion", | |
| ): | |
| gr.Markdown(USER_GUIDE_SECTIONS["how_files"]) | |
| with gr.Accordion( | |
| "Micro-Quiz", open=False, elem_classes="clean-accordion" | |
| ): | |
| gr.Markdown(USER_GUIDE_SECTIONS["micro_quiz"]) | |
| with gr.Accordion( | |
| "Summarization", | |
| open=False, | |
| elem_classes="clean-accordion", | |
| ): | |
| gr.Markdown(USER_GUIDE_SECTIONS["summarization"]) | |
| with gr.Accordion( | |
| "Export Conversation", | |
| open=False, | |
| elem_classes="clean-accordion", | |
| ): | |
| gr.Markdown(USER_GUIDE_SECTIONS["export_conversation"]) | |
| with gr.Accordion( | |
| "FAQ", open=False, elem_classes="clean-accordion" | |
| ): | |
| gr.Markdown(USER_GUIDE_SECTIONS["faq"]) | |
| gr.Markdown("---") | |
| gr.Button("System Settings", size="sm", variant="secondary", interactive=False) | |
| gr.HTML( | |
| """ | |
| <div style="font-size: 11px; color: #9ca3af; margin-top: 15px; text-align: left;"> | |
| © 2025 Made by <a href="https://www.linkedin.com/in/qinghua-xia-479199252/" target="_blank" style="color: #6b7280; text-decoration: underline;">Sarah Xia</a> | |
| </div> | |
| """ | |
| ) | |
| # === Center Main === | |
| with gr.Column(scale=3): | |
| gr.Markdown( | |
| """ | |
| <div style="background-color:#f9fafb; padding:10px; border-radius:5px; margin-top:10px; font-size:0.9em; color:#555;"> | |
| ✦ <b>Instruction:</b> This prototype is <b>pre-loaded</b> with <b>Module 10 – Responsible AI (Alto, 2024, Chapter 12)</b>.<br> | |
| ✦ You do <b>not</b> need to upload files (uploads are optional).<br> | |
| ✦ Please log in on the right before chatting with Clare. | |
| </div> | |
| """ | |
| ) | |
| chatbot = gr.Chatbot( | |
| label="", | |
| height=450, | |
| avatar_images=(None, CLARE_LOGO_PATH), | |
| show_label=False, | |
| type="tuples", | |
| ) | |
| # Rating bar (last answer) | |
| gr.Markdown("#### Rate Clare’s last answer") | |
| with gr.Row(): | |
| thumb_up_btn = gr.Button( | |
| "👍 Helpful", size="sm", interactive=False | |
| ) | |
| thumb_down_btn = gr.Button( | |
| "👎 Not helpful", size="sm", interactive=False | |
| ) | |
| feedback_toggle_btn = gr.Button( | |
| "Give detailed feedback", size="sm", variant="secondary", interactive=False | |
| ) | |
| feedback_text = gr.Textbox( | |
| label="What worked well or what was wrong?", | |
| placeholder="Optional: describe what you liked / what was confusing or incorrect.", | |
| lines=3, | |
| visible=False, | |
| ) | |
| feedback_submit_btn = gr.Button( | |
| "Submit Feedback", size="sm", variant="primary", visible=False, interactive=False | |
| ) | |
| user_input = gr.Textbox( | |
| label="Your Input", | |
| placeholder="Please log in on the right before asking Clare anything...", | |
| show_label=False, | |
| container=True, | |
| autofocus=False, | |
| interactive=False, | |
| ) | |
| with gr.Row(): | |
| with gr.Column(scale=2): | |
| syllabus_file = gr.File( | |
| file_types=[".docx", ".pdf", ".pptx"], | |
| file_count="single", | |
| height=160, | |
| label="Upload additional Module 10 file (.docx/.pdf/.pptx) — optional", | |
| interactive=False, | |
| ) | |
| with gr.Column(scale=1): | |
| doc_type = gr.Dropdown( | |
| choices=DOC_TYPES, | |
| value="Syllabus", | |
| label="File type", | |
| container=True, | |
| interactive=False, | |
| ) | |
| gr.HTML("<div style='height:5px'></div>") | |
| docs_btn = gr.Button( | |
| "📂 Loaded Docs", | |
| size="sm", | |
| variant="secondary", | |
| interactive=False, | |
| ) | |
| with gr.Column(scale=2): | |
| with gr.Group(elem_classes="memory-line-box"): | |
| gr.HTML( | |
| f""" | |
| <div style="font-weight:bold; font-size:14px; margin-bottom:5px;"> | |
| <span class="html-tooltip" data-tooltip="See User Guide for explanation">Memory Line</span> | |
| </div> | |
| <div style="position: relative; height: 35px; margin-top: 10px; margin-bottom: 5px;"> | |
| <div style="position: absolute; bottom: 5px; left: 0; width: 100%; height: 8px; background-color: #e5e7eb; border-radius: 4px;"></div> | |
| <div style="position: absolute; bottom: 5px; left: 0; width: 40%; height: 8px; background-color: #8B1A1A; border-radius: 4px 0 0 4px;"></div> | |
| <img src="{image_to_base64(CLARE_RUN_PATH)}" style="position: absolute; left: 36%; bottom: 8px; height: 35px; z-index: 10;"> | |
| </div> | |
| <div style="display:flex; justify-content:space-between; align-items:center;"> | |
| <div style="font-size: 12px; color: #666;">Next Review: T+7</div> | |
| <div style="font-size: 12px; color: #004a99; text-decoration:underline; cursor:pointer;">Report ⬇️</div> | |
| </div> | |
| """ | |
| ) | |
| review_btn = gr.Button( | |
| "Review Now", | |
| size="sm", | |
| variant="primary", | |
| interactive=False, | |
| ) | |
| session_status = gr.Markdown(visible=False) | |
| # === Right Sidebar === | |
| with gr.Column(scale=1, min_width=180): | |
| with gr.Group(elem_classes="login-panel"): | |
| gr.HTML(f"<img src='{image_to_base64(CLARE_READING_PATH)}'>") | |
| with gr.Group(visible=True) as login_state_1: | |
| login_start_btn = gr.Button( | |
| "Student Login", elem_classes="login-main-btn" | |
| ) | |
| with gr.Group(visible=False) as login_state_2: | |
| name_input = gr.Textbox( | |
| label="Student Name", placeholder="Name", container=True | |
| ) | |
| id_input = gr.Textbox( | |
| label="Email/ID", placeholder="ID", container=True | |
| ) | |
| login_confirm_btn = gr.Button( | |
| "Enter", variant="primary", size="sm" | |
| ) | |
| with gr.Group(visible=False) as login_state_3: | |
| student_info_html = gr.HTML() | |
| logout_btn = gr.Button( | |
| "Log out", elem_classes="logout-btn", size="sm" | |
| ) | |
| gr.Markdown("### Actions") | |
| export_btn = gr.Button( | |
| "Export Conversation", size="sm", elem_classes="action-btn", interactive=False | |
| ) | |
| quiz_btn = gr.Button( | |
| "Let's Try (Micro-Quiz)", size="sm", elem_classes="action-btn", interactive=False | |
| ) | |
| summary_btn = gr.Button( | |
| "Summarization", size="sm", elem_classes="action-btn", interactive=False | |
| ) | |
| gr.Markdown("### Results") | |
| with gr.Group(elem_classes="result-box"): | |
| result_display = gr.Markdown( | |
| value="Results (export / summary) will appear here...", | |
| label="Generated Content", | |
| ) | |
| # ================== Login Flow ================== | |
| def show_inputs(): | |
| return { | |
| login_state_1: gr.update(visible=False), | |
| login_state_2: gr.update(visible=True), | |
| login_state_3: gr.update(visible=False), | |
| } | |
| login_start_btn.click( | |
| show_inputs, outputs=[login_state_1, login_state_2, login_state_3] | |
| ) | |
| def confirm_login(name, id_val): | |
| if not name or not id_val: | |
| return { | |
| login_state_1: gr.update(), | |
| login_state_2: gr.update(), | |
| login_state_3: gr.update(), | |
| student_info_html: gr.update( | |
| value="<p style='color:red; font-size:12px;'>Please enter both Name and Email/ID to start.</p>" | |
| ), | |
| user_name_state: gr.update(), | |
| user_id_state: gr.update(), | |
| feedback_used_state: False, | |
| user_input: gr.update(interactive=False), | |
| clear_btn: gr.update(interactive=False), | |
| export_btn: gr.update(interactive=False), | |
| quiz_btn: gr.update(interactive=False), | |
| summary_btn: gr.update(interactive=False), | |
| syllabus_file: gr.update(interactive=False), | |
| doc_type: gr.update(interactive=False), | |
| review_btn: gr.update(interactive=False), | |
| language_preference: gr.update(interactive=False), | |
| learning_mode: gr.update(interactive=False), | |
| model_name: gr.update(interactive=False), | |
| docs_btn: gr.update(interactive=False), | |
| thumb_up_btn: gr.update(interactive=False, value="👍 Helpful"), | |
| thumb_down_btn: gr.update(interactive=False, value="👎 Not helpful"), | |
| feedback_toggle_btn: gr.update(interactive=False), | |
| feedback_text: gr.update(visible=False, value=""), | |
| feedback_submit_btn: gr.update(interactive=False, visible=False), | |
| } | |
| info_html = f""" | |
| <div style="margin-bottom:10px;"> | |
| <div style="font-weight:bold; font-size:16px;">{name}</div> | |
| <div style="color:#666; font-size:12px;">{id_val}</div> | |
| </div> | |
| """ | |
| return { | |
| login_state_1: gr.update(visible=False), | |
| login_state_2: gr.update(visible=False), | |
| login_state_3: gr.update(visible=True), | |
| student_info_html: gr.update(value=info_html), | |
| user_name_state: name, | |
| user_id_state: id_val, | |
| feedback_used_state: False, | |
| user_input: gr.update( | |
| interactive=True, | |
| placeholder="Ask about Module 10 concepts, Responsible AI, or let Clare test you...", | |
| ), | |
| clear_btn: gr.update(interactive=True), | |
| export_btn: gr.update(interactive=True), | |
| quiz_btn: gr.update(interactive=True), | |
| summary_btn: gr.update(interactive=True), | |
| syllabus_file: gr.update(interactive=True), | |
| doc_type: gr.update(interactive=True), | |
| review_btn: gr.update(interactive=True), | |
| language_preference: gr.update(interactive=True), | |
| learning_mode: gr.update(interactive=True), | |
| model_name: gr.update(interactive=False), | |
| docs_btn: gr.update(interactive=True), | |
| # ✅ 登录后仍然不允许点赞点踩,必须“有回答”才解锁 | |
| thumb_up_btn: gr.update(interactive=False, value="👍 Helpful"), | |
| thumb_down_btn: gr.update(interactive=False, value="👎 Not helpful"), | |
| feedback_toggle_btn: gr.update(interactive=True), | |
| feedback_text: gr.update(visible=False, value=""), | |
| feedback_submit_btn: gr.update(interactive=True, visible=False), | |
| } | |
| login_confirm_btn.click( | |
| confirm_login, | |
| inputs=[name_input, id_input], | |
| outputs=[ | |
| login_state_1, | |
| login_state_2, | |
| login_state_3, | |
| student_info_html, | |
| user_name_state, | |
| user_id_state, | |
| feedback_used_state, | |
| user_input, | |
| clear_btn, | |
| export_btn, | |
| quiz_btn, | |
| summary_btn, | |
| syllabus_file, | |
| doc_type, | |
| review_btn, | |
| language_preference, | |
| learning_mode, | |
| model_name, | |
| docs_btn, | |
| thumb_up_btn, | |
| thumb_down_btn, | |
| feedback_toggle_btn, | |
| feedback_text, | |
| feedback_submit_btn, | |
| ], | |
| ) | |
| def logout(): | |
| return { | |
| login_state_1: gr.update(visible=True), | |
| login_state_2: gr.update(visible=False), | |
| login_state_3: gr.update(visible=False), | |
| name_input: gr.update(value=""), | |
| id_input: gr.update(value=""), | |
| user_name_state: "", | |
| user_id_state: "", | |
| feedback_used_state: False, | |
| student_info_html: gr.update(value=""), | |
| user_input: gr.update( | |
| value="", | |
| interactive=False, | |
| placeholder="Please log in on the right before asking Clare anything...", | |
| ), | |
| clear_btn: gr.update(interactive=False), | |
| export_btn: gr.update(interactive=False), | |
| quiz_btn: gr.update(interactive=False), | |
| summary_btn: gr.update(interactive=False), | |
| syllabus_file: gr.update(interactive=False), | |
| doc_type: gr.update(interactive=False), | |
| review_btn: gr.update(interactive=False), | |
| language_preference: gr.update(interactive=False), | |
| learning_mode: gr.update(interactive=False), | |
| docs_btn: gr.update(interactive=False), | |
| thumb_up_btn: gr.update(interactive=False, value="👍 Helpful"), | |
| thumb_down_btn: gr.update(interactive=False, value="👎 Not helpful"), | |
| feedback_toggle_btn: gr.update(interactive=False), | |
| feedback_text: gr.update(visible=False, value=""), | |
| feedback_submit_btn: gr.update(interactive=False, visible=False), | |
| } | |
| logout_btn.click( | |
| logout, | |
| outputs=[ | |
| login_state_1, | |
| login_state_2, | |
| login_state_3, | |
| name_input, | |
| id_input, | |
| user_name_state, | |
| user_id_state, | |
| feedback_used_state, | |
| student_info_html, | |
| user_input, | |
| clear_btn, | |
| export_btn, | |
| quiz_btn, | |
| summary_btn, | |
| syllabus_file, | |
| doc_type, | |
| review_btn, | |
| language_preference, | |
| learning_mode, | |
| docs_btn, | |
| thumb_up_btn, | |
| thumb_down_btn, | |
| feedback_toggle_btn, | |
| feedback_text, | |
| feedback_submit_btn, | |
| ], | |
| ) | |
| # ================== Main Logic ================== | |
| def update_course_and_rag(file, doc_type_val): | |
| local_topics = preloaded_topics or [] | |
| local_chunks = preloaded_chunks or [] | |
| if file is not None: | |
| try: | |
| topics = extract_course_topics_from_file(file, doc_type_val) | |
| except Exception: | |
| topics = [] | |
| try: | |
| chunks = build_rag_chunks_from_file(file, doc_type_val) | |
| except Exception: | |
| chunks = [] | |
| local_topics = (preloaded_topics or []) + (topics or []) | |
| local_chunks = (preloaded_chunks or []) + (chunks or []) | |
| status_md = ( | |
| f"✅ **Loaded Module 10 base reading + uploaded {doc_type_val} file.**\n\n" | |
| "Both will be used for explanations and quizzes." | |
| ) | |
| else: | |
| status_md = ( | |
| "✅ **Using pre-loaded Module 10 reading only.**\n\n" | |
| "You may optionally upload additional Module 10 materials." | |
| ) | |
| return local_topics, local_chunks, status_md | |
| syllabus_file.change( | |
| update_course_and_rag, | |
| [syllabus_file, doc_type], | |
| [course_outline_state, rag_chunks_state, session_status], | |
| ) | |
| def show_loaded_docs(doc_type_val): | |
| gr.Info( | |
| f"For this experiment, Clare always includes the pre-loaded Module 10 reading.\n" | |
| f"Additional uploaded {doc_type_val} files will be used as supplementary context.", | |
| title="Loaded Documents", | |
| ) | |
| docs_btn.click(show_loaded_docs, inputs=[doc_type]) | |
| def respond( | |
| message, | |
| chat_history, | |
| course_outline, | |
| weaknesses, | |
| cognitive_state, | |
| rag_chunks, | |
| model_name_val, | |
| lang_pref, | |
| mode_val, | |
| doc_type_val, | |
| user_id_val, | |
| feedback_used, | |
| ): | |
| # 未登录:不解锁按钮 | |
| if not user_id_val: | |
| out_msg = ( | |
| "🔒 Please log in with your Student Name and Email/ID on the right " | |
| "before using Clare." | |
| ) | |
| new_history = (chat_history or []) + [[message, out_msg]] | |
| new_status = render_session_status( | |
| mode_val or "Concept Explainer", | |
| weaknesses or [], | |
| cognitive_state or {"confusion": 0, "mastery": 0}, | |
| ) | |
| return ( | |
| "", | |
| new_history, | |
| weaknesses, | |
| cognitive_state, | |
| new_status, | |
| "", | |
| "", | |
| feedback_used, | |
| gr.update(interactive=False, value="👍 Helpful"), | |
| gr.update(interactive=False, value="👎 Not helpful"), | |
| ) | |
| resolved_lang = detect_language(message or "", lang_pref) | |
| # 空输入:不改变按钮状态 | |
| if not message or not message.strip(): | |
| new_status = render_session_status( | |
| mode_val or "Concept Explainer", | |
| weaknesses or [], | |
| cognitive_state or {"confusion": 0, "mastery": 0}, | |
| ) | |
| return ( | |
| "", | |
| chat_history, | |
| weaknesses, | |
| cognitive_state, | |
| new_status, | |
| "", | |
| "", | |
| feedback_used, | |
| gr.update(), | |
| gr.update(), | |
| ) | |
| weaknesses = update_weaknesses_from_message(message, weaknesses or []) | |
| cognitive_state = update_cognitive_state_from_message(message, cognitive_state) | |
| if is_academic_query(message): | |
| rag_context_text, rag_used_chunks = retrieve_relevant_chunks( | |
| message, rag_chunks or [] | |
| ) | |
| else: | |
| rag_context_text, rag_used_chunks = "", [] | |
| start_ts = time.time() | |
| answer, new_history = chat_with_clare( | |
| message=message, | |
| history=chat_history, | |
| model_name=model_name_val, | |
| language_preference=resolved_lang, | |
| learning_mode=mode_val, | |
| doc_type=doc_type_val, | |
| course_outline=course_outline, | |
| weaknesses=weaknesses, | |
| cognitive_state=cognitive_state, | |
| rag_context=rag_context_text, | |
| ) | |
| end_ts = time.time() | |
| latency_ms = (end_ts - start_ts) * 1000.0 | |
| if is_academic_query(message) and rag_used_chunks: | |
| ref_text = format_references(rag_used_chunks) | |
| else: | |
| ref_text = "" | |
| if ref_text and new_history: | |
| last_user, last_assistant = new_history[-1] | |
| if "References (RAG context used):" not in (last_assistant or ""): | |
| last_assistant = f"{last_assistant}\n\n{ref_text}" | |
| new_history[-1] = [last_user, last_assistant] | |
| answer = last_assistant | |
| student_id = user_id_val or "ANON" | |
| experiment_id = "RESP_AI_W10" | |
| try: | |
| log_event( | |
| { | |
| "experiment_id": experiment_id, | |
| "student_id": student_id, | |
| "event_type": "chat_turn", | |
| "timestamp": end_ts, | |
| "latency_ms": latency_ms, | |
| "question": message, | |
| "answer": answer, | |
| "model_name": model_name_val, | |
| "language": resolved_lang, | |
| "learning_mode": mode_val, | |
| } | |
| ) | |
| except Exception as e: | |
| print("log_event error:", e) | |
| new_status = render_session_status(mode_val, weaknesses, cognitive_state) | |
| # ✅ 有新回答:重置 feedback_used=False,并解锁按钮(恢复文案) | |
| return ( | |
| "", | |
| new_history, | |
| weaknesses, | |
| cognitive_state, | |
| new_status, | |
| message, | |
| answer, | |
| False, | |
| gr.update(interactive=True, value="👍 Helpful"), | |
| gr.update(interactive=True, value="👎 Not helpful"), | |
| ) | |
| user_input.submit( | |
| respond, | |
| [ | |
| user_input, | |
| chatbot, | |
| course_outline_state, | |
| weakness_state, | |
| cognitive_state_state, | |
| rag_chunks_state, | |
| model_name, | |
| language_preference, | |
| learning_mode, | |
| doc_type, | |
| user_id_state, | |
| feedback_used_state, | |
| ], | |
| [ | |
| user_input, | |
| chatbot, | |
| weakness_state, | |
| cognitive_state_state, | |
| session_status, | |
| last_question_state, | |
| last_answer_state, | |
| feedback_used_state, | |
| thumb_up_btn, | |
| thumb_down_btn, | |
| ], | |
| ) | |
| # ===== Micro-Quiz ===== | |
| def start_micro_quiz( | |
| chat_history, | |
| course_outline, | |
| weaknesses, | |
| cognitive_state, | |
| rag_chunks, | |
| model_name_val, | |
| lang_pref, | |
| mode_val, | |
| doc_type_val, | |
| user_id_val, | |
| ): | |
| if not user_id_val: | |
| gr.Info("Please log in first to start a micro-quiz.", title="Login required") | |
| return ( | |
| chat_history, | |
| weaknesses, | |
| cognitive_state, | |
| render_session_status( | |
| mode_val or "Concept Explainer", | |
| weaknesses or [], | |
| cognitive_state or {"confusion": 0, "mastery": 0}, | |
| ), | |
| ) | |
| quiz_instruction = ( | |
| "We are running a short micro-quiz session based ONLY on **Module 10 – " | |
| "Responsible AI (Alto, 2024, Chapter 12)** and the pre-loaded materials.\n\n" | |
| "Step 1 – Before asking any content question:\n" | |
| "• First ask me which quiz style I prefer right now:\n" | |
| " - (1) Multiple-choice questions\n" | |
| " - (2) Short-answer / open-ended questions\n" | |
| "• Ask me explicitly: \"Which quiz style do you prefer now: 1) Multiple-choice or 2) Short-answer? " | |
| "Please reply with 1 or 2.\"\n" | |
| "• Do NOT start a content question until I have answered 1 or 2.\n\n" | |
| "Step 2 – After I choose the style:\n" | |
| "• If I choose 1 (multiple-choice):\n" | |
| " - Ask ONE multiple-choice question at a time, based on Module 10 concepts " | |
| "(Responsible AI definition, risk types, mitigation layers, EU AI Act, etc.).\n" | |
| " - Provide 3–4 options (A, B, C, D) and make only one option clearly correct.\n" | |
| "• If I choose 2 (short-answer):\n" | |
| " - Ask ONE short-answer question at a time, also based on Module 10 concepts.\n" | |
| " - Do NOT show the answer when you ask the question.\n\n" | |
| "Step 3 – For each answer I give:\n" | |
| "• Grade my answer (correct / partially correct / incorrect).\n" | |
| "• Give a brief explanation and the correct answer.\n" | |
| "• Then ask if I want another question of the SAME style.\n" | |
| "• Continue this pattern until I explicitly say to stop.\n\n" | |
| "Please start by asking me which quiz style I prefer (1 = multiple-choice, 2 = short-answer). " | |
| "Do not ask any content question before I choose." | |
| ) | |
| resolved_lang = lang_pref | |
| start_ts = time.time() | |
| quiz_ctx_text, _quiz_ctx_chunks = retrieve_relevant_chunks( | |
| "Module 10 quiz", rag_chunks or [] | |
| ) | |
| answer, new_history = chat_with_clare( | |
| message=quiz_instruction, | |
| history=chat_history, | |
| model_name=model_name_val, | |
| language_preference=resolved_lang, | |
| learning_mode=mode_val, | |
| doc_type=doc_type_val, | |
| course_outline=course_outline, | |
| weaknesses=weaknesses, | |
| cognitive_state=cognitive_state, | |
| rag_context=quiz_ctx_text, | |
| ) | |
| end_ts = time.time() | |
| latency_ms = (end_ts - start_ts) * 1000.0 | |
| student_id = user_id_val or "ANON" | |
| experiment_id = "RESP_AI_W10" | |
| try: | |
| log_event( | |
| { | |
| "experiment_id": experiment_id, | |
| "student_id": student_id, | |
| "event_type": "micro_quiz_start", | |
| "timestamp": end_ts, | |
| "latency_ms": latency_ms, | |
| "question": quiz_instruction, | |
| "answer": answer, | |
| "model_name": model_name_val, | |
| "language": resolved_lang, | |
| "learning_mode": mode_val, | |
| } | |
| ) | |
| except Exception as e: | |
| print("log_event error:", e) | |
| new_status = render_session_status(mode_val, weaknesses, cognitive_state) | |
| return new_history, weaknesses, cognitive_state, new_status | |
| quiz_btn.click( | |
| start_micro_quiz, | |
| [ | |
| chatbot, | |
| course_outline_state, | |
| weakness_state, | |
| cognitive_state_state, | |
| rag_chunks_state, | |
| model_name, | |
| language_preference, | |
| learning_mode, | |
| doc_type, | |
| user_id_state, | |
| ], | |
| [chatbot, weakness_state, cognitive_state_state, session_status], | |
| ) | |
| # ===== Feedback Handlers (thumb + detailed) ===== | |
| def show_feedback_box(): | |
| return { | |
| feedback_text: gr.update(visible=True), | |
| feedback_submit_btn: gr.update(visible=True), | |
| } | |
| feedback_toggle_btn.click( | |
| show_feedback_box, | |
| None, | |
| [feedback_text, feedback_submit_btn], | |
| ) | |
| def send_thumb_up( | |
| last_q, | |
| last_a, | |
| user_id_val, | |
| mode_val, | |
| model_name_val, | |
| lang_pref, | |
| feedback_used, | |
| ): | |
| # 没有可评价回答:保持禁用 | |
| if not last_q and not last_a: | |
| print("No last QA to log for thumbs_up.") | |
| return ( | |
| feedback_used, | |
| gr.update(interactive=False, value="👍 Helpful"), | |
| gr.update(interactive=False, value="👎 Not helpful"), | |
| ) | |
| # 已经反馈过:直接禁用 | |
| if feedback_used: | |
| print("Feedback already sent for this answer (thumb_up).") | |
| return ( | |
| feedback_used, | |
| gr.update(interactive=False), | |
| gr.update(interactive=False), | |
| ) | |
| try: | |
| log_event( | |
| { | |
| "experiment_id": "RESP_AI_W10", | |
| "student_id": user_id_val or "ANON", | |
| "event_type": "like", | |
| "timestamp": time.time(), | |
| "question": last_q, | |
| "answer": last_a, | |
| "model_name": model_name_val, | |
| "language": lang_pref, | |
| "learning_mode": mode_val, | |
| } | |
| ) | |
| print("[Feedback] thumbs_up logged to LangSmith.") | |
| except Exception as e: | |
| print("thumb_up log error:", e) | |
| # 点完一次:置 True + 按钮置灰 + 文案 sent | |
| return ( | |
| True, | |
| gr.update(interactive=False, value="👍 Helpful (sent)"), | |
| gr.update(interactive=False), | |
| ) | |
| def send_thumb_down( | |
| last_q, | |
| last_a, | |
| user_id_val, | |
| mode_val, | |
| model_name_val, | |
| lang_pref, | |
| feedback_used, | |
| ): | |
| if not last_q and not last_a: | |
| print("No last QA to log for thumbs_down.") | |
| return ( | |
| feedback_used, | |
| gr.update(interactive=False, value="👍 Helpful"), | |
| gr.update(interactive=False, value="👎 Not helpful"), | |
| ) | |
| if feedback_used: | |
| print("Feedback already sent for this answer (thumb_down).") | |
| return ( | |
| feedback_used, | |
| gr.update(interactive=False), | |
| gr.update(interactive=False), | |
| ) | |
| try: | |
| log_event( | |
| { | |
| "experiment_id": "RESP_AI_W10", | |
| "student_id": user_id_val or "ANON", | |
| "event_type": "dislike", | |
| "timestamp": time.time(), | |
| "question": last_q, | |
| "answer": last_a, | |
| "model_name": model_name_val, | |
| "language": lang_pref, | |
| "learning_mode": mode_val, | |
| } | |
| ) | |
| print("[Feedback] thumbs_down logged to LangSmith.") | |
| except Exception as e: | |
| print("thumb_down log error:", e) | |
| return ( | |
| True, | |
| gr.update(interactive=False), | |
| gr.update(interactive=False, value="👎 Not helpful (sent)"), | |
| ) | |
| thumb_up_btn.click( | |
| send_thumb_up, | |
| [ | |
| last_question_state, | |
| last_answer_state, | |
| user_id_state, | |
| learning_mode, | |
| model_name, | |
| language_preference, | |
| feedback_used_state, | |
| ], | |
| [feedback_used_state, thumb_up_btn, thumb_down_btn], | |
| ) | |
| thumb_down_btn.click( | |
| send_thumb_down, | |
| [ | |
| last_question_state, | |
| last_answer_state, | |
| user_id_state, | |
| learning_mode, | |
| model_name, | |
| language_preference, | |
| feedback_used_state, | |
| ], | |
| [feedback_used_state, thumb_up_btn, thumb_down_btn], | |
| ) | |
| def submit_detailed_feedback( | |
| text, last_q, last_a, user_id_val, mode_val, model_name_val, lang_pref | |
| ): | |
| if not text or not text.strip(): | |
| return gr.update( | |
| value="", | |
| placeholder="Please enter some feedback before submitting.", | |
| ) | |
| try: | |
| log_event( | |
| { | |
| "experiment_id": "RESP_AI_W10", | |
| "student_id": user_id_val or "ANON", | |
| "event_type": "detailed_feedback", | |
| "timestamp": time.time(), | |
| "question": last_q, | |
| "answer": last_a, | |
| "feedback_text": text.strip(), | |
| "model_name": model_name_val, | |
| "language": lang_pref, | |
| "learning_mode": mode_val, | |
| } | |
| ) | |
| print("[Feedback] detailed_feedback logged to LangSmith.") | |
| except Exception as e: | |
| print("detailed_feedback log error:", e) | |
| return gr.update( | |
| value="", | |
| placeholder="Thanks! Your feedback has been recorded.", | |
| ) | |
| feedback_submit_btn.click( | |
| submit_detailed_feedback, | |
| [ | |
| feedback_text, | |
| last_question_state, | |
| last_answer_state, | |
| user_id_state, | |
| learning_mode, | |
| model_name, | |
| language_preference, | |
| ], | |
| [feedback_text], | |
| ) | |
| # ===== Export / Summary ===== | |
| export_btn.click( | |
| lambda h, c, m, w, cog: export_conversation(h, c, m, w, cog), | |
| [chatbot, course_outline_state, learning_mode, weakness_state, cognitive_state_state], | |
| [result_display], | |
| ) | |
| summary_btn.click( | |
| lambda h, c, w, cog, m, l: summarize_conversation( | |
| h, c, w, cog, m, l | |
| ), | |
| [ | |
| chatbot, | |
| course_outline_state, | |
| weakness_state, | |
| cognitive_state_state, | |
| model_name, | |
| language_preference, | |
| ], | |
| [result_display], | |
| ) | |
| # ===== Reset Conversation ===== | |
| def clear_all(): | |
| empty_state = {"confusion": 0, "mastery": 0} | |
| default_status = render_session_status("Concept Explainer", [], empty_state) | |
| return ( | |
| [], | |
| [], | |
| empty_state, | |
| [], | |
| "", | |
| default_status, | |
| "", | |
| "", | |
| False, | |
| gr.update(interactive=False, value="👍 Helpful"), | |
| gr.update(interactive=False, value="👎 Not helpful"), | |
| ) | |
| clear_btn.click( | |
| clear_all, | |
| None, | |
| [ | |
| chatbot, | |
| weakness_state, | |
| cognitive_state_state, | |
| rag_chunks_state, | |
| result_display, | |
| session_status, | |
| last_question_state, | |
| last_answer_state, | |
| feedback_used_state, | |
| thumb_up_btn, | |
| thumb_down_btn, | |
| ], | |
| queue=False, | |
| ) | |
| if __name__ == "__main__": | |
| demo.launch( | |
| share=True, | |
| server_name="0.0.0.0", | |
| server_port=7860, | |
| ) | |