Spaces:
Sleeping
Sleeping
| """ | |
| Educational LLM Application Based on Gradio | |
| """ | |
| import os | |
| import gradio as gr | |
| from typing import Dict, Any, List, Tuple, Optional | |
| from visualization import create_network_graph | |
| from llm_utils import decompose_concepts, get_concept_explanation, call_llm | |
| from concept_handler import MOCK_DECOMPOSITION_RESULT, MOCK_EXPLANATION_RESULT | |
| from config import DEBUG_MODE | |
| from fastapi import FastAPI | |
| from fastapi.responses import JSONResponse | |
| import json | |
| from fastapi.staticfiles import StaticFiles | |
| import matplotlib as mpl | |
| import platform | |
| # 设置中文字体 | |
| if platform.system() == "Linux": | |
| # Linux系统使用文泉驿微米黑 | |
| mpl.rcParams['font.sans-serif'] = ['WenQuanYi Micro Hei'] | |
| else: | |
| # Windows系统使用微软雅黑 | |
| mpl.rcParams['font.sans-serif'] = ['Microsoft YaHei'] | |
| mpl.rcParams['axes.unicode_minus'] = False # 解决负号显示问题 | |
| def ensure_assets_directory(): | |
| """确保assets目录存在并包含必要的静态文件""" | |
| # 创建assets目录 | |
| os.makedirs("assets", exist_ok=True) | |
| # 将CSS内容写入文件 | |
| css_path = os.path.join("assets", "style.css") | |
| if not os.path.exists(css_path): | |
| with open(css_path, "w", encoding="utf-8") as f: | |
| f.write(custom_css) # 使用之前定义的custom_css内容 | |
| # 读取CSS文件 | |
| def load_css(): | |
| """读取CSS文件内容""" | |
| css_path = os.path.join("assets", "style.css") | |
| try: | |
| with open(css_path, "r", encoding="utf-8") as f: | |
| return f.read() | |
| except Exception as e: | |
| print(f"读取CSS文件失败: {str(e)}") | |
| return "" # 如果读取失败,返回空字符串 | |
| # 在应用启动时初始化 | |
| ensure_assets_directory() | |
| custom_css = load_css() | |
| # Custom JavaScript code | |
| custom_js = """ | |
| // Handle concept card clicks | |
| function conceptClick(conceptId) { | |
| // Find the hidden input field and update its value | |
| const conceptSelection = document.getElementById('concept-selection'); | |
| if (conceptSelection) { | |
| conceptSelection.value = conceptId; | |
| conceptSelection.dispatchEvent(new Event('input', { bubbles: true })); | |
| // Highlight the selected card | |
| document.querySelectorAll('.concept-card').forEach(card => { | |
| card.classList.remove('selected-card'); | |
| if (card.getAttribute('data-concept-id') === conceptId) { | |
| card.classList.add('selected-card'); | |
| } | |
| }); | |
| } | |
| } | |
| // Enhance image display after loading | |
| document.addEventListener('DOMContentLoaded', function() { | |
| const graphContainer = document.getElementById('concept-graph'); | |
| if (graphContainer) { | |
| const observer = new MutationObserver(function(mutations) { | |
| mutations.forEach(function(mutation) { | |
| if (mutation.addedNodes && mutation.addedNodes.length > 0) { | |
| const img = graphContainer.querySelector('img'); | |
| if (img) { | |
| img.style.maxWidth = '100%'; | |
| img.style.height = 'auto'; | |
| img.style.borderRadius = '8px'; | |
| img.style.boxShadow = '0 4px 8px rgba(0,0,0,0.1)'; | |
| } | |
| } | |
| }); | |
| }); | |
| observer.observe(graphContainer, { childList: true, subtree: true }); | |
| } | |
| }); | |
| """ | |
| # Create cache directory | |
| os.makedirs("cache", exist_ok=True) | |
| # Global state storage | |
| class AppState: | |
| def __init__(self): | |
| self.user_profile = {} | |
| self.current_concepts_data = None | |
| self.nodes_dict = {} | |
| self.concepts_explanations = {} # Cache for generated concept explanations | |
| self.concepts_expansions = {} # Cache for generated concept expansions | |
| self.card_explanations = {} # 新增:缓存卡片点击生成的解释内容 | |
| def update_user_profile(self, grade: str, subject: str, needs: str) -> Dict[str, str]: | |
| """Update user profile""" | |
| self.user_profile = { | |
| "grade": grade, | |
| "subject": subject, | |
| "needs": needs | |
| } | |
| return self.user_profile | |
| def set_concepts_data(self, concepts_data: Dict[str, Any], nodes_dict: Dict[str, Any]): | |
| """Set current concept data and node dictionary""" | |
| self.current_concepts_data = concepts_data | |
| self.nodes_dict = nodes_dict | |
| def cache_concept_explanation(self, concept_id: str, explanation_data: Dict[str, Any]): | |
| """Cache concept explanation data""" | |
| self.concepts_explanations[concept_id] = explanation_data | |
| def get_cached_explanation(self, concept_id: str) -> Optional[Dict[str, Any]]: | |
| """Get cached concept explanation if it exists""" | |
| return self.concepts_explanations.get(concept_id) | |
| def cache_concept_expansion(self, concept_id: str, expansion_data: Dict[str, Any]): | |
| """Cache concept expansion data""" | |
| self.concepts_expansions[concept_id] = expansion_data | |
| def get_cached_expansion(self, concept_id: str) -> Optional[Dict[str, Any]]: | |
| """Get cached concept expansion if it exists""" | |
| return self.concepts_expansions.get(concept_id) | |
| def cache_card_explanation(self, concept_id: str, explanation_text: str): | |
| """缓存卡片点击的解释内容""" | |
| self.card_explanations[concept_id] = explanation_text | |
| def get_cached_card_explanation(self, concept_id: str) -> Optional[str]: | |
| """获取缓存的卡片解释内容""" | |
| return self.card_explanations.get(concept_id) | |
| # Initialize application state | |
| app_state = AppState() | |
| # CreateFastAPI应用 | |
| app = FastAPI( | |
| title="Educational LLM Assistant", | |
| description="Interactive Learning Through AI-Powered Concept Breakdown", | |
| version="1.0.0" | |
| ) | |
| # 添加静态文件服务 | |
| app.mount("/assets", StaticFiles(directory="assets"), name="assets") | |
| # 修改 FastAPI 路由部分 | |
| async def trigger_llm(data: dict): | |
| try: | |
| concept_id = data.get("concept_id") | |
| if not concept_id: | |
| return JSONResponse({"error": "Missing concept_id"}, status_code=400) | |
| # 生成解释内容 | |
| explanation_content = generate_card_explanation(concept_id) | |
| # 只返回生成的内容,让前端处理UI更新 | |
| return JSONResponse({ | |
| "status": "success", | |
| "content": explanation_content | |
| }) | |
| except Exception as e: | |
| return JSONResponse({"error": str(e)}, status_code=500) | |
| # Helper function for formatting concept cards | |
| def generate_concept_cards(concept_map: Dict) -> str: | |
| """Generate HTML for concept cards with enhanced styling""" | |
| cards_html = '<div class="concept-cards-container">' | |
| for concept in concept_map.get("sub_concepts", []): | |
| difficulty_class = f"difficulty-{concept.get('difficulty', 'basic')}" | |
| concept_id = concept['id'] | |
| concept_name = concept['name'] | |
| concept_description = concept['description'] | |
| # 修改fetch回调部分 | |
| cards_html += f""" | |
| <div class="concept-card {difficulty_class}" | |
| data-concept-id="{concept_id}" | |
| onclick="(function(id) {{ | |
| console.log('点击概念卡片:', id); | |
| // 更新UI状态 | |
| document.querySelectorAll('.concept-card').forEach(card => {{ | |
| card.classList.remove('selected-card'); | |
| if (card.getAttribute('data-concept-id') === id) {{ | |
| card.classList.add('selected-card'); | |
| }} | |
| }}); | |
| // 显示加载动画 | |
| const directAnswer = document.querySelector('.answer-box'); | |
| if (directAnswer) {{ | |
| const existingContainers = document.querySelectorAll('.concept-explanation-container'); | |
| existingContainers.forEach(container => container.remove()); | |
| const loadingContainer = document.createElement('div'); | |
| loadingContainer.className = 'concept-explanation-container'; | |
| loadingContainer.innerHTML = ` | |
| <div class="loading"> | |
| <div class="loading-spinner"></div> | |
| <div class="loading-text">正在加载概念解释...</div> | |
| </div> | |
| `; | |
| directAnswer.parentNode.insertBefore(loadingContainer, directAnswer.nextSibling); | |
| }} | |
| // 触发Gradio事件以获取缓存或生成新内容 | |
| const cardSelectionInput = document.getElementById('card-selection'); | |
| if (cardSelectionInput) {{ | |
| cardSelectionInput.value = id; | |
| cardSelectionInput.dispatchEvent(new Event('input', {{ bubbles: true }})); | |
| }} | |
| }})('{concept_id}')"> | |
| <div class="concept-header"> | |
| <h3>{concept_name}</h3> | |
| <span class="difficulty-badge">{concept.get('difficulty', 'basic')}</span> | |
| </div> | |
| <p>{concept_description}</p> | |
| </div> | |
| """ | |
| cards_html += """ | |
| <style> | |
| .concept-cards-container { | |
| display: grid; | |
| grid-template-columns: repeat(auto-fit, minmax(250px, 1fr)); | |
| gap: 15px; | |
| padding: 10px; | |
| } | |
| .concept-card { | |
| background: white; | |
| border-radius: 10px; | |
| padding: 15px; | |
| cursor: pointer; | |
| transition: all 0.3s ease; | |
| border: 1px solid #e9ecef; | |
| } | |
| .concept-header { | |
| display: flex; | |
| justify-content: space-between; | |
| align-items: center; | |
| margin-bottom: 10px; | |
| } | |
| .difficulty-badge { | |
| padding: 4px 8px; | |
| border-radius: 12px; | |
| font-size: 0.8em; | |
| font-weight: 500; | |
| } | |
| .difficulty-basic .difficulty-badge { | |
| background: #e3f2fd; | |
| color: #1976d2; | |
| } | |
| .difficulty-intermediate .difficulty-badge { | |
| background: #fff3e0; | |
| color: #f57c00; | |
| } | |
| .difficulty-advanced .difficulty-badge { | |
| background: #ffebee; | |
| color: #d32f2f; | |
| } | |
| </style> | |
| """ | |
| return cards_html | |
| # Function definitions | |
| def update_profile(grade, subject, needs): | |
| app_state.update_user_profile(grade, subject, needs) | |
| return f"*Current user profile: {grade} {subject} student - Learning needs: {needs if needs else 'Not specified'}*" | |
| # 添加新的函数用于生成解释 | |
| def generate_explanation(question: str, concept_map: Dict[str, Any], user_profile: Dict[str, str]) -> str: | |
| """ | |
| Generate explanation for the question using LLM | |
| Args: | |
| question: Original question | |
| concept_map: Concept map data | |
| user_profile: User profile information | |
| Returns: | |
| Generated explanation | |
| """ | |
| system_prompt = """You are an expert educational AI tutor. Please provide a clear and concise answer | |
| to the student's question, considering their grade level and subject background. | |
| Your response must be in JSON format with the following structure: | |
| { | |
| "explanation": "Your detailed explanation here" | |
| } | |
| The explanation should be: | |
| 1. Direct and focused on the question | |
| 2. Appropriate for the student's level | |
| 3. Connected to the main concept | |
| 4. Easy to understand | |
| """ | |
| user_prompt = f""" | |
| Please provide a JSON response explaining the following question: | |
| Question: {question} | |
| Student Background: | |
| - Grade Level: {user_profile['grade']} | |
| - Subject: {user_profile['subject']} | |
| - Learning Needs: {user_profile.get('needs', 'Not specified')} | |
| Main Concept: {concept_map.get('main_concept', '')} | |
| Remember to format your response as a JSON object with an "explanation" field. | |
| """ | |
| try: | |
| response = call_llm(system_prompt, user_prompt) | |
| return response.get("explanation", "No explanation could be generated.") | |
| except Exception as e: | |
| if DEBUG_MODE: | |
| print(f"Error generating explanation: {str(e)}") | |
| return "Could not generate explanation at this time." | |
| # 修改 analyze_question 函数 | |
| def analyze_question(question, grade, subject, learning_needs): | |
| """ | |
| Analyze question and return results as HTML | |
| Returns: | |
| Tuple of (answer_section, question_answer, concept_graph, concept_cards, concepts_section, error_msg, card_explanation_section) | |
| """ | |
| try: | |
| # 首先返回加载状态 | |
| yield ( | |
| gr.update(visible=True), # 显示答案区域 | |
| gr.update(value="<div class='loading'>Analyzing your question...</div>"), # 显示加载信息 | |
| gr.update(value="<div class='loading'>Generating concept map...</div>"), # 显示加载信息 | |
| gr.update(value="<div class='loading'>Preparing concept cards...</div>"), # 显示加载信息 | |
| gr.update(visible=True), | |
| gr.update(visible=False), | |
| gr.update(visible=False) # 隐藏卡片解释区域 | |
| ) | |
| user_profile = { | |
| "grade": grade, | |
| "subject": subject, | |
| "needs": learning_needs | |
| } | |
| concept_map = decompose_concepts(user_profile, question) | |
| # 检查是否需要生成解释 | |
| explanation = concept_map.get("Explanation", "").strip() | |
| if not explanation: | |
| explanation = generate_explanation(question, concept_map, user_profile) | |
| concept_map["Explanation"] = explanation | |
| # 创建节点字典 | |
| nodes_dict = { | |
| concept["id"]: { | |
| "name": concept["name"], | |
| "description": concept["description"] | |
| } | |
| for concept in concept_map["sub_concepts"] | |
| } | |
| # 存储到应用状态 | |
| app_state.set_concepts_data(concept_map, nodes_dict) | |
| # 格式化解答HTML | |
| answer_html = f""" | |
| <div class="answer-content"> | |
| {concept_map["Explanation"]} | |
| </div> | |
| <div class="main-concept"> | |
| <strong>Main Concept:</strong> {concept_map.get("main_concept", "")} | |
| </div> | |
| """ | |
| # 生成可视化图 | |
| graph_data_url = create_network_graph(concept_map) | |
| graph_html = f""" | |
| <div class="concept-graph-container"> | |
| <img src="{graph_data_url}" alt="Concept Knowledge Graph" /> | |
| </div> | |
| """ | |
| # 生成概念卡片HTML | |
| cards_html = generate_concept_cards(concept_map) | |
| # 返回最终结果 | |
| yield ( | |
| gr.update(visible=True), | |
| gr.update(value=answer_html), | |
| gr.update(value=graph_html), | |
| gr.update(value=cards_html), | |
| gr.update(visible=True), | |
| gr.update(visible=False), | |
| gr.update(visible=False) # 隐藏卡片解释区域 | |
| ) | |
| except Exception as e: | |
| if DEBUG_MODE: | |
| print(f"Error analyzing question: {str(e)}") | |
| import traceback | |
| print(traceback.format_exc()) | |
| yield ( | |
| gr.update(visible=False), | |
| gr.update(value=""), | |
| gr.update(value=""), | |
| gr.update(value=""), | |
| gr.update(visible=False), | |
| gr.update(visible=True, value=f"Error: {str(e)}"), | |
| gr.update(visible=False) # 出错时也隐藏卡片解释区域 | |
| ) | |
| def format_explanation(explanation_data): | |
| """ | |
| Format explanation data into HTML | |
| Args: | |
| explanation_data: Dictionary with explanation data | |
| Returns: | |
| Formatted explanation text | |
| """ | |
| if not explanation_data: | |
| return "No explanation available" | |
| explanation = explanation_data.get("explanation", "No explanation available") | |
| return explanation | |
| def show_concept_explanation(concept_id): | |
| if not concept_id or concept_id not in app_state.nodes_dict: | |
| return { | |
| explanation_section: gr.update(visible=False), | |
| error_msg: gr.update(visible=True, value="⚠️ Invalid concept ID") | |
| } | |
| # Get concept information | |
| concept_info = app_state.nodes_dict[concept_id] | |
| concept_name = concept_info["name"] | |
| concept_description = concept_info["description"] | |
| # First check from cache | |
| explanation_data = app_state.get_cached_explanation(concept_id) | |
| if not explanation_data: | |
| try: | |
| # 使用 llm_utils 替代 llm_chain | |
| user_profile = { | |
| "grade": app_state.user_profile.get("grade", "High School"), | |
| "subject": app_state.user_profile.get("subject", "Math"), | |
| "needs": app_state.user_profile.get("needs", "") | |
| } | |
| explanation_data = get_concept_explanation( | |
| user_profile, | |
| concept_id, | |
| concept_name, | |
| concept_description | |
| ) | |
| # 缓存结果 | |
| app_state.cache_concept_explanation(concept_id, explanation_data) | |
| except Exception as e: | |
| if DEBUG_MODE: | |
| print(f"Error explaining concept: {str(e)}") | |
| return { | |
| explanation_header: f"### {concept_name} Concept Explanation", | |
| explanation_content: f"Error generating explanation: {str(e)}", | |
| examples_content: "", | |
| resources_content: "", | |
| practice_content: "", | |
| concepts_section: gr.update(visible=False), | |
| explanation_section: gr.update(visible=True), | |
| error_msg: gr.update(visible=False) | |
| } | |
| # 从explanation_data提取和格式化内容 | |
| explanation = explanation_data.get("explanation", "No explanation available") | |
| # Format examples | |
| examples_html = "<div class='examples-container'>" | |
| for idx, example in enumerate(explanation_data.get("examples", [])): | |
| examples_html += f""" | |
| <div class="example-box"> | |
| <h4>Example {idx+1} ({example.get('difficulty', 'Difficulty not specified')})</h4> | |
| <p><strong>Problem:</strong> {example.get('problem', 'None')}</p> | |
| <p><strong>Solution:</strong> <pre style="white-space: pre-wrap;">{example.get('solution', 'None')}</pre></p> | |
| </div> | |
| """ | |
| examples_html += "</div>" | |
| # Format resources | |
| resources_html = "<div class='resources-container'>" | |
| if explanation_data.get("resources"): | |
| for res in explanation_data.get("resources", []): | |
| link_html = f"<a href='{res.get('link', '#')}' target='_blank'>View Resource</a>" if res.get('link') else "" | |
| resources_html += f""" | |
| <div class="resource-item"> | |
| <p><strong>{res.get('type', 'Resource')}:</strong> {res.get('title', 'Unnamed resource')}</p> | |
| <p>{res.get('description', 'No description')}</p> | |
| {link_html} | |
| </div> | |
| """ | |
| else: | |
| resources_html += "<p>No related learning resources available</p>" | |
| resources_html += "</div>" | |
| # Format practice problems | |
| practice_html = "<div class='practice-container'>" | |
| if explanation_data.get("practice_questions"): | |
| for idx, question in enumerate(explanation_data.get("practice_questions", [])): | |
| practice_html += f""" | |
| <div class="example-box"> | |
| <h4>Practice Problem {idx+1} ({question.get('difficulty', 'Difficulty not specified')})</h4> | |
| <p><strong>Question:</strong> {question.get('question', 'None')}</p> | |
| <details> | |
| <summary>View Answer</summary> | |
| <p>{question.get('answer', 'None')}</p> | |
| </details> | |
| </div> | |
| """ | |
| else: | |
| practice_html += "<p>No practice problems available</p>" | |
| practice_html += "</div>" | |
| return { | |
| explanation_header: f"### {concept_name} Concept Explanation", | |
| explanation_content: explanation, | |
| examples_content: examples_html, | |
| resources_content: resources_html, | |
| practice_content: practice_html, | |
| concepts_section: gr.update(visible=False), | |
| explanation_section: gr.update(visible=True), | |
| error_msg: gr.update(visible=False) | |
| } | |
| def back_to_concepts(): | |
| return { | |
| concepts_section: gr.update(visible=True), | |
| explanation_section: gr.update(visible=False) | |
| } | |
| # JS function to handle click events | |
| def handle_concept_click(concept_id): | |
| if concept_id: | |
| return show_concept_explanation(concept_id) | |
| return None | |
| # 添加新函数,用于生成详细的概念解释 | |
| def generate_card_explanation(concept_id: str) -> str: | |
| """生成详细的概念解释 | |
| Args: | |
| concept_id: 概念ID | |
| Returns: | |
| HTML格式的解释内容 | |
| """ | |
| try: | |
| print(f"开始生成概念解释: {concept_id}") | |
| # 获取概念信息 | |
| concept_info = app_state.nodes_dict.get(concept_id) | |
| if not concept_info: | |
| raise ValueError(f"找不到概念信息: {concept_id}") | |
| concept_name = concept_info["name"] | |
| concept_description = concept_info["description"] | |
| # 获取前置概念 | |
| prerequisites = [] | |
| if app_state.current_concepts_data and "relationships" in app_state.current_concepts_data: | |
| for rel in app_state.current_concepts_data["relationships"]: | |
| if rel.get("target") == concept_id and rel.get("type") == "prerequisite": | |
| source_id = rel.get("source") | |
| if source_id in app_state.nodes_dict: | |
| prerequisites.append(app_state.nodes_dict[source_id]["name"]) | |
| # 修改system_prompt,明确指定所有必需字段 | |
| system_prompt = """You are an expert educational tutor. Please provide a clear and detailed explanation of the concept based on the student's grade level. | |
| Your response MUST be in the following JSON format and MUST include ALL of these fields: | |
| { | |
| "explanation": "Detailed concept explanation", | |
| "key_points": ["key point 1", "key point 2", ...], | |
| "examples": [ | |
| { | |
| "problem": "Example problem", | |
| "solution": "Detailed solution steps", | |
| "difficulty": "basic/intermediate/advanced" | |
| } | |
| ], | |
| "practice": [ | |
| { | |
| "question": "Practice question", | |
| "answer": "Answer with explanation", | |
| "difficulty": "basic/intermediate/advanced" | |
| } | |
| ], | |
| "resources": [ | |
| { | |
| "type": "Video/Article/Interactive/Book", | |
| "title": "Resource title", | |
| "description": "Brief description of the resource", | |
| "link": "Optional URL to the resource" | |
| } | |
| ] | |
| } | |
| All fields are required. For resources, provide at least one learning resource that would help students understand this concept better. | |
| Ensure that: | |
| 1. The explanation is appropriate for the student's grade level | |
| 2. Use appropriate terminology | |
| 3. Include specific examples | |
| 4. Provide clear solution steps | |
| 5. Include relevant learning resources""" | |
| user_prompt = f"""Please explain this concept and provide ALL required information including explanation, key points, examples, practice questions, and learning resources: | |
| Concept Name: {concept_name} | |
| Concept Description: {concept_description} | |
| Prerequisites: {', '.join(prerequisites) if prerequisites else 'None'} | |
| Student Background: | |
| - Grade Level: {app_state.user_profile.get('grade', 'High School')} | |
| - Subject: {app_state.user_profile.get('subject', 'Math')} | |
| - Learning Needs: {app_state.user_profile.get('needs', 'Comprehensive understanding')} | |
| Remember to include all required sections in your response.""" | |
| print("正在调用LLM生成解释...") # 添加调试日志 | |
| # 导入并调用LLM | |
| from llm_utils import call_llm | |
| try: | |
| response = call_llm(system_prompt, user_prompt) | |
| print("LLM响应:", response) # 添加调试日志 | |
| if not isinstance(response, dict): | |
| raise ValueError("LLM返回的响应格式不正确") | |
| except Exception as llm_error: | |
| print(f"调用LLM时出错: {str(llm_error)}") | |
| raise | |
| # 修改formatted_explanation部分 | |
| formatted_explanation = f""" | |
| <div class="card-explanation"> | |
| <h3>{concept_name}</h3> | |
| <div class="explanation-section"> | |
| <h4>📚 Concept Explanation</h4> | |
| <div class="content-box"> | |
| {response.get('explanation', 'No explanation available')} | |
| </div> | |
| </div> | |
| <div class="key-points-section"> | |
| <h4>🎯 Key Points</h4> | |
| <ul> | |
| {''.join([f'<li>{point}</li>' for point in response.get('key_points', [])])} | |
| </ul> | |
| </div> | |
| <div class="examples-section"> | |
| <h4>📝 Example Analysis</h4> | |
| {''.join([ | |
| f''' | |
| <div class="example-box"> | |
| <div class="example-header"> | |
| <span class="difficulty-badge">{example.get('difficulty', 'Basic')}</span> | |
| </div> | |
| <div class="example-problem"> | |
| <strong>Example:</strong>{example.get('problem', '')} | |
| </div> | |
| <div class="solution-box"> | |
| <strong>Solution:</strong>{example.get('solution', '')} | |
| </div> | |
| </div> | |
| ''' | |
| for example in response.get('examples', []) | |
| ])} | |
| </div> | |
| <div class="practice-section"> | |
| <h4>✍️ Practice Problems</h4> | |
| {''.join([ | |
| f''' | |
| <div class="exercise-box"> | |
| <div class="exercise-header"> | |
| <span class="difficulty-badge">{practice.get('difficulty', 'Basic')}</span> | |
| </div> | |
| <div class="question"> | |
| <strong>Problem:</strong>{practice.get('question', '')} | |
| </div> | |
| <details class="answer-details"> | |
| <summary>View Answer</summary> | |
| <div class="solution-box"> | |
| {practice.get('answer', '')} | |
| </div> | |
| </details> | |
| </div> | |
| ''' | |
| for practice in response.get('practice', []) | |
| ])} | |
| </div> | |
| <div class="resources-section"> | |
| <h4>📚 Learning Resources</h4> | |
| {''.join([ | |
| f''' | |
| <div class="resource-box"> | |
| <div class="resource-header"> | |
| <span class="resource-type">{resource.get('type', 'Resource')}</span> | |
| </div> | |
| <div class="resource-content"> | |
| <strong>{resource.get('title', '')}</strong> | |
| <p>{resource.get('description', '')}</p> | |
| {f'<a href="{resource.get("link")}" target="_blank">View Resource</a>' if resource.get('link') else ''} | |
| </div> | |
| </div> | |
| ''' | |
| for resource in response.get('resources', []) | |
| ])} | |
| </div> | |
| </div> | |
| <style> | |
| .card-explanation {{ | |
| padding: 20px; | |
| background: white; | |
| border-radius: 12px; | |
| box-shadow: 0 2px 8px rgba(0,0,0,0.1); | |
| }} | |
| .explanation-section, .key-points-section, .examples-section, | |
| .practice-section, .resources-section {{ | |
| margin-top: 20px; | |
| padding: 15px; | |
| background: #f8f9fa; | |
| border-radius: 8px; | |
| }} | |
| .content-box {{ | |
| line-height: 1.6; | |
| color: #2c3e50; | |
| }} | |
| .example-box, .exercise-box, .resource-box {{ | |
| background-color: #f1f8ff; | |
| border-left: 4px solid #2196f3; | |
| padding: 15px; | |
| margin: 10px 0; | |
| border-radius: 0 8px 8px 0; | |
| }} | |
| .difficulty-badge, .resource-type {{ | |
| display: inline-block; | |
| padding: 4px 8px; | |
| border-radius: 12px; | |
| font-size: 0.85em; | |
| background: #e3f2fd; | |
| color: #1976d2; | |
| margin-bottom: 10px; | |
| }} | |
| .solution-box {{ | |
| margin-top: 10px; | |
| padding: 10px; | |
| background: rgba(52, 152, 219, 0.05); | |
| border-radius: 4px; | |
| }} | |
| .answer-details summary {{ | |
| cursor: pointer; | |
| color: #2196f3; | |
| margin: 10px 0; | |
| }} | |
| .resource-content a {{ | |
| display: inline-block; | |
| margin-top: 10px; | |
| color: #2196f3; | |
| text-decoration: none; | |
| padding: 5px 10px; | |
| border: 1px solid #2196f3; | |
| border-radius: 4px; | |
| }} | |
| .resource-content a:hover {{ | |
| background: #e3f2fd; | |
| }} | |
| </style> | |
| """ | |
| # 缓存结果 | |
| app_state.cache_card_explanation(concept_id, formatted_explanation) | |
| print(f"已缓存概念解释内容") | |
| return formatted_explanation | |
| except Exception as e: | |
| import traceback | |
| error_msg = f"生成解释时出错: {str(e)}" | |
| print(error_msg) | |
| print(traceback.format_exc()) | |
| return f"""<div class="error-message"> | |
| <h3>无法生成详细解释</h3> | |
| <p>{error_msg}</p> | |
| <h4>基本概念信息:</h4> | |
| <p><strong>{concept_name}</strong>: {concept_description}</p> | |
| </div>""" | |
| def handle_card_selection(concept_id: str) -> Dict: | |
| """处理卡片选择事件并生成概念解释 | |
| Args: | |
| concept_id: 选中的概念ID | |
| Returns: | |
| 包含面板更新和内容的字典 | |
| """ | |
| try: | |
| print(f"处理卡片选择: {concept_id}") | |
| # 首先检查缓存 | |
| cached_explanation = app_state.get_cached_card_explanation(concept_id) | |
| if cached_explanation: | |
| print("使用缓存的解释内容") | |
| # 直接返回缓存的内容,不需要生成加载动画 | |
| return { | |
| concept_detail_panel: gr.update(visible=True), | |
| concept_detail_content: gr.update(value=cached_explanation, visible=True) | |
| } | |
| # 获取概念信息 | |
| if not concept_id or concept_id not in app_state.nodes_dict: | |
| raise ValueError(f"无效的概念ID: {concept_id}") | |
| # 显示加载动画 | |
| loading_html = """ | |
| <div class="loading"> | |
| <div class="loading-spinner"></div> | |
| <div class="loading-text">正在生成概念解释...</div> | |
| </div> | |
| """ | |
| # 先返回加载状态 | |
| yield { | |
| concept_detail_panel: gr.update(visible=True), | |
| concept_detail_content: gr.update(value=loading_html, visible=True) | |
| } | |
| print("开始生成新的解释内容") | |
| explanation_content = generate_card_explanation(concept_id) | |
| # 缓存生成的内容 | |
| app_state.cache_card_explanation(concept_id, explanation_content) | |
| print(f"已缓存概念 {concept_id} 的解释内容") | |
| # 返回生成的内容 | |
| return { | |
| concept_detail_panel: gr.update(visible=True), | |
| concept_detail_content: gr.update(value=explanation_content, visible=True) | |
| } | |
| except Exception as e: | |
| import traceback | |
| print(f"生成解释时出错: {str(e)}") | |
| print(traceback.format_exc()) | |
| error_content = f""" | |
| <div class="error-message"> | |
| <h3>生成解释时出错</h3> | |
| <p>{str(e)}</p> | |
| </div> | |
| """ | |
| return { | |
| concept_detail_panel: gr.update(visible=True), | |
| concept_detail_content: gr.update(value=error_content, visible=True) | |
| } | |
| def create_interface(): | |
| """ | |
| Create enhanced Gradio interface with better layout and styling | |
| """ | |
| global expanded_concept_section, expanded_concept_name, expanded_concept_description | |
| global key_points, examples, misconceptions, learning_tips, close_expanded | |
| # Custom CSS with improved styling | |
| custom_css = """ | |
| /* Global styles */ | |
| body { | |
| font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Microsoft YaHei", "WenQuanYi Micro Hei", sans-serif; | |
| color: #333; | |
| background-color: #f7f9fc; | |
| } | |
| /* Section styling */ | |
| .section { | |
| background: white; | |
| border-radius: 15px; | |
| padding: 20px; | |
| margin-bottom: 20px; | |
| box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05); | |
| } | |
| .header-section { | |
| background: linear-gradient(135deg, #2193b0, #6dd5ed); | |
| color: white; | |
| padding: 20px; | |
| border-radius: 15px; | |
| margin-bottom: 30px; | |
| box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1); | |
| } | |
| .question-section { | |
| background: #f8f9fa; | |
| } | |
| /* Concept graph styling */ | |
| .concept-graph-container { | |
| margin: 20px 0; | |
| text-align: center; | |
| } | |
| .concept-graph-container img { | |
| max-width: 100%; | |
| border-radius: 10px; | |
| box-shadow: 0 4px 15px rgba(0,0,0,0.1); | |
| } | |
| /* Button styles */ | |
| .primary-button { | |
| background: linear-gradient(135deg, #3498db, #2980b9); | |
| border: none; | |
| color: white; | |
| padding: 10px 20px; | |
| border-radius: 5px; | |
| cursor: pointer; | |
| box-shadow: 0 4px 6px rgba(52, 152, 219, 0.3); | |
| transition: all 0.3s ease; | |
| } | |
| .primary-button:hover { | |
| transform: translateY(-2px); | |
| box-shadow: 0 6px 8px rgba(0, 0, 0, 0.1); | |
| } | |
| /* Tab styles */ | |
| .tabs { | |
| border-bottom: 2px solid #e0e0e0; | |
| } | |
| .tab-selected { | |
| color: #3498db; | |
| border-bottom: 2px solid #3498db; | |
| } | |
| /* Error message styling */ | |
| .error-message { | |
| color: #d32f2f; | |
| background: #ffebee; | |
| padding: 10px; | |
| border-radius: 5px; | |
| margin: 10px 0; | |
| border-left: 4px solid #d32f2f; | |
| } | |
| /* Concept card styles */ | |
| .concept-card { | |
| transition: all 0.3s ease; | |
| border: 1px solid #e0e0e0; | |
| border-radius: 12px; | |
| padding: 16px; | |
| margin-bottom: 16px; | |
| cursor: pointer; | |
| background-color: #fff; | |
| box-shadow: 0 2px 5px rgba(0,0,0,0.05); | |
| } | |
| .concept-card:hover { | |
| box-shadow: 0 5px 15px rgba(0,0,0,0.1); | |
| transform: translateY(-3px); | |
| border-color: #bdc3c7; | |
| } | |
| .selected-card { | |
| border-color: #3498db; | |
| background-color: rgba(52, 152, 219, 0.05); | |
| box-shadow: 0 0 0 2px rgba(52, 152, 219, 0.3); | |
| } | |
| """ | |
| # 自定义 JavaScript - 移动到HTML头部 | |
| custom_js_html = """ | |
| <script type="text/javascript"> | |
| // 确保函数在全局作用域定义 | |
| window.conceptCardClick = function(conceptId) { | |
| console.log('卡片点击:', conceptId); | |
| // 找到隐藏输入框并更新 | |
| const cardSelectionInput = document.getElementById('card-selection'); | |
| if (cardSelectionInput) { | |
| cardSelectionInput.value = conceptId; | |
| cardSelectionInput.dispatchEvent(new Event('input', { bubbles: true })); | |
| // 更新选中样式 | |
| document.querySelectorAll('.concept-card').forEach(card => { | |
| card.classList.remove('selected-card'); | |
| if (card.getAttribute('data-concept-id') === conceptId) { | |
| card.classList.add('selected-card'); | |
| } | |
| }); | |
| } | |
| } | |
| document.addEventListener('DOMContentLoaded', function() { | |
| // 增强图像显示 | |
| const graphContainer = document.getElementById('concept-graph'); | |
| if (graphContainer) { | |
| const observer = new MutationObserver(function(mutations) { | |
| mutations.forEach(function(mutation) { | |
| if (mutation.addedNodes && mutation.addedNodes.length > 0) { | |
| const img = graphContainer.querySelector('img'); | |
| if (img) { | |
| img.style.maxWidth = '100%'; | |
| img.style.height = 'auto'; | |
| img.style.borderRadius = '8px'; | |
| img.style.boxShadow = '0 4px 8px rgba(0,0,0,0.1)'; | |
| } | |
| } | |
| }); | |
| }); | |
| observer.observe(graphContainer, { childList: true, subtree: true }); | |
| } | |
| }); | |
| </script> | |
| """ | |
| with gr.Blocks(css=custom_css, title="Educational LLM Assistant") as demo: | |
| # 添加自定义 JavaScript - 确保作为头部内容 | |
| gr.HTML(custom_js_html, elem_id="custom-js") | |
| # Header section | |
| with gr.Row(elem_classes="header-section"): | |
| with gr.Column(scale=2): | |
| gr.Markdown("# 🎓 Educational LLM Assistant") | |
| gr.Markdown("Interactive Learning Through AI-Powered Concept Breakdown") | |
| # Main content container | |
| with gr.Row(): | |
| # Left column - Profile and Question | |
| with gr.Column(scale=1): | |
| # Profile section | |
| with gr.Group(elem_classes="section"): | |
| gr.Markdown("### 👤 Learning Profile") | |
| with gr.Row(): | |
| grade_input = gr.Dropdown( | |
| choices=["Elementary", "Middle School", "High School", "College", "Graduate"], | |
| label="Grade Level", | |
| value="High School" | |
| ) | |
| subject_input = gr.Dropdown( | |
| choices=["Math", "Physics", "Chemistry", "Biology", "Computer Science"], | |
| label="Subject", | |
| value="Math" | |
| ) | |
| needs_input = gr.TextArea( | |
| label="Learning Goals", | |
| placeholder="What do you want to achieve?", | |
| lines=3 | |
| ) | |
| profile_btn = gr.Button("Save Profile", elem_classes="primary-button") | |
| profile_status = gr.Markdown("*No profile set*") | |
| # Question section | |
| with gr.Group(elem_classes="section question-section"): | |
| gr.Markdown("### ❓ Your Question") | |
| question_input = gr.TextArea( | |
| label="Enter your question", | |
| placeholder="What would you like to learn about?", | |
| lines=4 | |
| ) | |
| question_submit_btn = gr.Button( | |
| "Analyze Question", | |
| elem_classes="primary-button" | |
| ) | |
| # Answer section | |
| with gr.Group(visible=False, elem_classes="section answer-section") as answer_section: | |
| gr.Markdown("### 📝 Direct Answer") | |
| question_answer = gr.HTML( | |
| value="", | |
| elem_classes="answer-box" | |
| ) | |
| # 新增可视化生成面板 | |
| with gr.Group(visible=False) as concept_detail_panel: | |
| gr.Markdown("### 🎯 概念详解") | |
| concept_detail_content = gr.HTML( | |
| value="", | |
| elem_classes="concept-detail-box" | |
| ) | |
| # Right column - Concept Map and Explanation | |
| with gr.Column(scale=2): | |
| # Concept map section | |
| with gr.Group(visible=False, elem_classes="section") as concepts_section: | |
| gr.Markdown("### 🔍 Knowledge Map") | |
| # 使用HTML代替Plot | |
| concept_graph = gr.HTML( | |
| label="Concept Graph", | |
| elem_id="concept-graph", | |
| elem_classes="concept-graph-container" | |
| ) | |
| with gr.Row(): | |
| concept_cards = gr.HTML( | |
| label="Related Concepts", | |
| elem_classes="concept-cards-area" | |
| ) | |
| # Explanation section | |
| with gr.Group(visible=False, elem_classes="section") as explanation_section: | |
| explanation_header = gr.Markdown("### 📚 Concept Explanation") | |
| with gr.Tabs(elem_classes="tabs") as explanation_tabs: | |
| with gr.TabItem("📖 Explanation", elem_classes="tab-content"): | |
| explanation_content = gr.Markdown() | |
| with gr.TabItem("📝 Examples", elem_classes="tab-content"): | |
| examples_content = gr.HTML() | |
| with gr.TabItem("🔖 Resources", elem_classes="tab-content"): | |
| resources_content = gr.HTML() | |
| with gr.TabItem("✏️ Practice", elem_classes="tab-content"): | |
| practice_content = gr.HTML() | |
| back_btn = gr.Button( | |
| "← Back to Concept Map", | |
| elem_classes="primary-button" | |
| ) | |
| # 添加扩展内容部分 | |
| with gr.Group(visible=False, elem_classes="section") as expanded_concept_section: | |
| gr.Markdown("### 📚 Expanded Concept Details") | |
| expanded_concept_name = gr.Markdown("") | |
| expanded_concept_description = gr.Markdown("") | |
| with gr.Accordion("Key Points", open=True): | |
| key_points = gr.Markdown("") | |
| with gr.Accordion("Examples", open=True): | |
| examples = gr.Markdown("") | |
| with gr.Accordion("Common Misconceptions", open=True): | |
| misconceptions = gr.Markdown("") | |
| with gr.Accordion("Learning Tips", open=True): | |
| learning_tips = gr.Markdown("") | |
| close_expanded = gr.Button("Back to Concepts", variant="secondary") | |
| # Error message | |
| error_msg = gr.Markdown(visible=False, elem_classes="error-message") | |
| # 隐藏的概念选择输入框 | |
| concept_selection = gr.Textbox(visible=False, elem_id="concept-selection") | |
| # 新增的卡片点击选择输入框 | |
| card_selection = gr.Textbox(visible=False, elem_id="card-selection") | |
| # Event bindings | |
| profile_btn.click( | |
| update_profile, | |
| [grade_input, subject_input, needs_input], | |
| profile_status | |
| ) | |
| question_submit_btn.click( | |
| fn=analyze_question, | |
| inputs=[question_input, grade_input, subject_input, needs_input], | |
| outputs=[ | |
| answer_section, | |
| question_answer, | |
| concept_graph, | |
| concept_cards, | |
| concepts_section, | |
| error_msg, | |
| # 重置卡片解释部分 | |
| concept_detail_panel | |
| ], | |
| api_name=False, | |
| show_progress=True, | |
| ) | |
| concept_selection.change( | |
| fn=handle_concept_click, | |
| inputs=[concept_selection], | |
| outputs=[ | |
| explanation_header, | |
| explanation_content, | |
| examples_content, | |
| resources_content, | |
| practice_content, | |
| concepts_section, | |
| explanation_section, | |
| error_msg | |
| ] | |
| ) | |
| # 新增的卡片点击事件处理 | |
| card_selection.input( | |
| fn=handle_card_selection, | |
| inputs=[card_selection], | |
| outputs=[ | |
| concept_detail_panel, | |
| concept_detail_content | |
| ], | |
| api_name="handle_card_click" | |
| ) | |
| back_btn.click( | |
| fn=back_to_concepts, | |
| inputs=None, | |
| outputs=[concepts_section, explanation_section] | |
| ) | |
| close_expanded.click( | |
| fn=lambda: { | |
| expanded_concept_section: gr.update(visible=False), | |
| concepts_section: gr.update(visible=True) | |
| }, | |
| inputs=[], | |
| outputs=[expanded_concept_section, concepts_section] | |
| ) | |
| # 挂载Gradio应用 | |
| gr.mount_gradio_app(app, demo, path="/") | |
| return demo | |
| if __name__ == "__main__": | |
| demo = create_interface() | |
| demo.launch(server_name="0.0.0.0", server_port=7860) |