""" 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 # Custom CSS styles custom_css = """ /* Global styles */ body { font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Microsoft YaHei", "WenQuanYi Micro Hei", sans-serif; color: #333; background-color: #f7f9fc; } /* Heading styles */ h1 { color: #2c3e50; font-weight: 700; margin-bottom: 1rem; } h2, h3, h4 { color: #3498db; font-weight: 600; } /* Button styles */ button.primary { background: linear-gradient(135deg, #3498db, #2980b9); border: none; box-shadow: 0 4px 6px rgba(52, 152, 219, 0.3); } button.secondary { background: #ecf0f1; color: #2980b9; border: 1px solid #bdc3c7; } button:hover { transform: translateY(-2px); box-shadow: 0 6px 8px rgba(0, 0, 0, 0.1); transition: all 0.3s ease; } /* Input fields enhancement */ input, textarea, select { border: 1px solid #ddd; border-radius: 8px; padding: 10px; box-shadow: inset 0 1px 3px rgba(0, 0, 0, 0.05); transition: all 0.3s ease; } input:focus, textarea:focus, select:focus { border-color: #3498db; box-shadow: 0 0 0 2px rgba(52, 152, 219, 0.25); } /* Tab styles */ .tabs { border-bottom: 2px solid #e0e0e0; } .tab-selected { color: #3498db; border-bottom: 2px solid #3498db; } /* 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); } .concept-title { font-weight: bold; margin-bottom: 8px; color: #2c3e50; font-size: 1.1em; } .concept-desc { font-size: 0.95em; color: #7f8c8d; line-height: 1.5; } /* Knowledge graph styles */ #concept-graph { background-color: #fff; border-radius: 12px; padding: 16px; box-shadow: 0 2px 10px rgba(0,0,0,0.05); } #concept-graph img { max-width: 100%; height: auto; border-radius: 8px; box-shadow: 0 4px 15px rgba(0,0,0,0.1); transition: all 0.3s ease; } #concept-graph img:hover { box-shadow: 0 8px 25px rgba(0,0,0,0.15); } /* Example box styles */ .example-box { background-color: #f1f8fe; border-left: 4px solid #3498db; padding: 15px; margin: 15px 0; border-radius: 0 8px 8px 0; } .example-box h4 { margin-top: 0; color: #2980b9; } /* Resource item styles */ .resource-item { padding: 12px; margin: 10px 0; border-bottom: 1px dashed #e0e0e0; transition: all 0.2s ease; } .resource-item:hover { background-color: #f9f9f9; } /* Details and answers styles */ details { margin: 10px 0; padding: 10px; border: 1px solid #e0e0e0; border-radius: 8px; background-color: #f9f9f9; } summary { cursor: pointer; color: #3498db; font-weight: 600; padding: 5px; } summary:hover { color: #2980b9; } /* Layout container styles */ .container { background-color: #fff; border-radius: 12px; padding: 20px; box-shadow: 0 4px 6px rgba(0,0,0,0.05); margin-bottom: 20px; } /* Responsive adjustments */ @media (max-width: 768px) { .concept-card { padding: 12px; } .example-box { padding: 12px; } } /* Answer box styles */ .answer-box { background-color: #f8f9fa; border-radius: 12px; padding: 20px; margin: 15px 0; border: 1px solid #e9ecef; box-shadow: 0 2px 4px rgba(0,0,0,0.05); } .answer-content { margin-bottom: 15px; line-height: 1.6; color: #2c3e50; } .answer-content h4 { color: #3498db; margin-top: 0; margin-bottom: 10px; } .main-concept { background-color: #e3f2fd; padding: 10px 15px; border-radius: 8px; color: #1976d2; font-size: 0.95em; } .main-concept strong { color: #1565c0; } /* Answer section styles */ .answer-section { margin-top: 20px; background: white; border-radius: 15px; padding: 20px; } .answer-box { background-color: #f8f9fa; border-radius: 12px; padding: 20px; margin: 15px 0; border: 1px solid #e9ecef; box-shadow: 0 2px 4px rgba(0,0,0,0.05); transition: all 0.3s ease; } .answer-box:hover { box-shadow: 0 4px 8px rgba(0,0,0,0.1); } .answer-content { margin-bottom: 15px; line-height: 1.6; color: #2c3e50; font-size: 1.1em; } .main-concept { background-color: #e3f2fd; padding: 12px 16px; border-radius: 8px; color: #1976d2; font-size: 0.95em; margin-top: 15px; border: 1px solid rgba(25, 118, 210, 0.1); } .main-concept strong { color: #1565c0; font-weight: 600; } /* Loading animation styles */ .loading { padding: 20px; text-align: center; color: #666; font-size: 1.1em; position: relative; } .loading:after { content: '...'; position: absolute; animation: dots 1.5s steps(5, end) infinite; } @keyframes dots { 0%, 20% { content: '.'; } 40% { content: '..'; } 60% { content: '...'; } 80%, 100% { content: ''; } } .loading::before { content: ''; display: block; width: 30px; height: 30px; border: 3px solid #3498db; border-top-color: transparent; border-radius: 50%; margin: 0 auto 10px; animation: spin 1s linear infinite; } @keyframes spin { to { transform: rotate(360deg); } } /* 概念详解面板样式 */ .concept-explanation { padding: 20px; background: white; border-radius: 12px; box-shadow: 0 2px 8px rgba(0,0,0,0.1); } .explanation-content { line-height: 1.6; margin: 15px 0; color: #2c3e50; } .examples-section { margin-top: 20px; } .example-box { background: #f8f9fa; border-left: 4px solid #3498db; padding: 15px; margin: 15px 0; border-radius: 0 8px 8px 0; } .example-problem { margin-bottom: 10px; color: #2c3e50; } .example-solution { color: #34495e; padding: 10px; background: rgba(52, 152, 219, 0.05); border-radius: 4px; } /* 加载动画样式 */ .loading { text-align: center; padding: 20px; } .loading-spinner { border: 3px solid #f3f3f3; border-top: 3px solid #3498db; border-radius: 50%; width: 30px; height: 30px; animation: spin 1s linear infinite; margin: 0 auto 10px; } @keyframes spin { 0% { transform: rotate(0deg); } 100% { transform: rotate(360deg); } } .loading-text { color: #666; font-size: 0.9em; } /* 新增的样式 */ .concept-explanation-container { margin-top: 20px; padding: 15px; background: white; border-radius: 12px; box-shadow: 0 2px 8px rgba(0,0,0,0.1); transition: all 0.3s ease; max-height: 600px; /* 固定最大高度 */ overflow-y: auto; /* 添加垂直滚动条 */ /* 自定义滚动条样式 */ scrollbar-width: thin; scrollbar-color: #3498db #f1f1f1; } /* Webkit浏览器的滚动条样式 */ .concept-explanation-container::-webkit-scrollbar { width: 8px; } .concept-explanation-container::-webkit-scrollbar-track { background: #f1f1f1; border-radius: 4px; } .concept-explanation-container::-webkit-scrollbar-thumb { background: #3498db; border-radius: 4px; } .concept-explanation-container::-webkit-scrollbar-thumb:hover { background: #2980b9; } .concept-explanation-container h3 { color: #2c3e50; margin-bottom: 15px; position: sticky; top: 0; background: white; padding: 10px 0; z-index: 1; } /* 内容区域样式 */ .card-explanation { padding: 20px; background: white; border-radius: 12px; } .explanation-section, .key-points-section, .examples-section, .practice-section, .resources-section { margin-top: 20px; padding: 15px; background: #f8f9fa; border-radius: 8px; } .explanation-section h4, .key-points-section h4, .examples-section h4, .practice-section h4, .resources-section h4 { color: #2c3e50; margin-bottom: 10px; } /* 修改generate_card_explanation函数中的标题文本 */ .explanation-section h4:before { content: "📚 Concept Explanation"; } .key-points-section h4:before { content: "🎯 Key Points"; } .examples-section h4:before { content: "📝 Example Analysis"; } .practice-section h4:before { content: "✍️ Practice Problems"; } .resources-section h4:before { content: "📚 Learning Resources"; } /* 加载动画容器 */ .loading-container { display: flex; flex-direction: column; align-items: center; justify-content: center; padding: 40px; background: white; border-radius: 12px; box-shadow: 0 2px 8px rgba(0,0,0,0.1); } /* 加载动画旋转器 */ .loading-spinner { width: 50px; height: 50px; border: 4px solid #f3f3f3; border-top: 4px solid #3498db; border-radius: 50%; animation: spin 1s linear infinite; margin-bottom: 20px; } /* 加载文本 */ .loading-text { color: #666; font-size: 1.1em; margin-top: 10px; } /* 旋转动画 */ @keyframes spin { 0% { transform: rotate(0deg); } 100% { transform: rotate(360deg); } } """ # 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() # 修改 FastAPI 路由部分 @app.post("/trigger_llm") 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 = '
' 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"""
正在加载概念解释...
`; 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}')">

{concept_name}

{concept.get('difficulty', 'basic')}

{concept_description}

""" cards_html += """ """ 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="
Analyzing your question...
"), # 显示加载信息 gr.update(value="
Generating concept map...
"), # 显示加载信息 gr.update(value="
Preparing concept cards...
"), # 显示加载信息 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"""
{concept_map["Explanation"]}
Main Concept: {concept_map.get("main_concept", "")}
""" # 生成可视化图 graph_data_url = create_network_graph(concept_map) graph_html = f"""
Concept Knowledge Graph
""" # 生成概念卡片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 = "
" for idx, example in enumerate(explanation_data.get("examples", [])): examples_html += f"""

Example {idx+1} ({example.get('difficulty', 'Difficulty not specified')})

Problem: {example.get('problem', 'None')}

Solution:

{example.get('solution', 'None')}

""" examples_html += "
" # Format resources resources_html = "
" if explanation_data.get("resources"): for res in explanation_data.get("resources", []): link_html = f"View Resource" if res.get('link') else "" resources_html += f"""

{res.get('type', 'Resource')}: {res.get('title', 'Unnamed resource')}

{res.get('description', 'No description')}

{link_html}
""" else: resources_html += "

No related learning resources available

" resources_html += "
" # Format practice problems practice_html = "
" if explanation_data.get("practice_questions"): for idx, question in enumerate(explanation_data.get("practice_questions", [])): practice_html += f"""

Practice Problem {idx+1} ({question.get('difficulty', 'Difficulty not specified')})

Question: {question.get('question', 'None')}

View Answer

{question.get('answer', 'None')}

""" else: practice_html += "

No practice problems available

" practice_html += "
" 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"""

{concept_name}

📚 Concept Explanation

{response.get('explanation', 'No explanation available')}

🎯 Key Points

📝 Example Analysis

{''.join([ f'''
{example.get('difficulty', 'Basic')}
Example:{example.get('problem', '')}
Solution:{example.get('solution', '')}
''' for example in response.get('examples', []) ])}

✍️ Practice Problems

{''.join([ f'''
{practice.get('difficulty', 'Basic')}
Problem:{practice.get('question', '')}
View Answer
{practice.get('answer', '')}
''' for practice in response.get('practice', []) ])}

📚 Learning Resources

{''.join([ f'''
{resource.get('type', 'Resource')}
{resource.get('title', '')}

{resource.get('description', '')}

{f'View Resource' if resource.get('link') else ''}
''' for resource in response.get('resources', []) ])}
""" # 缓存结果 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"""

无法生成详细解释

{error_msg}

基本概念信息:

{concept_name}: {concept_description}

""" 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 = """
正在生成概念解释...
""" # 先返回加载状态 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"""

生成解释时出错

{str(e)}

""" 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 = """ """ 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] ) # 添加FastAPI集成 gr.mount_gradio_app(app, demo, path="/") return demo if __name__ == "__main__": demo = create_interface() demo.launch() # # 使用uvicorn启动 # import uvicorn # uvicorn.run(app, host="0.0.0.0", port=7861) # if __name__ == "__main__": # demo.launch()