Spaces:
Running
Running
""" | |
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 路由部分 | |
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] | |
) | |
# 添加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() | |