NexusLearnAI / app.py
ChaseHan's picture
Create app.py
4174457 verified
raw
history blame
54 kB
"""
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 = '<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()
# 使用uvicorn启动
import uvicorn
uvicorn.run(app, host="0.0.0.0", port=7861)