"""
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_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"""
"""
# 生成概念卡片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
{''.join([f'- {point}
' for point in response.get('key_points', [])])}
📝 Example Analysis
{''.join([
f'''
Example:{example.get('problem', '')}
Solution:{example.get('solution', '')}
'''
for example in response.get('examples', [])
])}
✍️ Practice Problems
{''.join([
f'''
Problem:{practice.get('question', '')}
View Answer
{practice.get('answer', '')}
'''
for practice in response.get('practice', [])
])}
📚 Learning Resources
{''.join([
f'''
{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"""
"""
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()