0841b1a103
- 前端:Vue3 + TS + Element Plus,24 个页面路由(课件/组题/教案/动画/思维导图/作文批改/命题/课堂/资源/社区等) - 后端:FastAPI + SQLAlchemy + SQLite,17 个路由模块,AI 服务层含降级模板 - AI:glm-5.x 推理模型已禁用思维链,确保输出真实内容 - 修复:ai_service 两处请求体注入 enable_thinking/thinking disabled - 测试账号:13900999999 / test1234
3191 lines
222 KiB
Python
3191 lines
222 KiB
Python
import json
|
||
import re
|
||
import html as html_lib
|
||
import httpx
|
||
import logging
|
||
import time
|
||
import base64
|
||
from config import get_settings
|
||
|
||
settings = get_settings()
|
||
logger = logging.getLogger(__name__)
|
||
|
||
|
||
class AIService:
|
||
# Shared circuit-breaker timestamp: all instances share one breaker so a
|
||
# single AI outage trips the breaker globally across every router.
|
||
_ai_unavailable_until: float = 0.0
|
||
|
||
def __init__(self):
|
||
self.api_key = settings.AI_API_KEY
|
||
self.api_base = settings.AI_API_BASE.rstrip("/")
|
||
self.model = settings.AI_MODEL
|
||
self.client = httpx.AsyncClient(
|
||
base_url=self.api_base,
|
||
headers={"Authorization": f"Bearer {self.api_key}"},
|
||
timeout=httpx.Timeout(connect=10.0, read=120.0, write=15.0, pool=10.0),
|
||
)
|
||
|
||
async def _chat(self, messages: list[dict], temperature: float = 0.7, max_tokens: int = 8192) -> str:
|
||
if time.monotonic() < AIService._ai_unavailable_until:
|
||
logger.warning("AI provider skipped (circuit breaker active for %.0fs more)", AIService._ai_unavailable_until - time.monotonic())
|
||
raise RuntimeError("AI_PROVIDER_UNAVAILABLE")
|
||
try:
|
||
response = await self.client.post(
|
||
"/chat/completions",
|
||
json={
|
||
"model": self.model,
|
||
"messages": messages,
|
||
"temperature": temperature,
|
||
"max_tokens": max_tokens,
|
||
# glm-5.x is a reasoning model: without disabling
|
||
# thinking it burns the entire token budget on
|
||
# chain-of-thought and returns EMPTY content
|
||
# (finish_reason=length), forcing every feature to
|
||
# fall back to static templates. Disable for real output.
|
||
"enable_thinking": False,
|
||
"thinking": {"type": "disabled"},
|
||
},
|
||
)
|
||
response.raise_for_status()
|
||
except httpx.ConnectError as exc:
|
||
# Connection-level failure: server likely down. Short breaker so we retry sooner.
|
||
logger.error("AI provider connect failed: %s: %s", type(exc).__name__, exc)
|
||
AIService._ai_unavailable_until = time.monotonic() + 20
|
||
raise RuntimeError("AI_PROVIDER_CONNECT_ERROR") from exc
|
||
except httpx.TimeoutException as exc:
|
||
# Timeout (connect/read): server slow or unreachable. Short breaker.
|
||
logger.error("AI provider timeout: %s: %s", type(exc).__name__, exc)
|
||
AIService._ai_unavailable_until = time.monotonic() + 20
|
||
raise RuntimeError("AI_PROVIDER_TIMEOUT") from exc
|
||
except httpx.HTTPStatusError as exc:
|
||
# HTTP error (4xx/5xx): server is up but rejected the request.
|
||
logger.error("AI provider HTTP %s: %s", exc.response.status_code, str(exc.response.text)[:200])
|
||
breaker = 30 if exc.response.status_code >= 500 else 120
|
||
AIService._ai_unavailable_until = time.monotonic() + breaker
|
||
raise RuntimeError("AI_PROVIDER_HTTP_ERROR") from exc
|
||
except Exception as exc:
|
||
logger.error("AI provider request failed: %s: %s", type(exc).__name__, exc)
|
||
AIService._ai_unavailable_until = time.monotonic() + 30
|
||
raise RuntimeError("AI_PROVIDER_UNAVAILABLE") from exc
|
||
data = response.json()
|
||
content = data["choices"][0]["message"]["content"] or ""
|
||
# Reasoning models may return empty content if thinking consumed the token budget
|
||
if not content.strip():
|
||
logger.warning("AI provider returned empty content (finish=%s, model=%s) - reasoning budget likely exhausted", data["choices"][0].get("finish_reason","?"), self.model)
|
||
raise RuntimeError("AI_PROVIDER_EMPTY_RESPONSE")
|
||
finish_reason = data["choices"][0].get("finish_reason", "")
|
||
if finish_reason == "length":
|
||
content = content + '..."}'
|
||
return content
|
||
|
||
async def _chat_json(self, messages: list[dict], temperature: float = 0.3, max_tokens: int = 8192) -> dict:
|
||
content = await self._chat(messages, temperature, max_tokens)
|
||
return self._extract_json(content)
|
||
|
||
|
||
async def ocr_image(self, image_bytes: bytes, filename: str = "") -> str:
|
||
"""Extract text from an image using the AI model's vision capability."""
|
||
if not self.api_key or self.api_key == "change-me-in-production":
|
||
raise RuntimeError("AI_PROVIDER_UNAVAILABLE")
|
||
if time.monotonic() < AIService._ai_unavailable_until:
|
||
raise RuntimeError("AI_PROVIDER_UNAVAILABLE")
|
||
|
||
b64 = base64.b64encode(image_bytes).decode("ascii")
|
||
ext = "png"
|
||
lower_name = (filename or "").lower()
|
||
if lower_name.endswith((".jpg", ".jpeg")):
|
||
ext = "jpeg"
|
||
elif lower_name.endswith(".webp"):
|
||
ext = "webp"
|
||
elif lower_name.endswith(".gif"):
|
||
ext = "gif"
|
||
|
||
messages = [
|
||
{
|
||
"role": "user",
|
||
"content": [
|
||
{
|
||
"type": "text",
|
||
"text": (
|
||
"请仔细识别这张图片中的所有文字内容,包括题目、公式、表格和标注。"
|
||
"完整提取文字,保持原有结构和层次。"
|
||
"如果是题目,请保留题号和选项格式。"
|
||
"数学公式用可读文本表示(例如 a² + b² = c²)。"
|
||
"只输出识别到的文字内容,不要添加解释或说明。"
|
||
"如果图片中没有文字或无法识别,请回复:[无法识别文字内容]"
|
||
),
|
||
},
|
||
{
|
||
"type": "image_url",
|
||
"image_url": {"url": f"data:image/{ext};base64,{b64}"},
|
||
},
|
||
],
|
||
}
|
||
]
|
||
|
||
try:
|
||
response = await self.client.post(
|
||
"/chat/completions",
|
||
json={
|
||
"model": self.model,
|
||
"messages": messages,
|
||
"temperature": 0.1,
|
||
"max_tokens": 4096,
|
||
"enable_thinking": False,
|
||
"thinking": {"type": "disabled"},
|
||
},
|
||
timeout=httpx.Timeout(connect=10.0, read=90.0, write=15.0, pool=10.0),
|
||
)
|
||
response.raise_for_status()
|
||
except Exception as exc:
|
||
logger.error("Vision OCR request failed: %s", exc)
|
||
AIService._ai_unavailable_until = time.monotonic() + 60
|
||
raise RuntimeError("AI_PROVIDER_UNAVAILABLE") from exc
|
||
|
||
data = response.json()
|
||
return data["choices"][0]["message"]["content"].strip()
|
||
|
||
def _extract_json(self, content: str) -> dict:
|
||
content = content.strip()
|
||
|
||
# Strip markdown code fences
|
||
import re as _re
|
||
content = _re.sub(r'^```\w*\n?', '', content)
|
||
content = _re.sub(r'\n?```$', '', content)
|
||
content = content.strip()
|
||
|
||
first_brace = content.find('{')
|
||
if first_brace == -1:
|
||
return {"raw_content": content}
|
||
|
||
last_brace = content.rfind('}')
|
||
if last_brace <= first_brace:
|
||
return {"raw_content": content}
|
||
|
||
# Try the full span first
|
||
candidate = content[first_brace:last_brace + 1]
|
||
try:
|
||
result = json.loads(candidate)
|
||
if isinstance(result, dict):
|
||
return result
|
||
except json.JSONDecodeError:
|
||
pass
|
||
|
||
# Try auto-fixing truncated JSON by closing open structures
|
||
fixed = self._try_fix_truncated_json(candidate)
|
||
if fixed:
|
||
return fixed
|
||
|
||
# Try replacing HTML double quotes with single quotes inside the html field
|
||
fixed2 = _re.sub(r'(style|onclick|onchange)=\\?"([^"]*?)\\?"', r"\1='\2'", candidate)
|
||
if fixed2 != candidate:
|
||
try:
|
||
result = json.loads(fixed2)
|
||
if isinstance(result, dict):
|
||
return result
|
||
except json.JSONDecodeError:
|
||
pass
|
||
|
||
# Find all '}' positions from end to start
|
||
pos = last_brace
|
||
while pos > first_brace:
|
||
if content[pos] == '}':
|
||
try:
|
||
result = json.loads(content[first_brace:pos + 1])
|
||
if isinstance(result, dict):
|
||
return result
|
||
except json.JSONDecodeError:
|
||
pass
|
||
pos -= 1
|
||
|
||
# Last resort: try to extract html field via regex
|
||
html_match = _re.search(r'"html"\s*:\s*"((?:[^"\\]|\\.)*)"', content, _re.DOTALL)
|
||
if html_match:
|
||
try:
|
||
html_val = json.loads('"' + html_match.group(1) + '"')
|
||
return {"title": "动画", "description": "", "html": html_val}
|
||
except:
|
||
pass
|
||
|
||
# All attempts failed - save debug info
|
||
try:
|
||
with open("debug_json_fail.txt", "w", encoding="utf-8") as f:
|
||
f.write(f"Content length: {len(content)}\n")
|
||
f.write(f"First 500 chars:\n{content[:500]}\n\n")
|
||
f.write(f"Last 500 chars:\n{content[-500:]}\n\n")
|
||
f.write(f"Full content:\n{content}\n")
|
||
except:
|
||
pass
|
||
|
||
return {"raw_content": content}
|
||
|
||
def _try_fix_truncated_json(self, candidate: str) -> dict | None:
|
||
"""Try to fix truncated JSON by closing open strings/brackets."""
|
||
import re as _re
|
||
# Count brackets
|
||
opens = candidate.count('{') - candidate.count('}')
|
||
opens_bracket = candidate.count('[') - candidate.count(']')
|
||
|
||
# Check if we're inside a string
|
||
in_string = False
|
||
escape = False
|
||
for ch in candidate:
|
||
if escape:
|
||
escape = False
|
||
continue
|
||
if ch == '\\':
|
||
escape = True
|
||
continue
|
||
if ch == '"':
|
||
in_string = not in_string
|
||
|
||
fixed = candidate
|
||
if in_string:
|
||
fixed += ''
|
||
# Close open string if inside one
|
||
if in_string:
|
||
fixed += '"'
|
||
# Close open brackets
|
||
for _ in range(max(0, opens_bracket)):
|
||
fixed += ']'
|
||
for _ in range(max(0, opens)):
|
||
fixed += '}'
|
||
|
||
try:
|
||
result = json.loads(fixed)
|
||
if isinstance(result, dict):
|
||
# If html field exists, this is usable
|
||
if 'html' in result:
|
||
return result
|
||
except json.JSONDecodeError:
|
||
pass
|
||
|
||
# Try simpler fix: just close the outermost object
|
||
if in_string:
|
||
# Find last complete key-value pair before truncation
|
||
# Try: add closing quote, skip to end of object
|
||
for suffix in ['"}', '"}]}', '"}]}', '"]}', '"}' ]:
|
||
try:
|
||
result = json.loads(candidate + suffix)
|
||
if isinstance(result, dict):
|
||
return result
|
||
except json.JSONDecodeError:
|
||
continue
|
||
|
||
return None
|
||
|
||
async def chat_teaching(self, history: list[dict], user_message: str, subject: str = "", grade: str = "") -> str:
|
||
"""General-purpose conversational teaching assistant (multi-turn)."""
|
||
context_bits = []
|
||
if subject:
|
||
context_bits.append(f"任教学科:{subject}")
|
||
if grade:
|
||
context_bits.append(f"教学学段/年级:{grade}")
|
||
context_line = (";".join(context_bits) + "。") if context_bits else ""
|
||
|
||
system_prompt = (
|
||
"你是“智教助手”,一位经验丰富、耐心细致的 K12 全学科教学顾问与备课搭档。"
|
||
"你的任务是帮助一线教师解决真实的教学问题:备课思路、知识点讲解、活动设计、"
|
||
"课堂提问、学情诊断、家校沟通、教法选择等。\n"
|
||
"回答要求:\n"
|
||
"1. 用简体中文回答,语气专业、亲切、可落地,避免空话套话。\n"
|
||
"2. 结构清晰,善用标题、要点、表格和示例,必要时分步骤说明。\n"
|
||
"3. 紧扣中国课程标准和一线课堂实际,给出可直接使用的内容(如完整提问、活动脚本、讲解话术)。\n"
|
||
"4. 遇到模糊请求时主动追问关键信息(年级、学科、目标、时长)再给出方案。\n"
|
||
"5. 对学生的易错点、认知难点要有针对性提醒;涉及数值/事实要准确。\n"
|
||
f"{context_line}"
|
||
)
|
||
|
||
messages: list[dict] = [{"role": "system", "content": system_prompt}]
|
||
for turn in (history or []):
|
||
role = turn.get("role")
|
||
content = (turn.get("content") or "").strip()
|
||
if role in {"user", "assistant"} and content:
|
||
messages.append({"role": role, "content": content})
|
||
messages.append({"role": "user", "content": user_message})
|
||
try:
|
||
return await self._chat(messages, temperature=0.6, max_tokens=8192)
|
||
except RuntimeError:
|
||
return self._fallback_chat(history or [], user_message, subject, grade)
|
||
|
||
async def generate_mindmap(self, topic: str, subject: str = "综合", grade: str = "") -> dict:
|
||
"""生成知识思维导图,返回层级节点结构。"""
|
||
topic = self._repair_text(topic)
|
||
grade_hint = f",适用学段:{grade}" if grade else ""
|
||
system_prompt = f"""你是一位资深学科教学专家,擅长梳理知识结构。根据教师给出的主题,生成一份层次清晰、内容专业的知识思维导图。
|
||
|
||
要求:
|
||
- 围绕主题梳理 4 到 6 个一级分支(核心知识维度)
|
||
- 每个一级分支下展开 2 到 4 个二级子节点
|
||
- 节点标题用精炼短语(4 到 14 字),不要长句
|
||
- 内容要符合学科规范与教学逻辑,覆盖该主题的核心知识脉络(学科:{subject}{grade_hint})
|
||
- 如果主题偏小众,也尽量给出合理、可教学的结构
|
||
|
||
直接输出JSON(不要markdown代码块,不要其他文字):
|
||
{{"title":"主题名","nodes":[{{"title":"一级分支标题","color":"#00a870","children":[{{"title":"二级子节点标题","children":[{{"title":"可展开的细节要点"}}]}}]}}]}}"""
|
||
try:
|
||
result = await self._chat_json([
|
||
{"role": "system", "content": system_prompt},
|
||
{"role": "user", "content": topic},
|
||
], temperature=0.5, max_tokens=8192)
|
||
return self._normalize_mindmap_result(result, topic, subject)
|
||
except RuntimeError:
|
||
return self._fallback_mindmap(topic, subject)
|
||
|
||
def _normalize_mindmap_result(self, result: dict, topic: str, subject: str) -> dict:
|
||
topic = self._repair_text(topic)
|
||
nodes = result.get("nodes")
|
||
if not isinstance(nodes, list) or not nodes:
|
||
return self._fallback_mindmap(topic, subject)
|
||
palette = ["#00a870", "#2563eb", "#d97706", "#db2777", "#7c3aed", "#0891b2", "#4f46e5", "#ea580c"]
|
||
clean: list = []
|
||
for i, branch in enumerate(nodes):
|
||
if not isinstance(branch, dict) or not str(branch.get("title", "")).strip():
|
||
continue
|
||
color = branch.get("color") or palette[i % len(palette)]
|
||
children = self._collect_mindmap_children(branch.get("children"))
|
||
if not children:
|
||
continue
|
||
clean.append({"title": str(branch["title"]).strip()[:30], "color": color, "children": children})
|
||
if len(clean) < 2:
|
||
return self._fallback_mindmap(topic, subject)
|
||
return {"title": str(result.get("title") or topic)[:80], "nodes": clean[:8]}
|
||
|
||
def _collect_mindmap_children(self, raw) -> list:
|
||
if not isinstance(raw, list):
|
||
return []
|
||
out: list = []
|
||
for item in raw:
|
||
if isinstance(item, dict) and str(item.get("title", "")).strip():
|
||
title = str(item["title"]).strip()[:30]
|
||
sub = self._collect_mindmap_children(item.get("children"))
|
||
node = {"title": title}
|
||
if sub:
|
||
node["children"] = sub
|
||
out.append(node)
|
||
elif isinstance(item, str) and item.strip():
|
||
out.append({"title": item.strip()[:30]})
|
||
return out[:6]
|
||
|
||
def _fallback_chat(self, history: list, user_message: str, subject: str = "", grade: str = "") -> str:
|
||
"""AI 离线时的教学顾问兜底:按意图匹配给出可落地、结构化的教学建议。"""
|
||
subject = (subject or "").strip()
|
||
grade = (grade or "").strip()
|
||
msg = self._repair_text(user_message).strip()
|
||
ctx_bits = []
|
||
if subject:
|
||
ctx_bits.append(subject)
|
||
if grade:
|
||
ctx_bits.append(grade)
|
||
ctx = ("(" + "·".join(ctx_bits) + ")") if ctx_bits else ""
|
||
offline_note = (
|
||
"\n\n— 当前为离线知识模式(AI 云端未连通),以上为基于教学经验的通用建议;"
|
||
"如需针对你具体班级、教材版本的个性化方案,请在 AI 服务恢复后再次提问。"
|
||
)
|
||
|
||
if any(k in msg for k in ["你好", "您好", "在吗", "谢谢", "感谢", "辛苦"]) and len(msg) <= 12:
|
||
hi = "你好!我是智教助手,你的教学顾问与备课搭档。" + (f"已记录你的任教学段{ctx}。" if ctx else "")
|
||
return (
|
||
hi + "\n\n我可以帮你:备课与教学设计、知识点讲解思路、课堂活动与提问设计、"
|
||
"学情诊断与分层教学、家校沟通、复习备考、课堂管理、学生动机激发等。\n\n"
|
||
"直接告诉我你想解决的教学问题,例如:\n"
|
||
"- \"如何讲清分数乘法的算理?\"\n"
|
||
"- \"设计一节《草船借箭》的导入环节\"\n"
|
||
"- \"班上两极分化严重怎么办?\"\n"
|
||
"- \"家长群里如何回复投诉?\"" + offline_note
|
||
)
|
||
|
||
subj_resp = self._subject_advice(msg, subject, grade)
|
||
if subj_resp:
|
||
return subj_resp + offline_note
|
||
|
||
if any(k in msg for k in ["备课", "教学设计", "教案", "教学环节", "教学目标", "怎么设计", "如何设计", "设计一节", "设计这节", "课时方案"]):
|
||
return self._chat_lesson_prep(ctx) + offline_note
|
||
if any(k in msg for k in ["怎么讲", "如何讲", "怎么解释", "如何解释", "讲清楚", "讲不清", "学生不懂", "听不懂", "理解不了", "突破难点", "重难点", "算理", "为什么这样", "原理"]):
|
||
return self._chat_explain(ctx) + offline_note
|
||
if any(k in msg for k in ["导入", "开场", "引入", "情境", "激发兴趣", "吸引"]):
|
||
return self._chat_intro(ctx) + offline_note
|
||
if any(k in msg for k in ["课堂活动", "互动活动", "小组活动", "合作学习", "游戏", "动手", "探究", "角色扮演", "情境表演"]):
|
||
return self._chat_activity(ctx) + offline_note
|
||
if any(k in msg for k in ["提问", "追问", "问题设计", "提问技巧", "课堂提问"]):
|
||
return self._chat_questioning(ctx) + offline_note
|
||
if any(k in msg for k in ["纪律", "管纪律", "课堂管理", "课堂常规", "调皮", "捣乱", "走神", "注意力", "不专心", "讲话", "吵闹", "坐不住"]):
|
||
return self._chat_management(ctx) + offline_note
|
||
if any(k in msg for k in ["学困生", "后进生", "学困", "待优生", "跟不上", "两极分化", "差距大", "分层", "差异化", "因材施教", "培优补差"]):
|
||
return self._chat_differentiation(ctx) + offline_note
|
||
if any(k in msg for k in ["家长", "家校", "家访", "家长会", "家长群", "沟通家长", "投诉"]):
|
||
return self._chat_parents(ctx) + offline_note
|
||
if any(k in msg for k in ["作业", "布置作业", "评价", "批改", "反馈", "形成性评价", "作业量", "作业设计"]):
|
||
return self._chat_assessment(ctx) + offline_note
|
||
if any(k in msg for k in ["不想学", "没兴趣", "没动力", "厌学", "积极性", "不想听", "敷衍", "内驱力", "学习动机"]):
|
||
return self._chat_motivation(ctx) + offline_note
|
||
if any(k in msg for k in ["复习", "备考", "期末", "期中", "考试", "复习课", "冲刺", "考点", "应试"]):
|
||
return self._chat_review(ctx) + offline_note
|
||
if any(k in msg for k in ["新教师", "新手", "刚入职", "实习", "第一次上课", "新老师", "刚上班"]):
|
||
return self._chat_newteacher(ctx) + offline_note
|
||
|
||
return self._chat_general(msg, ctx) + offline_note
|
||
|
||
def _subject_advice(self, msg: str, subject: str, grade: str) -> str:
|
||
"""识别学科相关提问,返回学科特色建议;无匹配返回空串。"""
|
||
is_math = any(k in msg for k in ["数学", "算理", "算式", "方程", "函数", "几何", "分数", "小数", "乘法", "除法", "加减法", "应用题"])
|
||
is_chinese = any(k in msg for k in ["语文", "课文", "识字", "写字", "阅读理解", "作文", "古诗词", "古诗", "文言文", "拼音", "草船借箭", "荷塘月色"])
|
||
is_english = any(k in msg for k in ["英语", "单词", "语法", "口语", "听力", "时态", "从句", "because"])
|
||
is_physics = any(k in msg for k in ["物理", "力学", "电学", "电路", "牛顿", "压强", "浮力", "光学"])
|
||
if is_math:
|
||
return (
|
||
"【数学教学建议】\n\n"
|
||
"一、算理优于算法\n"
|
||
"数学核心是让学生懂\"为什么\",而非记\"怎么做\"。讲新运算先用实物/画图/数轴把算理可视化(如分数乘法用面积模型),再给法则。\n\n"
|
||
"二、三讲三练结构\n"
|
||
"- 讲例题(边讲边写思维过程)→ 练模仿题\n"
|
||
"- 讲变式(改一个条件/数据)→ 练变式题\n"
|
||
"- 讲错题(学生典型错误当反面教材)→ 练易错题\n\n"
|
||
"三、让错误可见\n"
|
||
"数学最怕假装会。多用板演、白板小测、手势即时暴露思维。错题不只对答案,让学生讲\"我当时怎么想的\"。\n\n"
|
||
"四、应用题三部曲\n"
|
||
"读题(圈关键信息)→ 画图/列表(文字转成关系)→ 列式(再算)。低年级卡第一步,高年级卡第二步。\n\n"
|
||
"告诉我具体课题(如\"分数除法\"\"平行四边形面积\"),我给更细的活动设计。"
|
||
)
|
||
if is_chinese:
|
||
return (
|
||
"【语文教学建议】\n\n"
|
||
"一、朗读是地基\n"
|
||
"低中年级把朗读做扎实:教师范读→跟读→指名读→齐读,注意停顿、重音、语气。朗读到位,理解自然跟上。\n\n"
|
||
"二、课文\"三问法\"\n"
|
||
"- 写了什么?(整体感知)\n"
|
||
"- 怎么写的?(品词析句、写法)\n"
|
||
"- 为什么这样写?(情感、主旨)\n\n"
|
||
"三、作文从\"有话写\"到\"写得好\"\n"
|
||
"先解决没素材:多搞观察课、活动作文。再解决写不具体:训练动作分解(把\"他跑了\"拆成 5 个动作)。最后才润色文采。\n\n"
|
||
"四、识字/字词\n"
|
||
"字理识字(讲字源)、归类识字(同偏旁)、语境识字(放词句里记),比机械抄写有效。\n\n"
|
||
"告诉我具体课文或课型,我给针对性设计。"
|
||
)
|
||
if is_english:
|
||
return (
|
||
"【英语教学建议】\n\n"
|
||
"一、输入先行(i+1 原则)\n"
|
||
"新语言点先大量听/看→理解,再要求说/写。用 TPR、图片、情境让学生先懂意思,不要一上来就机械跟读。\n\n"
|
||
"二、词不离句,句不离境\n"
|
||
"单词放进有意义的句子里记,句子放进情境里用。避免单词—中文翻译的孤立记忆,那样学生只会背不会用。\n\n"
|
||
"三、PPP 模式\n"
|
||
"Presentation(呈现)→ Practice(控制练习)→ Production(自由产出)。第三步最常被省略,却最关键——要给真实任务去用语言。\n\n"
|
||
"四、让沉默期变短\n"
|
||
"不爱开口的学生:先做非语言回应(指、选、画),再重复性输出(跟读),最后创造性输出。降低开口焦虑。\n\n"
|
||
"告诉我具体单元/课型,我给细化方案。"
|
||
)
|
||
if is_physics:
|
||
return (
|
||
"【物理教学建议】\n\n"
|
||
"一、实验是物理的灵魂\n"
|
||
"能做实验绝不只讲实验。演示实验要让现象出乎意料(引发认知冲突),分组实验让每个人动手。器材不足用生活物品替代(矿泉水瓶测压强)。\n\n"
|
||
"二、从现象到规律四步\n"
|
||
"观察现象 → 提出问题 → 猜想/设计实验 → 得出规律。规律不要直接给,让学生发现。\n\n"
|
||
"三、建模与数学化\n"
|
||
"讲公式时一定讲清每个字母的物理意义和单位,强调公式是规律的语言,而非要背的符号。\n\n"
|
||
"四、典型误区预防\n"
|
||
"力与运动(惯性误解)、电流方向、浮力沉浮条件……把这些高频误区编成判断题课前测,对症讲解。"
|
||
)
|
||
return ""
|
||
|
||
def _chat_lesson_prep(self, ctx: str) -> str:
|
||
return (
|
||
f"关于备课,建议你按\"目标—学情—活动—评价\"四步设计{ctx}:\n\n"
|
||
"一、明确可检验的教学目标(最关键)\n"
|
||
"用\"学生能做什么 + 什么条件下 + 达到什么程度\"来写。例:学生能在 5 分钟内独立完成 3 道两位数乘法,正确率≥80%。\n"
|
||
"避免\"让学生理解…\"\"培养…能力\"这类无法检测的目标。一节课 2-3 个核心目标即可。\n\n"
|
||
"二、诊断学情\n"
|
||
"这节课学生已有什么基础?常见误区是什么?课前用 2 道前测题摸底,或翻看上节课作业的典型错误。\n\n"
|
||
"三、设计主干活动(建议 3 环节)\n"
|
||
"- 导入(3-5 分钟):旧知、生活情境或认知冲突引入。\n"
|
||
"- 新知建构(15-20 分钟):不要直接给结论,设计让学生发现的活动——观察、操作、讨论、归纳。\n"
|
||
"- 巩固迁移(10-15 分钟):模仿→变式→应用,难度阶梯上升。\n\n"
|
||
"四、嵌入评价(防止假装听懂)\n"
|
||
"每个环节末尾设一个检测点:提问、小测、板演、同伴互评,让你随时知道学生学到哪了。\n\n"
|
||
"时间提醒:教师讲授总时长控制在 15 分钟内,把时间还给学生练与思。\n\n"
|
||
"告诉我具体课题和年级,我给完整的环节脚本与活动设计。"
|
||
)
|
||
|
||
def _chat_explain(self, ctx: str) -> str:
|
||
return (
|
||
f"讲清一个难点,核心是搭台阶 + 可视化 + 证伪误区{ctx}:\n\n"
|
||
"一、先找到学生的卡点\n"
|
||
"学生不懂通常卡在某一个具体环节,不是整体不懂。问自己:这个知识依赖哪个旧知?哪个概念是转折点?把卡点定位到最小单元。\n\n"
|
||
"二、搭台阶(最近发展区)\n"
|
||
"把难点拆成 3-4 个递进小问题,每个学生能答上来,最终自己推出结论。例:讲分数除法→先复习分数意义→再问除以一个数=乘它倒数为什么成立→用面积图验证。\n\n"
|
||
"三、可视化(多通道呈现)\n"
|
||
"能画图就画图,能摆实物就摆实物。抽象概念配具象表征(数轴、面积模型、流程图、表格),让学生看见关系。\n\n"
|
||
"四、用错误反证(认知冲突)\n"
|
||
"先抛一个看起来对其实是错的判断,让多数学生踩坑,再一起分析为什么错——比直接讲正确结论印象深 3 倍。\n\n"
|
||
"五、让学生复述/教别人\n"
|
||
"讲完别问懂了吗(学生都会点头),而让 1-2 个学生用自己的话复述,或讲给同桌听。能讲清楚才是真懂。\n\n"
|
||
"把具体知识点告诉我,我帮你拆台阶、设计冲突问题。"
|
||
)
|
||
|
||
def _chat_intro(self, ctx: str) -> str:
|
||
return (
|
||
f"好的导入能在 3-5 分钟抓住学生,常用 5 种方式{ctx}:\n\n"
|
||
"1. 情境导入:用学生熟悉的生活场景切入(买菜算账、校园事件、新闻热点),让知识有用。\n"
|
||
"2. 冲突导入:抛一个反直觉的现象或问题(如:1千克铁和1千克棉花谁重?),引发好奇。\n"
|
||
"3. 悬念导入:讲半截故事/留个谜,答案藏在今天的内容里。\n"
|
||
"4. 旧知导入:从上节课内容自然延伸,温故知新。\n"
|
||
"5. 操作导入:动手做个小实验/小游戏,身体先参与,注意力立刻集中。\n\n"
|
||
"避坑:导入别超过 5 分钟,别为热闹而热闹——导入必须直接服务于本课目标。\n\n"
|
||
"告诉我课题,我帮你设计 1-2 个具体导入脚本。"
|
||
)
|
||
|
||
def _chat_activity(self, ctx: str) -> str:
|
||
return (
|
||
f"设计有效课堂活动,关键是任务清晰 + 人人有事做 + 及时反馈{ctx}:\n\n"
|
||
"常用活动类型\n"
|
||
"- 小组讨论:4 人一组,问题要有争议性/开放性,讨论前给 1 分钟独立思考,避免搭便车。\n"
|
||
"- 角色扮演:语文/英语/历史特别适合,提前给角色卡和台词框架。\n"
|
||
"- 操作探究:数学/科学课用学具、实验,让学生做出结论。\n"
|
||
"- 游戏竞赛:抢答、卡片配对、知识闯关,适合巩固环节,注意控制节奏。\n"
|
||
"- 同伴互教:会的学生教不会的,教的人反而学得更深。\n\n"
|
||
"让活动不失控的 3 个原则\n"
|
||
"1. 活动前讲清规则和产出(讨论结束每组派代表说 1 条),黑板上挂任务。\n"
|
||
"2. 活动中教师巡视、记录、点拨,不站在讲台。\n"
|
||
"3. 活动后必须汇报/展示/点评,否则学生会觉得讨论了没用。\n\n"
|
||
"告诉我学科和课型,我帮你设计一个具体活动。"
|
||
)
|
||
|
||
def _chat_questioning(self, ctx: str) -> str:
|
||
return (
|
||
f"高质量课堂提问,记住三秒原则 + 追问 + 全员应答{ctx}:\n\n"
|
||
"一、提问前的设计\n"
|
||
"备课时写下 3-5 个核心问题,分三类:\n"
|
||
"- 记忆型(是什么)→ 唤醒旧知\n"
|
||
"- 理解型(为什么/怎么说)→ 促思考\n"
|
||
"- 应用/评价型(如果…会怎样/你同意吗)→ 促迁移\n"
|
||
"一节好课,后两类要占多数。\n\n"
|
||
"二、提问技巧\n"
|
||
"- 先问后叫人:先抛问题,停 3-5 秒(让所有人想),再随机点名。\n"
|
||
"- 追问:学生答完,追问你怎么想到的?还有别的可能吗?把思维引向深处。\n"
|
||
"- 不轻易否定:错误答案也是资源,追问这个想法哪里有问题,让全班分析。\n"
|
||
"- 全员应答:用白板、手势、答题卡、随机抽签,让每个学生被迫回应。\n\n"
|
||
"避坑:别只问对不对/是不是;别一问完立刻自己答。\n\n"
|
||
"把要讲的课题告诉我,我帮你设计一组阶梯式提问。"
|
||
)
|
||
|
||
def _chat_management(self, ctx: str) -> str:
|
||
return (
|
||
f"课堂纪律靠规则前置 + 节奏控制 + 关系先行,而非吼叫{ctx}:\n\n"
|
||
"一、预防优于纠正\n"
|
||
"- 开学第一周把课堂常规立清楚(举手发言、不插嘴、安静信号),坚持执行。\n"
|
||
"- 用固定的安静信号(拍手节奏、举手倒数、手势),比喊安静有效。\n\n"
|
||
"二、走神/讲话:用教学手段拉回,而非批评\n"
|
||
"- 突然停顿 3 秒(全班会安静看你)。\n"
|
||
"- 把走神学生的名字编进例题(假设小明买了 3 个本子…),温和提醒。\n"
|
||
"- 走下讲台站到他旁边继续讲,距离本身就是管理。\n\n"
|
||
"三、调皮/对抗:私下处理,保护自尊\n"
|
||
"- 课堂上不当众发作(易激化),课后单独谈。\n"
|
||
"- 谈话用我观察到…+ 我担心…+ 我希望…句式,而非指责。\n"
|
||
"- 找到行为背后的需求(求关注?听不懂?家庭问题?),对症下药。\n\n"
|
||
"四、根本:让课足够有趣 + 师生关系好\n"
|
||
"纪律问题 80% 来自课堂太无聊或学生不喜欢老师。把课上精彩、多关心学生,纪律自然好转。\n\n"
|
||
"具体场景(哪个年级、什么行为)告诉我,我给针对性策略。"
|
||
)
|
||
|
||
def _chat_differentiation(self, ctx: str) -> str:
|
||
return (
|
||
f"应对两极分化/学困生,核心是分层不减负、抓两头带中间{ctx}:\n\n"
|
||
"一、分层不分班(课内分层)\n"
|
||
"- 目标分层:同一节课,学困生只要达成基础目标,学优生有挑战任务。\n"
|
||
"- 任务分层:布置 A(基础)/B(提高)/C(拓展)三档作业,学生选做或指定。\n"
|
||
"- 提问分层:简单问题给学困生(建立信心),难题给学优生。\n\n"
|
||
"二、补差要抓前置基础\n"
|
||
"学困生卡住往往是旧知识漏洞,不是这节课听不懂。诊断他缺哪块基础,用课前 10 分钟或课后针对性补。\n\n"
|
||
"三、同伴互助(最省力)\n"
|
||
"小老师制:1 个学优生帮 1 个学困生,捆绑考核(两人都进步才表扬)。教的人学得更深,被教的人压力小、敢问。\n\n"
|
||
"四、保护学困生自尊\n"
|
||
"- 永远不当众羞辱(这么简单都不会是毒药)。\n"
|
||
"- 抓住任何小进步公开表扬,建立我能学的信念。\n"
|
||
"- 错题私下反馈,不公布排名。\n\n"
|
||
"五、培优要给空间\n"
|
||
"学优生最怕吃不饱被打发。给自主探究任务、当小老师、参加竞赛,让能量有出口。\n\n"
|
||
"具体学科和年级告诉我,我帮你设计分层任务清单。"
|
||
)
|
||
|
||
def _chat_parents(self, ctx: str) -> str:
|
||
return (
|
||
f"家校沟通关键是先共情 + 讲事实 + 给方案,而非告状{ctx}:\n\n"
|
||
"一、沟通前心态\n"
|
||
"家长不是对手,是同盟。即使投诉的家长,本质也是为孩子好。先听他说完,别急着辩解。\n\n"
|
||
"二、报问题的标准话术(三明治法)\n"
|
||
"1. 先说一个孩子的优点/进步(让家长放下防备)。\n"
|
||
"2. 客观陈述问题(只讲事实,不带评价:最近三次作业有两次没交,而非这孩子太懒)。\n"
|
||
"3. 表达关心 + 给具体方案(我担心影响他后续学习,咱们一起想想办法,我建议…您看行吗?)。\n\n"
|
||
"三、家长群沟通禁忌\n"
|
||
"- 不在群里点名批评个别学生(保护隐私)。\n"
|
||
"- 不在群里跟家长争论(私聊处理)。\n"
|
||
"- 通知类信息简洁清晰,避免长语音。\n"
|
||
"- 投诉先回应已收到,X 点前给您答复,别晾着。\n\n"
|
||
"四、家访/约谈\n"
|
||
"- 提前预约,不突袭。\n"
|
||
"- 先表扬再谈问题,结束时达成 1-2 个可执行小目标。\n"
|
||
"- 记录谈话要点,下次跟进。\n\n"
|
||
"具体场景(投诉什么/孩子什么问题)告诉我,我帮你写一段沟通话术。"
|
||
)
|
||
|
||
def _chat_assessment(self, ctx: str) -> str:
|
||
return (
|
||
f"作业与评价的核心是少而精 + 即时反馈 + 促学而非排名{ctx}:\n\n"
|
||
"一、作业设计原则\n"
|
||
"- 少而精:3 道有思维含量的题 > 30 道机械重复。\n"
|
||
"- 分层:基础题全员必做,挑战题选做,避免学困生抄、学优生闲。\n"
|
||
"- 形式多样:除书面外,加口头、实践、项目作业(如用本周学的统计调查家里一周用电量)。\n"
|
||
"- 必批必改必反馈:不批的作业等于没布置;批改后要讲评典型错题。\n\n"
|
||
"二、形成性评价(重过程)\n"
|
||
"不只看期末考试。日常用:课堂提问记录、小测、作品集、同伴互评,让评价贯穿学习全过程。\n\n"
|
||
"三、反馈的艺术\n"
|
||
"- 具体:第二步符号写反了 > 粗心。\n"
|
||
"- 可改进:指出下一步怎么做,而非只判对错。\n"
|
||
"- 及时:当天/次日反馈,隔一周就失效。\n"
|
||
"- 成长导向:比上次进步了比 85 分更有激励作用。\n\n"
|
||
"具体学科和题型告诉我,我帮你设计一份分层作业。"
|
||
)
|
||
|
||
def _chat_motivation(self, ctx: str) -> str:
|
||
return (
|
||
f"激发学习动机,从胜任感 + 自主感 + 归属感入手{ctx}:\n\n"
|
||
"一、胜任感:让他尝到成功的甜头\n"
|
||
"没兴趣的学生,往往因为长期学不会而放弃。给他够得着的小目标,让他体验我会了——一次小测进步、一道题做对,立刻肯定。信心是兴趣的前提。\n\n"
|
||
"二、自主感:给他选择权\n"
|
||
"人对被命令的事天然抗拒。给有限选择:今天作业 A 还是 B?小组讨论你想当记录员还是汇报员?参与感提升投入度。\n\n"
|
||
"三、归属感:让他感到被看见\n"
|
||
"记住每个学生的名字和一件小事,课堂上让每个人都有发言机会。学生因为喜欢这个老师而喜欢这门课是真实存在的。\n\n"
|
||
"四、让知识有用 + 有趣\n"
|
||
"- 联系生活/热点/学生关心的事,让知识活起来。\n"
|
||
"- 用游戏、竞赛、悬念,让过程有乐趣。\n"
|
||
"- 慎用外在奖励:物质奖励/加分短期有效,长期会削弱内驱力。多用成长反馈、公开认可。\n\n"
|
||
"五、排查外部原因\n"
|
||
"突然厌学,先了解:家庭变故?同伴关系?沉迷手机?身体/心理问题?对症才能解。\n\n"
|
||
"具体是哪个学生/什么表现告诉我,我给针对性方案。"
|
||
)
|
||
|
||
def _chat_review(self, ctx: str) -> str:
|
||
return (
|
||
f"复习备考要结构化 + 错题驱动 + 模拟实战,而非题海{ctx}:\n\n"
|
||
"一、先建知识结构(脑图)\n"
|
||
"复习不是把新课重讲一遍。先带学生画整章/整册知识脑图,理清脉络,让零散知识串成网。结构清晰,提取才快。\n\n"
|
||
"二、错题驱动(最高效)\n"
|
||
"把全学期高频错题/易错点整理成专题,针对性突破。会的题不重复刷,时间留给真问题。建议每个学生建错题本,定期重做。\n\n"
|
||
"三、三轮复习节奏\n"
|
||
"- 第一轮:单元过关,扫清知识盲点(重基础)。\n"
|
||
"- 第二轮:专题整合,跨章节串联(重综合)。\n"
|
||
"- 第三轮:模拟实战,限时训练(重应试与心态)。\n\n"
|
||
"四、模拟考试要仿真\n"
|
||
"限时、独立、评分标准对齐正式考试。考后不只对答案,要分析:哪类题失分?知识漏洞还是应试失误?制定针对性补救。\n\n"
|
||
"五、别忘了心态\n"
|
||
"考前焦虑的学生很多。适当减压,强调尽力就好,别把分数和人格绑定。\n\n"
|
||
"具体学科和学段告诉我,我帮你列复习专题清单。"
|
||
)
|
||
|
||
def _chat_newteacher(self, ctx: str) -> str:
|
||
return (
|
||
f"新手教师上路,先稳住课堂 + 关系 + 心态三件事{ctx}:\n\n"
|
||
"一、第一年别追求花哨,先求稳\n"
|
||
"- 把常规立住(纪律、作业、发言规则),课堂乱什么都做不好。\n"
|
||
"- 备课写详案(每环节说什么、问什么、学生可能怎么答),别只写框架。\n"
|
||
"- 多听老教师的课,模仿是最好的学习。\n\n"
|
||
"二、师生关系:严慈相济\n"
|
||
"- 开学先立威(规则严、说到做到),再慢慢施恩(关心、幽默、认可)。顺序反了很难管。\n"
|
||
"- 记住每个学生名字,课后多聊天。被看见的学生才会配合你。\n"
|
||
"- 永远不当众发火/羞辱学生,一次就可能毁掉整学期关系。\n\n"
|
||
"三、教学:少讲多练,别贪多\n"
|
||
"- 一节课 2-3 个核心点足够,讲透练透比走马观花强。\n"
|
||
"- 教师讲授控制在 15 分钟内,剩下时间给学生。\n"
|
||
"- 每节课留 5 分钟检测,知道自己教得怎么样。\n\n"
|
||
"四、心态:别和名师比,和自己比\n"
|
||
"- 第一年上不好很正常,三年才成型,五年才出风格。\n"
|
||
"- 别把所有问题归咎于自己(学生基础、家庭都是变量)。\n"
|
||
"- 找一个愿意带你/听你课的师傅,成长快 3 倍。\n\n"
|
||
"具体困惑(管不住班?备课没底?家长沟通?)告诉我,我针对性支招。"
|
||
)
|
||
|
||
def _chat_general(self, msg: str, ctx: str) -> str:
|
||
return (
|
||
f"我收到了你的问题。为了让建议更精准,先按问题—对象—目标帮你理一理{ctx}:\n\n"
|
||
"一、你想解决的核心问题是什么?\n"
|
||
"比如:备课没思路?某个知识点学生听不懂?课堂纪律乱?学生没兴趣?家长难沟通?复习效率低?\n\n"
|
||
"二、针对的对象和情境?\n"
|
||
"学科、年级、班级特点(人数、两极分化程度)、这节课/这个学生的具体情况。\n\n"
|
||
"三、你期望达成的目标?\n"
|
||
"想要一份完整教案?一个活动设计?一段沟通话术?一个分层作业?还是某个具体问题的解决思路?\n\n"
|
||
"把这些信息补充给我,我能给出可直接用的方案。你也可以直接试试这些常见问题:\n"
|
||
"- 如何讲清[某知识点]?\n"
|
||
"- 设计一节[某课题]的导入/活动\n"
|
||
"- 班上两极分化怎么办?\n"
|
||
"- 学生上课走神怎么管理?\n"
|
||
"- 如何回复家长的投诉?"
|
||
)
|
||
|
||
def _fallback_mindmap(self, topic: str, subject: str) -> dict:
|
||
topic = self._repair_text(topic)
|
||
title = topic[:24] or "思维导图"
|
||
for keys, nodes in self._mindmap_kb():
|
||
if any(k in topic for k in keys):
|
||
return {"title": title, "nodes": nodes}
|
||
return {"title": title, "nodes": self._mindmap_subject_template(subject)}
|
||
|
||
# ---- 主题感知思维导图知识库 ----
|
||
_MM_COLORS = ["#00a870", "#2563eb", "#d97706", "#db2777", "#7c3aed", "#0891b2"]
|
||
|
||
def _mm_nodes(self, *branches):
|
||
out = []
|
||
for i, (title, kids) in enumerate(branches):
|
||
out.append({"title": title, "color": self._MM_COLORS[i % len(self._MM_COLORS)],
|
||
"children": [{"title": k} for k in kids]})
|
||
return out
|
||
|
||
def _mindmap_kb(self):
|
||
mm = self._mm_nodes
|
||
# 历史
|
||
if False:
|
||
pass
|
||
kb = [
|
||
(["朝代", "历代", "王朝", "中国古代史"], mm(
|
||
("先秦文明", ["夏商周更替", "春秋五霸", "战国七雄"]),
|
||
("秦汉大一统", ["秦始皇统一", "汉武帝强盛", "丝绸之路"]),
|
||
("隋唐盛世", ["隋朝开科", "贞观之治", "开元盛世"]),
|
||
("宋元变革", ["宋代理学", "元朝行省制", "四大发明"]),
|
||
)),
|
||
(["丝绸之路"], mm(
|
||
("陆上丝路", ["张骞出使西域", "长安出发", "途经河西走廊"]),
|
||
("海上丝路", ["泉州广州港口", "瓷器丝绸外销", "郑和下西洋"]),
|
||
("贸易商品", ["丝绸", "瓷器", "茶叶", "香料"]),
|
||
("文化交流", ["佛教东传", "四大发明西传", "中外互通"]),
|
||
)),
|
||
(["工业革命"], mm(
|
||
("第一次工业革命", ["蒸汽机", "纺织业机械化", "铁路交通"]),
|
||
("第二次工业革命", ["电力", "内燃机", "化学工业"]),
|
||
("社会影响", ["城市化", "阶级变化", "殖民扩张"]),
|
||
("科技进步", ["生产力飞跃", "世界市场形成", "环境污染"]),
|
||
)),
|
||
(["抗日战争", "抗战"], mm(
|
||
("局部抗战", ["九一八事变", "东北沦陷", "义勇军抗争"]),
|
||
("全面抗战", ["七七事变", "正面战场", "敌后战场"]),
|
||
("重大战役", ["台儿庄", "百团大战", "平型关"]),
|
||
("伟大胜利", ["统一战线", "世界反法西斯", "日本投降"]),
|
||
)),
|
||
(["文艺复兴"], mm(
|
||
("兴起背景", ["意大利城邦", "资本主义萌芽", "人文主义"]),
|
||
("代表人物", ["但丁", "达芬奇", "莎士比亚"]),
|
||
("核心思想", ["以人为本", "个性解放", "反对神权"]),
|
||
("深远影响", ["思想解放", "科学革命", "宗教改革"]),
|
||
)),
|
||
(["辛亥革命"], mm(
|
||
("革命背景", ["清末危机", "民族资本主义", "革命思想传播"]),
|
||
("革命过程", ["武昌起义", "各省响应", "建立民国"]),
|
||
("历史意义", ["推翻帝制", "民主共和", "思想解放"]),
|
||
("局限性", ["革命不彻底", "军阀割据", "任务未完成"]),
|
||
)),
|
||
# 地理
|
||
(["气候", "气候类型", "气温降水"], mm(
|
||
("影响因子", ["纬度", "海陆", "地形", "洋流"]),
|
||
("主要类型", ["热带雨林", "季风气候", "温带大陆性", "地中海气候"]),
|
||
("降水规律", ["赤道多雨", "副热带少雨", "温带增多"]),
|
||
("中国气候", ["季风显著", "雨热同期", "气候复杂多样"]),
|
||
)),
|
||
(["地形", "地貌", "地势"], mm(
|
||
("基本地形", ["山地", "平原", "高原", "盆地", "丘陵"]),
|
||
("中国地势", ["西高东低", "阶梯分布", "大河东流"]),
|
||
("外力作用", ["流水侵蚀", "风力沉积", "冰川作用"]),
|
||
("典型地貌", ["喀斯特", "丹霞", "黄土高原", "冲积平原"]),
|
||
)),
|
||
(["河流", "水系", "长江", "黄河"], mm(
|
||
("长江", ["发源青藏", "流经十一省", "入东海", "黄金水道"]),
|
||
("黄河", ["发源巴颜喀拉", "泥沙含量大", "地上河", "入渤海"]),
|
||
("河流作用", ["供水灌溉", "航运发电", "塑造平原"]),
|
||
("治理保护", ["防洪堤坝", "水土保持", "生态修复"]),
|
||
)),
|
||
(["地球", "地球运动", "自转公转"], mm(
|
||
("自转", ["绕轴旋转", "昼夜交替", "时差"]),
|
||
("公转", ["绕日运行", "四季更替", "五带划分"]),
|
||
("黄赤交角", ["23.5度", "太阳直射点移动", "昼夜长短变化"]),
|
||
("地理意义", ["正午太阳高度", "节气", "气候形成"]),
|
||
)),
|
||
# 生物
|
||
(["细胞"], mm(
|
||
("细胞结构", ["细胞膜", "细胞质", "细胞核", "细胞壁(植物)"]),
|
||
("细胞器", ["线粒体", "叶绿体", "核糖体", "液泡"]),
|
||
("细胞分裂", ["有丝分裂", "减数分裂", "无丝分裂"]),
|
||
("细胞分化", ["形态变化", "功能特化", "形成组织"]),
|
||
)),
|
||
(["光合作用"], mm(
|
||
("反应条件", ["光照", "叶绿体", "适宜温度"]),
|
||
("原料产物", ["二氧化碳", "水", "氧气", "有机物"]),
|
||
("两个阶段", ["光反应", "暗反应", "能量转化"]),
|
||
("重要意义", ["制造有机物", "释放氧气", "碳氧平衡"]),
|
||
)),
|
||
(["生态系统"], mm(
|
||
("组成成分", ["非生物物质", "生产者", "消费者", "分解者"]),
|
||
("食物链网", ["捕食关系", "能量单向", "物质循环"]),
|
||
("能量流动", ["太阳能输入", "逐级递减", "10%-20%传递"]),
|
||
("生态平衡", ["自我调节", "稳定性", "人类影响"]),
|
||
)),
|
||
(["遗传", "基因", "DNA"], mm(
|
||
("遗传物质", ["DNA", "基因", "染色体"]),
|
||
("基本规律", ["分离定律", "自由组合", "显隐性"]),
|
||
("遗传变异", ["基因突变", "基因重组", "染色体变异"]),
|
||
("应用拓展", ["育种", "遗传咨询", "基因工程"]),
|
||
)),
|
||
(["人体", "消化", "呼吸", "循环"], mm(
|
||
("消化系统", ["口腔胃小肠", "消化酶", "吸收营养"]),
|
||
("呼吸系统", ["呼吸道", "肺泡", "气体交换"]),
|
||
("循环系统", ["心脏", "血管", "血液循环"]),
|
||
("神经系统", ["大脑", "神经传导", "反射调节"]),
|
||
)),
|
||
# 化学
|
||
(["元素周期表", "元素"], mm(
|
||
("结构规律", ["周期", "族", "原子序数", "递变规律"]),
|
||
("主族元素", ["碱金属", "卤素", "氧族", "氮族"]),
|
||
("周期变化", ["原子半径递减", "金属性减弱", "非金属性增强"]),
|
||
("应用价值", ["预测性质", "寻找新材料", "指导分类"]),
|
||
)),
|
||
(["化学反应"], mm(
|
||
("反应类型", ["化合", "分解", "置换", "复分解"]),
|
||
("反应条件", ["加热", "点燃", "催化", "通电"]),
|
||
("能量变化", ["放热反应", "吸热反应", "化学能转化"]),
|
||
("方程式", ["质量守恒", "配平", "符号规范"]),
|
||
)),
|
||
(["酸碱盐", "酸碱"], mm(
|
||
("常见酸", ["盐酸", "硫酸", "硝酸", "醋酸"]),
|
||
("常见碱", ["氢氧化钠", "氢氧化钙", "氨水"]),
|
||
("酸碱反应", ["中和反应", "生成盐和水", "pH变化"]),
|
||
("盐的性质", ["溶解性", "复分解", "焰色反应"]),
|
||
)),
|
||
(["原子", "分子", "物质构成"], mm(
|
||
("原子结构", ["原子核", "质子", "中子", "核外电子"]),
|
||
("分子构成", ["共价键", "分子间作用力", "分子特性"]),
|
||
("离子化合物", ["得失电子", "离子键", "晶体结构"]),
|
||
("物质分类", ["单质", "化合物", "混合物"]),
|
||
)),
|
||
# 物理
|
||
(["力学", "力", "牛顿"], mm(
|
||
("常见的力", ["重力", "弹力", "摩擦力", "浮力"]),
|
||
("牛顿三定律", ["惯性定律", "加速度定律", "作用反作用"]),
|
||
("压强", ["固体压强", "液体压强", "大气压强"]),
|
||
("简单机械", ["杠杆", "滑轮", "斜面", "功的原理"]),
|
||
)),
|
||
(["电学", "电流", "电路", "欧姆"], mm(
|
||
("基本物理量", ["电流", "电压", "电阻", "电功率"]),
|
||
("欧姆定律", ["I=U/R", "串并联电阻", "电压电流关系"]),
|
||
("电路连接", ["串联", "并联", "混联", "短路断路"]),
|
||
("电与磁", ["电流磁效应", "电磁感应", "电动机发电机"]),
|
||
)),
|
||
(["光学", "光", "反射折射"], mm(
|
||
("光的传播", ["直线传播", "光速", "影子小孔成像"]),
|
||
("光的反射", ["反射定律", "镜面反射", "漫反射"]),
|
||
("光的折射", ["折射定律", "全反射", "色散"]),
|
||
("透镜成像", ["凸透镜", "凹透镜", "成像规律", "应用"]),
|
||
)),
|
||
(["运动", "速度", "加速度"], mm(
|
||
("运动描述", ["参照物", "路程位移", "速度"]),
|
||
("匀速运动", ["速度恒定", "s=vt", "图像分析"]),
|
||
("变速运动", ["加速度", "v=v0+at", "匀变速直线"]),
|
||
("相对运动", ["相对速度", "相遇追及", "实际应用"]),
|
||
)),
|
||
# 数学
|
||
(["函数"], mm(
|
||
("函数概念", ["定义域", "值域", "对应关系"]),
|
||
("基本初等函数", ["一次函数", "二次函数", "反比例函数"]),
|
||
("指数对数", ["指数函数", "对数函数", "运算法则"]),
|
||
("函数性质", ["单调性", "奇偶性", "周期性"]),
|
||
)),
|
||
(["几何", "三角形", "圆"], mm(
|
||
("线与角", ["点线面", "相交平行", "角的关系"]),
|
||
("三角形", ["内角和", "全等相似", "勾股定理"]),
|
||
("四边形", ["平行四边形", "矩形菱形", "梯形"]),
|
||
("圆", ["圆的性质", "切线", "弧长扇形"]),
|
||
)),
|
||
(["方程", "一元", "二元", "不等式"], mm(
|
||
("一元一次", ["等式性质", "求解步骤", "应用题"]),
|
||
("方程组", ["二元一次", "代入消元", "加减消元"]),
|
||
("一元二次", ["因式分解", "公式法", "韦达定理"]),
|
||
("不等式", ["性质", "一元一次不等式", "不等式组"]),
|
||
)),
|
||
(["概率", "统计"], mm(
|
||
("数据收集", ["普查抽样", "频数频率", "统计图表"]),
|
||
("数据特征", ["平均数", "中位数众数", "方差极差"]),
|
||
("概率初步", ["事件分类", "古典概型", "频率估计", "树状图列举"]),
|
||
("应用", ["决策分析", "风险评估", "生活实例"]),
|
||
)),
|
||
# 语文
|
||
(["古诗", "古诗词", "唐诗宋词"], mm(
|
||
("发展脉络", ["诗经楚辞", "唐诗", "宋词", "元曲"]),
|
||
("代表诗人", ["李白杜甫", "白居易", "苏轼李清照"]),
|
||
("常见题材", ["山水田园", "边塞", "咏物言志", "送别"]),
|
||
("鉴赏方法", ["意象意境", "表现手法", "炼字", "情感主旨"]),
|
||
)),
|
||
(["记叙文"], mm(
|
||
("六要素", ["时间地点", "人物", "起因经过结果"]),
|
||
("结构层次", ["开头引入", "主体展开", "结尾点题"]),
|
||
("表达方式", ["叙述", "描写", "议论抒情"]),
|
||
("写作技巧", ["详略得当", "线索贯穿", "细节描写"]),
|
||
)),
|
||
(["议论文"], mm(
|
||
("三要素", ["论点", "论据", "论证"]),
|
||
("论点确立", ["中心论点", "分论点", "鲜明准确"]),
|
||
("论证方法", ["举例论证", "道理论证", "对比比喻"]),
|
||
("结构思路", ["提出问题", "分析问题", "解决问题"]),
|
||
)),
|
||
# 英语
|
||
(["时态"], mm(
|
||
("一般时态", ["一般现在", "一般过去", "一般将来"]),
|
||
("进行时", ["现在进行", "过去进行", "be+doing"]),
|
||
("完成时", ["现在完成", "过去完成", "have+done"]),
|
||
("时间状语", ["always/often", "yesterday", "already/yet"]),
|
||
)),
|
||
(["从句"], mm(
|
||
("名词性从句", ["主语从句", "宾语从句", "表语从句"]),
|
||
("定语从句", ["关系代词", "关系副词", "限制非限制"]),
|
||
("状语从句", ["时间原因", "条件让步", "目的结果"]),
|
||
("用法要点", ["连接词", "语序", "时态呼应"]),
|
||
)),
|
||
]
|
||
return kb
|
||
|
||
def _mindmap_subject_template(self, subject: str) -> list:
|
||
subject = (subject or "").strip()
|
||
mm = self._mm_nodes
|
||
templates = {
|
||
"数学": mm(
|
||
("概念理解", ["定义", "性质", "判定条件"]),
|
||
("公式法则", ["基本公式", "运算法则", "推导过程"]),
|
||
("典型例题", ["基础题", "应用题", "易错题"]),
|
||
("知识应用", ["实际问题", "跨学科", "拓展提升"]),
|
||
),
|
||
"物理": mm(
|
||
("物理概念", ["定义", "物理意义", "单位"]),
|
||
("规律定律", ["适用条件", "公式表达", "实验基础"]),
|
||
("实验探究", ["实验器材", "操作步骤", "数据处理"]),
|
||
("生活应用", ["现象解释", "技术应用", "综合计算"]),
|
||
),
|
||
"化学": mm(
|
||
("物质组成", ["元素", "结构", "性质"]),
|
||
("变化规律", ["反应类型", "反应条件", "实验现象"]),
|
||
("化学计算", ["化学式", "方程式", "质量计算"]),
|
||
("实际应用", ["材料能源", "环境保护", "生命健康"]),
|
||
),
|
||
"生物": mm(
|
||
("生命现象", ["结构基础", "生理功能", "生命活动"]),
|
||
("核心概念", ["定义", "原理", "机制"]),
|
||
("实验观察", ["方法", "现象", "结论"]),
|
||
("联系应用", ["生产实践", "健康生活", "生态保护"]),
|
||
),
|
||
"历史": mm(
|
||
("时代背景", ["政治", "经济", "文化"]),
|
||
("重要事件", ["时间", "人物", "过程"]),
|
||
("深远影响", ["制度变革", "思想文化", "社会发展"]),
|
||
("历史启示", ["经验教训", "规律认识", "现实意义"]),
|
||
),
|
||
"地理": mm(
|
||
("空间分布", ["位置", "范围", "界线"]),
|
||
("自然要素", ["地形气候", "水文土壤", "植被"]),
|
||
("人文要素", ["人口城市", "农业工业", "交通"]),
|
||
("人地关系", ["资源利用", "环境问题", "可持续发展"]),
|
||
),
|
||
"语文": mm(
|
||
("基础知识", ["字词", "语句", "修辞"]),
|
||
("文本理解", ["内容", "结构", "主旨"]),
|
||
("表达技巧", ["描写方法", "表现手法", "语言特色"]),
|
||
("拓展运用", ["写作借鉴", "迁移阅读", "文化传承"]),
|
||
),
|
||
"英语": mm(
|
||
("词汇积累", ["核心单词", "短语搭配", "词形变化"]),
|
||
("语法要点", ["句型结构", "时态语态", "从句"]),
|
||
("功能话题", ["日常交际", "话题表达", "文化背景"]),
|
||
("技能训练", ["听说", "读写", "综合运用"]),
|
||
),
|
||
}
|
||
if subject in templates:
|
||
return templates[subject]
|
||
return mm(
|
||
("核心概念", ["定义", "特征", "分类"]),
|
||
("知识结构", ["基本原理", "重要规律", "内在联系"]),
|
||
("重点方法", ["解题思路", "分析方法", "易错提示"]),
|
||
("应用拓展", ["生活实例", "学科关联", "拓展提升"]),
|
||
)
|
||
|
||
|
||
async def generate_animation(self, prompt: str, anim_type: str = "general") -> dict:
|
||
prompt = self._repair_text(prompt)
|
||
system_prompt = f"""你是一位教学动画设计专家。根据教师描述,生成教学动画的HTML页面。
|
||
|
||
重要规则:
|
||
- 所有HTML属性必须用单引号,如 style='color:red;' onclick='fn()'
|
||
- 不要使用<script>、Canvas或外部资源,优先用纯CSS动画、SVG和HTML元素实现
|
||
- 不要使用vh/vw单位,用px或%
|
||
- HTML从<div>开始,不含<html><head><body>
|
||
- 容器设置 style='width:100%;height:100%;position:relative;overflow:hidden;'
|
||
- 代码尽量精简,避免过长的内联样式
|
||
|
||
设计要求:
|
||
- 使用CSS keyframes或SVG animate实现教学动画效果
|
||
- 视觉风格要像教师备课平台的课堂白板:浅色背景、绿色主色(#00a870 / #0f766e)、白色内容舞台、细边框、柔和阴影
|
||
- 不要使用深色科幻背景、霓虹光效、大面积高饱和渐变;动画应清爽、可信、适合课堂投屏
|
||
- 配色克制,动画流畅,重点元素可以用少量橙色/蓝色强调
|
||
- 必须有成熟产品质感:顶部信息栏、白色主舞台、圆角、细边框、柔和投影、右侧知识卡或底部步骤进度
|
||
- 视觉不能像临时demo,不能只有几个漂浮圆点;图形必须承载知识含义
|
||
- 包含和教师输入主题直接相关的图形、标注、步骤和说明
|
||
- 可以使用伪播放控件样式,但不要依赖JavaScript控制逻辑
|
||
- 包含文字标注和说明
|
||
- 动画类型: {anim_type}
|
||
|
||
直接输出JSON(不要markdown代码块,不要其他文字):
|
||
{{"title":"动画标题","description":"动画说明","html":"HTML代码字符串","scenes":[{{"id":1,"name":"场景名","duration":3,"narration":"旁白"}}],"totalDuration":15,"interactive":true}}"""
|
||
try:
|
||
result = await self._chat_json([
|
||
{"role": "system", "content": system_prompt},
|
||
{"role": "user", "content": prompt},
|
||
], temperature=0.5, max_tokens=16384)
|
||
return self._normalize_animation_result(result, prompt, anim_type)
|
||
except RuntimeError:
|
||
return self._fallback_animation(prompt, anim_type)
|
||
|
||
def _normalize_animation_result(self, result: dict, prompt: str, anim_type: str) -> dict:
|
||
prompt = self._repair_text(prompt)
|
||
html = result.get("html")
|
||
if not isinstance(html, str):
|
||
return self._fallback_animation(prompt, anim_type)
|
||
|
||
cleaned = html.strip()
|
||
# The preview iframe must never show a "successful" blank result. Canvas/script
|
||
# animations from the model are often syntactically invalid or depend on brittle
|
||
# globals, so prefer the stable CSS animation fallback in that case.
|
||
if (
|
||
len(cleaned) < 160
|
||
or "<div" not in cleaned.lower()
|
||
or re.search(r"<script\b|<canvas\b", cleaned, re.IGNORECASE)
|
||
or re.search(r"display\s*:\s*none|opacity\s*:\s*0\b|height\s*:\s*0\b", cleaned, re.IGNORECASE)
|
||
):
|
||
return self._fallback_animation(prompt, anim_type)
|
||
|
||
if not re.search(r"animation|@keyframes|svg|style", cleaned, re.IGNORECASE):
|
||
return self._fallback_animation(prompt, anim_type)
|
||
|
||
if not self._animation_matches_prompt(prompt, cleaned, result):
|
||
return self._fallback_animation(prompt, anim_type)
|
||
|
||
if not self._animation_has_quality(cleaned):
|
||
return self._fallback_animation(prompt, anim_type)
|
||
|
||
result["html"] = cleaned.replace("100vh", "100%").replace("100vw", "100%")
|
||
result.setdefault("title", prompt[:24] or "教学动画")
|
||
result.setdefault("description", f"{anim_type} 教学动画")
|
||
result.setdefault("scenes", [{"id": 1, "name": "演示", "duration": 8, "narration": "观察动画变化"}])
|
||
result.setdefault("totalDuration", 8)
|
||
result.setdefault("interactive", True)
|
||
return result
|
||
|
||
def _animation_category(self, prompt: str) -> str:
|
||
prompt = self._repair_text(prompt)
|
||
prompt_lower = prompt.lower()
|
||
if any(k in prompt for k in ["圆柱", "体积", "π", "半径", "底面积"]):
|
||
return "cylinder"
|
||
if any(k in prompt for k in ["函数", "抛物线", "二次", "坐标", "图像", "y="]) or any(k in prompt_lower for k in ["function", "parabola", "quadratic", "axis"]):
|
||
return "function"
|
||
if any(k in prompt for k in ["光合", "叶绿体", "植物", "二氧化碳", "氧气"]):
|
||
return "photosynthesis"
|
||
if any(k in prompt for k in ["化学", "反应", "分子", "原子", "方程式", "燃烧"]):
|
||
return "chemistry"
|
||
if any(k in prompt for k in ["地球", "公转", "自转", "四季", "太阳"]):
|
||
return "orbit"
|
||
if any(k in prompt for k in ["力", "速度", "运动", "加速度", "物理", "斜面"]):
|
||
return "motion"
|
||
if any(k in prompt for k in ["三角形", "内角和", "内角", "勾股", "直角", "锐角", "钝角", "几何", "角度", "全等", "相似", "多边形", "梯形", "平行四边", "矩形", "正方形", "长方形", "圆面积", "圆的面积", "圆周长", "圆周率", "顶角", "底角", "平角", "周角"]):
|
||
return "geometry"
|
||
return "general"
|
||
|
||
def _animation_matches_prompt(self, prompt: str, html: str, result: dict) -> bool:
|
||
prompt = self._repair_text(prompt)
|
||
category = self._animation_category(prompt)
|
||
if category == "general":
|
||
return True
|
||
|
||
text = html.lower().replace(prompt.lower(), "")
|
||
markers = {
|
||
"cylinder": ["圆柱", "体积", "π", "半径", "底面积", "v ="],
|
||
"function": ["函数", "抛物线", "坐标", "y =", "ax", "curve"],
|
||
"photosynthesis": ["光合", "叶绿体", "植物", "co₂", "co2", "氧气", "o₂", "o2"],
|
||
"chemistry": ["化学", "反应", "分子", "原子", "方程式", "生成物"],
|
||
"orbit": ["地球", "公转", "自转", "四季", "太阳", "orbit"],
|
||
"motion": ["速度", "运动", "加速度", "物理", "力", "vector"],
|
||
"geometry": ["三角形", "内角", "勾股", "几何", "角度", "全等", "相似", "多边形", "180", "圆面积", "圆周长", "∠"],
|
||
}
|
||
return any(marker.lower() in text for marker in markers.get(category, []))
|
||
|
||
def _animation_has_quality(self, html: str) -> bool:
|
||
text = html.lower()
|
||
if any(bad in text for bad in ["#020617", "#111827", "#0a1628", "neon", "霓虹"]):
|
||
return False
|
||
required = ["border-radius", "box-shadow"]
|
||
if not all(token in text for token in required):
|
||
return False
|
||
has_light_stage = any(token in text for token in ["background:#fff", "background: #fff", "background:white", "background: white"])
|
||
has_platform_green = any(token in text for token in ["#00a870", "#0f766e", "#00543d", "#10b981"])
|
||
return has_light_stage and has_platform_green
|
||
|
||
def _repair_text(self, value: str) -> str:
|
||
if not isinstance(value, str):
|
||
return ""
|
||
# Some Windows clients send UTF-8 text that has been decoded as Latin-1.
|
||
# Repair that common mojibake shape before keyword matching.
|
||
if any(ch in value for ch in ["å", "ç", "æ", "è", "é"]):
|
||
try:
|
||
repaired = value.encode("latin1").decode("utf-8")
|
||
if repaired and repaired != value:
|
||
return repaired
|
||
except UnicodeError:
|
||
pass
|
||
return value
|
||
|
||
async def generate_exercise(self, prompt: str, exercise_type: str = "choice",
|
||
subject: str = "", knowledge_points: list = None,
|
||
count: int = 5) -> dict:
|
||
kp_str = "、".join(knowledge_points) if knowledge_points else "未指定"
|
||
system_prompt = f"""你是一位教学练习设计专家。生成{exercise_type}类型的练习题。
|
||
|
||
知识点:{kp_str}
|
||
学科:{subject or '未指定'}
|
||
题目数量:{count}
|
||
|
||
每道题目需要生成一个可以直接在浏览器渲染的HTML互动组件。
|
||
- 选择题:显示选项按钮,点击后高亮显示对错
|
||
- 填空题:显示输入框和提交按钮
|
||
- 判断题:显示对/错按钮
|
||
- HTML片段不要包含<html><head><body>标签,直接从<div>开始
|
||
- 不要使用vh/vw单位,使用px或%代替
|
||
|
||
输出JSON:
|
||
{{
|
||
"title": "练习标题",
|
||
"questions": [
|
||
{{
|
||
"id": 1,
|
||
"type": "choice|fill_blank|true_false|matching",
|
||
"question": "题干文本",
|
||
"options": ["A选项", "B选项", "C选项", "D选项"],
|
||
"answer": "正确答案",
|
||
"explanation": "详细解析",
|
||
"difficulty": "easy|medium|hard",
|
||
"html": "该题目的完整HTML互动代码"
|
||
}}
|
||
]
|
||
}}"""
|
||
try:
|
||
return await self._chat_json([
|
||
{"role": "system", "content": system_prompt},
|
||
{"role": "user", "content": prompt},
|
||
], temperature=0.5, max_tokens=8192)
|
||
except RuntimeError:
|
||
return self._fallback_exercise(prompt, exercise_type, subject, knowledge_points, count)
|
||
|
||
async def generate_lesson_plan(self, title: str, subject: str = "", grade: str = "",
|
||
objectives: list = None, duration: int = 45,
|
||
extra_requirements: str = "") -> dict:
|
||
obj_str = "\n".join(f"- {o}" for o in (objectives or []))
|
||
system_prompt = """你是一位资深教学设计专家。根据教师需求生成完整的教案。
|
||
|
||
教案内容要详实、可操作,每个环节的具体活动描述不少于50字。
|
||
|
||
输出JSON格式:
|
||
{
|
||
"title": "教案标题",
|
||
"objectives": ["教学目标1(具体可衡量的描述)", "教学目标2"],
|
||
"key_points": ["教学重点1(说明为什么是重点)"],
|
||
"difficulties": ["教学难点1(说明突破策略)"],
|
||
"phases": [
|
||
{
|
||
"name": "导入/新授/练习/巩固/总结",
|
||
"duration": 5,
|
||
"activities": ["活动1:具体描述活动内容和步骤", "活动2:具体描述"],
|
||
"teacher_actions": ["教师具体行为描述", "教师提问的问题"],
|
||
"student_actions": ["预期学生行为和回答", "学生参与方式"],
|
||
"resources": ["所需教具和材料"]
|
||
}
|
||
],
|
||
"homework": ["作业1:具体题目或任务描述"],
|
||
"reflection": "教学反思提示"
|
||
}"""
|
||
user_msg = f"课题:{title}\n学科:{subject}\n年级:{grade}\n时长:{duration}分钟\n教学目标:\n{obj_str}\n附加要求:{extra_requirements}"
|
||
try:
|
||
return await self._chat_json([
|
||
{"role": "system", "content": system_prompt},
|
||
{"role": "user", "content": user_msg},
|
||
])
|
||
except RuntimeError:
|
||
return self._fallback_lesson_plan(title, subject, grade, objectives, duration)
|
||
|
||
async def grade_essay(self, essay_text: str, grade_level: str = "初中",
|
||
essay_type: str = "记叙文", total_score: int = 50) -> dict:
|
||
system_prompt = f"""你是一位资深的语文作文批改专家。请对以下作文进行全面批改。
|
||
|
||
年级段:{grade_level}
|
||
作文类型:{essay_type}
|
||
满分:{total_score}分
|
||
|
||
评分维度及权重:
|
||
- 内容与立意 (30%)
|
||
- 结构与条理 (25%)
|
||
- 语言表达 (25%)
|
||
- 书写与规范 (20%)
|
||
|
||
批改要求:
|
||
1. 逐段给出具体评语
|
||
2. 指出好词好句并说明为什么好
|
||
3. 指出具体问题并给出修改建议
|
||
4. 总评要结合年级段要求来评价
|
||
|
||
输出JSON:
|
||
{{
|
||
"total_score": 38,
|
||
"scores": {{
|
||
"content": {{"score": 25, "max": 30, "comment": "具体评语"}},
|
||
"structure": {{"score": 20, "max": 25, "comment": "具体评语"}},
|
||
"language": {{"score": 18, "max": 25, "comment": "具体评语"}},
|
||
"writing": {{"score": 15, "max": 20, "comment": "具体评语"}}
|
||
}},
|
||
"overall_comment": "总评(100字以上详细评语)",
|
||
"strengths": ["优点1(引用原文并分析)"],
|
||
"weaknesses": ["不足1(引用原文并给出修改)"],
|
||
"suggestions": ["改进建议1(具体可操作的建议)"],
|
||
"annotations": [
|
||
{{"text": "原文片段", "comment": "批注", "type": "error|good|suggestion"}}
|
||
],
|
||
"level": "优秀|良好|中等|及格|不及格"
|
||
}}"""
|
||
try:
|
||
return await self._chat_json([
|
||
{"role": "system", "content": system_prompt},
|
||
{"role": "user", "content": essay_text},
|
||
])
|
||
except RuntimeError:
|
||
return self._fallback_essay_grade(essay_text, total_score)
|
||
|
||
async def generate_exam(self, subject: str, grade: str = "", knowledge_points: list = None,
|
||
difficulty: str = "medium", question_types: list = None,
|
||
count: int = 10, total_score: int = 100) -> dict:
|
||
kp_str = "、".join(knowledge_points) if knowledge_points else "综合"
|
||
qt_str = "、".join(question_types) if question_types else "选择题"
|
||
system_prompt = f"""你是一位专业的命题专家。请根据要求生成试卷。
|
||
|
||
学科:{subject}
|
||
年级:{grade}
|
||
知识点:{kp_str}
|
||
难度:{difficulty}
|
||
题型:{qt_str}
|
||
题目数量:{count}
|
||
总分:{total_score}
|
||
|
||
题目要求:
|
||
1. 题目内容准确、严谨,符合课标要求
|
||
2. 选项设计合理,干扰项有效
|
||
3. 解析详细,说明解题思路
|
||
4. 难度分布合理(简单30%、中等50%、困难20%)
|
||
|
||
输出JSON:
|
||
{{
|
||
"title": "试卷标题(含学科和知识点)",
|
||
"questions": [
|
||
{{
|
||
"id": 1,
|
||
"type": "choice|fill_blank|true_false|short_answer|calculation",
|
||
"score": 5,
|
||
"difficulty": "easy|medium|hard",
|
||
"content": "题干(含必要的数学公式、图表描述等)",
|
||
"options": ["A选项", "B选项", "C选项", "D选项"],
|
||
"answer": "答案",
|
||
"analysis": "详细解析(含解题步骤和知识点)"
|
||
}}
|
||
]
|
||
}}"""
|
||
try:
|
||
return await self._chat_json([
|
||
{"role": "system", "content": system_prompt},
|
||
{"role": "user", "content": f"请生成{subject}试卷,共{count}题,总分{total_score}分"},
|
||
])
|
||
except RuntimeError:
|
||
return self._fallback_exam(subject, grade, knowledge_points, difficulty, question_types, count, total_score)
|
||
|
||
def _fallback_animation(self, prompt: str, anim_type: str) -> dict:
|
||
category = self._animation_category(prompt)
|
||
if category == "cylinder":
|
||
return self._fallback_cylinder_animation(prompt, anim_type)
|
||
if category == "function":
|
||
return self._fallback_function_animation(prompt, anim_type)
|
||
if category == "photosynthesis":
|
||
return self._fallback_photosynthesis_animation(prompt, anim_type)
|
||
if category == "chemistry":
|
||
return self._fallback_chemistry_animation(prompt, anim_type)
|
||
if category == "orbit":
|
||
return self._fallback_orbit_animation(prompt, anim_type)
|
||
if category == "motion":
|
||
return self._fallback_motion_animation(prompt, anim_type)
|
||
if category == "geometry":
|
||
return self._fallback_geometry_animation(prompt, anim_type)
|
||
return self._fallback_general_animation(prompt, anim_type)
|
||
|
||
def _animation_meta(self, prompt: str, anim_type: str) -> tuple[str, str]:
|
||
title = html_lib.escape(prompt[:24] or "教学动画")
|
||
type_label = {
|
||
"math": "数学",
|
||
"physics": "物理",
|
||
"chemistry": "化学",
|
||
"general": "通用",
|
||
}.get(anim_type, "通用")
|
||
return title, type_label
|
||
|
||
def _animation_result(self, prompt: str, anim_type: str, html: str, description: str, scenes: list[dict] | None = None) -> dict:
|
||
title = html_lib.escape(prompt[:24] or "教学动画")
|
||
return {
|
||
"title": title,
|
||
"description": description,
|
||
"html": html,
|
||
"scenes": scenes or [{"id": 1, "name": "演示", "duration": 8, "narration": "观察动画变化"}],
|
||
"totalDuration": 12,
|
||
"interactive": True,
|
||
}
|
||
|
||
def _fallback_general_animation(self, prompt: str, anim_type: str) -> dict:
|
||
title, type_label = self._animation_meta(prompt, anim_type)
|
||
html = f"""<div style='width:100%;height:100%;position:relative;overflow:hidden;background:#f6fbf7;font-family:Microsoft YaHei,sans-serif;color:#12372d;'>
|
||
<style>
|
||
@keyframes floatBall {{
|
||
0% {{ transform: translateX(0) translateY(0); }}
|
||
25% {{ transform: translateX(160px) translateY(-34px); }}
|
||
50% {{ transform: translateX(320px) translateY(0); }}
|
||
75% {{ transform: translateX(480px) translateY(-24px); }}
|
||
100% {{ transform: translateX(620px) translateY(0); }}
|
||
}}
|
||
@keyframes pulseGlow {{
|
||
0%,100% {{ box-shadow: 0 8px 20px rgba(0,168,112,.16); }}
|
||
50% {{ box-shadow: 0 12px 32px rgba(0,168,112,.28); }}
|
||
}}
|
||
@keyframes drift {{
|
||
0%,100% {{ transform: translateY(0); opacity: .28; }}
|
||
50% {{ transform: translateY(-14px); opacity: .44; }}
|
||
}}
|
||
@keyframes scanLine {{
|
||
0% {{ transform: translateX(-20%); opacity: .2; }}
|
||
50% {{ opacity: .8; }}
|
||
100% {{ transform: translateX(115%); opacity: .2; }}
|
||
}}
|
||
@keyframes stepPulse {{
|
||
0%,100% {{ transform: scale(1); border-color:#d9e7dc; }}
|
||
50% {{ transform: scale(1.03); border-color:#00a870; }}
|
||
}}
|
||
</style>
|
||
<div style='position:absolute;left:36px;right:36px;top:24px;height:64px;display:flex;align-items:center;justify-content:space-between;border-bottom:1px solid #d9e7dc;'>
|
||
<div>
|
||
<h2 style='font-size:26px;margin:0 0 6px;line-height:1.2;color:#00543d;'>{title}</h2>
|
||
<p style='font-size:14px;color:#6b7f76;margin:0;'>{type_label}教学动画 · 清爽课堂演示版</p>
|
||
</div>
|
||
<span style='padding:8px 14px;border-radius:18px;background:#e8f7ef;color:#007a52;font-size:13px;font-weight:700;'>AI 动画</span>
|
||
</div>
|
||
<div style='position:absolute;left:54px;right:54px;top:112px;bottom:96px;background:#fff;border:1px solid #dce8de;border-radius:16px;box-shadow:0 12px 30px rgba(15,84,61,.08);overflow:hidden;'>
|
||
<div style='position:absolute;left:48px;right:48px;top:112px;height:3px;background:#d9e7dc;border-radius:2px;'></div>
|
||
<div style='position:absolute;left:48px;right:48px;top:112px;height:3px;background:linear-gradient(90deg,transparent,#00a870,transparent);animation:scanLine 4s linear infinite;'></div>
|
||
<div style='position:absolute;left:70px;top:170px;width:66px;height:66px;border-radius:50%;background:#00a870;animation:floatBall 6s ease-in-out infinite,pulseGlow 2s ease-in-out infinite;'></div>
|
||
<div style='position:absolute;left:154px;top:150px;width:18px;height:18px;border-radius:50%;background:#f59e0b;animation:drift 3.2s ease-in-out infinite;'></div>
|
||
<div style='position:absolute;left:334px;top:136px;width:26px;height:26px;border-radius:50%;background:#93c5fd;animation:drift 4s ease-in-out infinite;'></div>
|
||
<div style='position:absolute;left:554px;top:158px;width:14px;height:14px;border-radius:50%;background:#86efac;animation:drift 2.8s ease-in-out infinite;'></div>
|
||
</div>
|
||
<div style='position:absolute;left:94px;right:94px;bottom:116px;display:grid;grid-template-columns:repeat(3,1fr);gap:12px;'>
|
||
<div style='padding:14px;border:1px solid #d9e7dc;border-radius:12px;background:#fff;animation:stepPulse 2.4s ease-in-out infinite;'>1. 提出问题</div>
|
||
<div style='padding:14px;border:1px solid #d9e7dc;border-radius:12px;background:#fff;animation:stepPulse 2.4s ease-in-out infinite .4s;'>2. 动态观察</div>
|
||
<div style='padding:14px;border:1px solid #d9e7dc;border-radius:12px;background:#fff;animation:stepPulse 2.4s ease-in-out infinite .8s;'>3. 归纳结论</div>
|
||
</div>
|
||
<div style='position:absolute;left:36px;right:36px;bottom:30px;display:flex;gap:10px;align-items:center;color:#6b7f76;'>
|
||
<span style='padding:9px 16px;border-radius:18px;background:#00543d;color:#fff;font-weight:700;'>演示动画</span>
|
||
<span style='font-size:13px;'>可保存、可预览、可插入课件</span>
|
||
</div>
|
||
</div>"""
|
||
return self._animation_result(prompt, anim_type, html, f"{anim_type} 教学动画演示")
|
||
|
||
|
||
def _fallback_geometry_animation(self, prompt: str, anim_type: str) -> dict:
|
||
title, _ = self._animation_meta(prompt, anim_type)
|
||
html = f"""<div style='width:100%;height:100%;position:relative;overflow:hidden;background:#f6fbf7;font-family:Microsoft YaHei,sans-serif;color:#143b31;'>
|
||
<style>
|
||
@keyframes drawSide {{ 0% {{ stroke-dashoffset: 760; }} 60%,100% {{ stroke-dashoffset: 0; }} }}
|
||
@keyframes popAngle {{ 0%,55% {{ transform: scale(0); opacity: 0; }} 70%,90% {{ transform: scale(1); opacity: 1; }} 100% {{ transform: scale(0); opacity: 0; }} }}
|
||
@keyframes growA {{ 0%,12% {{ width: 0; }} 34%,84% {{ width: 44.4%; }} 96%,100% {{ width: 0; }} }}
|
||
@keyframes growB {{ 0%,28% {{ width: 0; }} 50%,84% {{ width: 33.3%; }} 96%,100% {{ width: 0; }} }}
|
||
@keyframes growC {{ 0%,44% {{ width: 0; }} 66%,84% {{ width: 22.3%; }} 96%,100% {{ width: 0; }} }}
|
||
@keyframes stamp {{ 0%,70% {{ transform: scale(0) rotate(-12deg); opacity: 0; }} 82%,92% {{ transform: scale(1) rotate(-6deg); opacity: 1; }} 100% {{ transform: scale(0); opacity: 0; }} }}
|
||
@keyframes barPulse {{ 0%,100% {{ box-shadow:0 6px 16px rgba(0,84,61,.10); }} 50% {{ box-shadow:0 10px 24px rgba(0,84,61,.18); }} }}
|
||
</style>
|
||
<div style='position:absolute;left:36px;right:36px;top:24px;height:64px;display:flex;align-items:center;justify-content:space-between;border-bottom:1px solid #d9e7dc;'>
|
||
<div>
|
||
<h2 style='margin:0 0 6px;font-size:26px;color:#00543d;'>{title}</h2>
|
||
<p style='margin:0;font-size:14px;color:#6b7f76;'>三角形三个内角拼合成一个平角 = 180°</p>
|
||
</div>
|
||
<span style='padding:8px 14px;border-radius:18px;background:#e8f7ef;color:#007a52;font-size:13px;font-weight:700;'>几何图形</span>
|
||
</div>
|
||
<div style='position:absolute;left:48px;right:48px;top:110px;bottom:64px;background:#fff;border:1px solid #dce8de;border-radius:16px;box-shadow:0 12px 30px rgba(15,84,61,.08);overflow:hidden;'>
|
||
<div style='position:absolute;left:36px;top:30px;width:45%;bottom:30px;display:flex;align-items:center;justify-content:center;'>
|
||
<svg viewBox='0 0 320 280' style='width:100%;height:100%;'>
|
||
<polygon points='160,34 44,242 276,242' fill='rgba(0,168,112,.06)' stroke='#00a870' stroke-width='5' stroke-linejoin='round' stroke-dasharray='760' stroke-dashoffset='760' style='animation:drawSide 6s ease-in-out infinite;'/>
|
||
<circle cx='160' cy='34' r='10' fill='#00a870' style='transform-box:fill-box;transform-origin:center;animation:popAngle 6s ease-in-out infinite;'/>
|
||
<text x='176' y='56' fill='#00543d' font-size='17' font-weight='700'>∠α 80°</text>
|
||
<circle cx='44' cy='242' r='10' fill='#f59e0b' style='transform-box:fill-box;transform-origin:center;animation:popAngle 6s ease-in-out infinite .6s;'/>
|
||
<text x='14' y='234' fill='#92400e' font-size='17' font-weight='700'>∠β 60°</text>
|
||
<circle cx='276' cy='242' r='10' fill='#3b82f6' style='transform-box:fill-box;transform-origin:center;animation:popAngle 6s ease-in-out infinite 1.2s;'/>
|
||
<text x='220' y='234' fill='#1d4ed8' font-size='17' font-weight='700'>∠γ 40°</text>
|
||
</svg>
|
||
</div>
|
||
<div style='position:absolute;right:30px;top:34px;width:47%;bottom:34px;display:flex;flex-direction:column;justify-content:center;gap:20px;'>
|
||
<div style='text-align:center;font-size:16px;color:#00543d;font-weight:700;'>拼角验证:三个内角 → 一条平角</div>
|
||
<div style='position:relative;height:46px;border-radius:10px;background:#f1f5f4;border:1px solid #e1eee4;overflow:hidden;animation:barPulse 3s ease-in-out infinite;'>
|
||
<div style='position:absolute;left:0;top:0;height:100%;background:linear-gradient(90deg,#00a870,#34d399);display:flex;align-items:center;justify-content:center;color:#fff;font-size:14px;font-weight:700;overflow:hidden;white-space:nowrap;animation:growA 6s ease-in-out infinite;'>α 80°</div>
|
||
<div style='position:absolute;left:44.4%;top:0;height:100%;background:linear-gradient(90deg,#f59e0b,#fbbf24);display:flex;align-items:center;justify-content:center;color:#fff;font-size:14px;font-weight:700;overflow:hidden;white-space:nowrap;animation:growB 6s ease-in-out infinite;'>β 60°</div>
|
||
<div style='position:absolute;left:77.7%;top:0;height:100%;background:linear-gradient(90deg,#3b82f6,#60a5fa);display:flex;align-items:center;justify-content:center;color:#fff;font-size:14px;font-weight:700;overflow:hidden;white-space:nowrap;animation:growC 6s ease-in-out infinite;'>γ 40°</div>
|
||
</div>
|
||
<div style='display:flex;justify-content:space-between;font-size:13px;color:#6b7f76;padding:0 2px;'><span>0°</span><span>180°</span></div>
|
||
<div style='text-align:center;font-size:22px;font-weight:800;color:#00543d;'>80° + 60° + 40° = 180°</div>
|
||
<div style='margin:0 auto;width:116px;height:116px;border-radius:50%;background:#e8f7ef;border:3px solid #00a870;display:flex;align-items:center;justify-content:center;font-size:26px;font-weight:800;color:#00543d;animation:stamp 6s ease-in-out infinite;'>180°</div>
|
||
</div>
|
||
</div>
|
||
</div>"""
|
||
return self._animation_result(prompt, anim_type, html, "三角形内角和动态演示:拼角成平角 180°")
|
||
|
||
|
||
def _fallback_function_animation(self, prompt: str, anim_type: str) -> dict:
|
||
title, _ = self._animation_meta(prompt, anim_type)
|
||
html = f"""<div style='width:100%;height:100%;position:relative;overflow:hidden;background:#f6fbf7;font-family:Microsoft YaHei,sans-serif;color:#15324a;'>
|
||
<style>
|
||
@keyframes drawCurve {{ 0% {{ stroke-dashoffset: 760; }} 100% {{ stroke-dashoffset: 0; }} }}
|
||
@keyframes movePoint {{ 0% {{ transform:translate(70px,360px); }} 25% {{ transform:translate(205px,230px); }} 50% {{ transform:translate(340px,150px); }} 75% {{ transform:translate(475px,230px); }} 100% {{ transform:translate(610px,360px); }} }}
|
||
@keyframes coeffPulse {{ 0%,100% {{ background:#e0f2fe; transform:scale(1); }} 50% {{ background:#bbf7d0; transform:scale(1.04); }} }}
|
||
</style>
|
||
<div style='position:absolute;left:36px;right:36px;top:24px;height:64px;display:flex;align-items:center;justify-content:space-between;border-bottom:1px solid #d9e7dc;'>
|
||
<div>
|
||
<h2 style='margin:0 0 6px;font-size:26px;color:#00543d;'>{title}</h2>
|
||
<p style='margin:0;font-size:14px;color:#6b7f76;'>观察二次函数图像随参数变化的趋势</p>
|
||
</div>
|
||
<span style='padding:8px 14px;border-radius:18px;background:#e8f7ef;color:#007a52;font-size:13px;font-weight:700;'>函数图像</span>
|
||
</div>
|
||
<div style='position:absolute;left:48px;right:48px;top:110px;bottom:64px;background:#fff;border:1px solid #dce8de;border-radius:16px;box-shadow:0 12px 30px rgba(15,84,61,.08);overflow:hidden;'>
|
||
<svg viewBox='0 0 680 420' style='position:absolute;left:24px;top:18px;width:calc(100% - 270px);height:calc(100% - 36px);background:#fbfefd;border:1px solid #e1eee4;border-radius:14px;'>
|
||
<defs><pattern id='grid' width='32' height='32' patternUnits='userSpaceOnUse'><path d='M32 0H0V32' fill='none' stroke='#e5efe8' stroke-width='1'/></pattern></defs>
|
||
<rect x='0' y='0' width='680' height='420' fill='url(#grid)'/>
|
||
<line x1='45' y1='350' x2='640' y2='350' stroke='#8aa39a' stroke-width='2'/>
|
||
<line x1='340' y1='35' x2='340' y2='382' stroke='#8aa39a' stroke-width='2'/>
|
||
<path d='M70 360 C150 265 230 175 340 150 C450 175 530 265 610 360' fill='none' stroke='#00a870' stroke-width='6' stroke-linecap='round' stroke-dasharray='760' stroke-dashoffset='760' style='animation:drawCurve 4s ease-in-out infinite alternate;'/>
|
||
<circle r='9' fill='#f97316' style='animation:movePoint 4s ease-in-out infinite;'/>
|
||
<text x='356' y='70' fill='#00543d' font-size='20' font-weight='700'>y = ax² + bx + c</text>
|
||
<text x='522' y='332' fill='#64748b' font-size='14'>x</text>
|
||
<text x='355' y='54' fill='#64748b' font-size='14'>y</text>
|
||
</svg>
|
||
<div style='position:absolute;right:24px;top:28px;width:210px;display:grid;gap:12px;'>
|
||
<div style='padding:14px;border-radius:12px;background:#ecfdf5;border:1px solid #bbf7d0;animation:coeffPulse 2.5s ease-in-out infinite;'><strong style='color:#00543d;'>a</strong><br/><span style='font-size:13px;color:#6b7f76;'>控制开口方向和宽窄</span></div>
|
||
<div style='padding:14px;border-radius:12px;background:#fffbeb;border:1px solid #fde68a;animation:coeffPulse 2.5s ease-in-out infinite .4s;'><strong style='color:#92400e;'>b</strong><br/><span style='font-size:13px;color:#6b7f76;'>影响对称轴位置</span></div>
|
||
<div style='padding:14px;border-radius:12px;background:#eff6ff;border:1px solid #bfdbfe;animation:coeffPulse 2.5s ease-in-out infinite .8s;'><strong style='color:#1d4ed8;'>c</strong><br/><span style='font-size:13px;color:#6b7f76;'>决定与 y 轴交点</span></div>
|
||
</div>
|
||
</div>
|
||
</div>"""
|
||
return self._animation_result(prompt, anim_type, html, "函数图像动态演示")
|
||
|
||
def _fallback_cylinder_animation(self, prompt: str, anim_type: str) -> dict:
|
||
title, _ = self._animation_meta(prompt, anim_type)
|
||
html = f"""<div style='width:100%;height:100%;position:relative;overflow:hidden;background:#f6fbf7;font-family:Microsoft YaHei,sans-serif;color:#143b31;'>
|
||
<style>
|
||
@keyframes sliceMove {{ 0%,100% {{ transform:translateX(0); opacity:.9; }} 50% {{ transform:translateX(130px); opacity:.65; }} }}
|
||
@keyframes fillRise {{ 0% {{ height:0; }} 100% {{ height:156px; }} }}
|
||
@keyframes formulaGlow {{ 0%,100% {{ border-color:#d9e7dc; }} 50% {{ border-color:#00a870; }} }}
|
||
</style>
|
||
<div style='position:absolute;left:36px;right:36px;top:24px;height:64px;border-bottom:1px solid #d9e7dc;'>
|
||
<h2 style='margin:0 0 6px;font-size:26px;color:#00543d;'>{title}</h2>
|
||
<p style='margin:0;color:#6b7f76;font-size:14px;'>把圆柱看作“底面积 × 高”的累积</p>
|
||
</div>
|
||
<div style='position:absolute;left:48px;right:48px;top:110px;bottom:64px;background:#fff;border:1px solid #dce8de;border-radius:16px;box-shadow:0 12px 30px rgba(15,84,61,.08);'>
|
||
<div style='position:absolute;left:70px;top:70px;width:180px;height:220px;'>
|
||
<div style='position:absolute;left:30px;top:30px;width:120px;height:156px;background:linear-gradient(90deg,#86efac,#d1fae5,#86efac);border-left:2px solid #00a870;border-right:2px solid #00a870;'></div>
|
||
<div style='position:absolute;left:30px;top:10px;width:120px;height:42px;border-radius:50%;background:#d1fae5;border:2px solid #00a870;'></div>
|
||
<div style='position:absolute;left:30px;top:164px;width:120px;height:42px;border-radius:50%;background:#a7f3d0;border:2px solid #00a870;'></div>
|
||
<div style='position:absolute;left:30px;bottom:14px;width:120px;background:rgba(0,168,112,.22);animation:fillRise 3s ease-in-out infinite alternate;'></div>
|
||
</div>
|
||
<div style='position:absolute;left:320px;top:92px;display:grid;gap:7px;'>
|
||
<div style='width:150px;height:14px;border-radius:50%;background:#d1fae5;border:1px solid #00a870;animation:sliceMove 2.8s ease-in-out infinite;'></div>
|
||
<div style='width:150px;height:14px;border-radius:50%;background:#bbf7d0;border:1px solid #00a870;animation:sliceMove 2.8s ease-in-out infinite .15s;'></div>
|
||
<div style='width:150px;height:14px;border-radius:50%;background:#86efac;border:1px solid #00a870;animation:sliceMove 2.8s ease-in-out infinite .3s;'></div>
|
||
<div style='width:150px;height:14px;border-radius:50%;background:#4ade80;border:1px solid #00a870;animation:sliceMove 2.8s ease-in-out infinite .45s;'></div>
|
||
</div>
|
||
<div style='position:absolute;right:38px;top:86px;width:230px;padding:24px;border-radius:14px;background:#f8faf8;border:1px solid #d9e7dc;animation:formulaGlow 2.6s ease-in-out infinite;'>
|
||
<div style='font-size:16px;color:#00543d;margin-bottom:12px;font-weight:700;'>体积公式</div>
|
||
<div style='font-size:32px;font-weight:800;color:#12372d;'>V = πr²h</div>
|
||
<p style='font-size:14px;line-height:1.7;color:#6b7f76;'>底面积 πr² 沿高度 h 连续叠加,得到圆柱体积。</p>
|
||
</div>
|
||
</div>
|
||
</div>"""
|
||
return self._animation_result(prompt, anim_type, html, "圆柱体积推导动画")
|
||
|
||
def _fallback_photosynthesis_animation(self, prompt: str, anim_type: str) -> dict:
|
||
title, _ = self._animation_meta(prompt, anim_type)
|
||
html = f"""<div style='width:100%;height:100%;position:relative;overflow:hidden;background:#f6fbf7;font-family:Microsoft YaHei,sans-serif;color:#14532d;'>
|
||
<style>
|
||
@keyframes ray {{ 0% {{ opacity:.15; transform:translateY(-12px); }} 50% {{ opacity:.85; transform:translateY(8px); }} 100% {{ opacity:.15; transform:translateY(-12px); }} }}
|
||
@keyframes bubbleIn {{ 0% {{ transform:translateX(0); opacity:.2; }} 50% {{ opacity:1; }} 100% {{ transform:translateX(235px); opacity:.2; }} }}
|
||
@keyframes oxygenOut {{ 0% {{ transform:translate(0,0); opacity:.1; }} 100% {{ transform:translate(180px,-90px); opacity:1; }} }}
|
||
</style>
|
||
<div style='position:absolute;left:36px;right:36px;top:24px;height:64px;border-bottom:1px solid #d9e7dc;'>
|
||
<h2 style='margin:0 0 6px;font-size:26px;color:#00543d;'>{title}</h2>
|
||
<p style='margin:0;color:#6b7f76;font-size:14px;'>光能驱动二氧化碳和水转化为有机物</p>
|
||
</div>
|
||
<div style='position:absolute;left:48px;right:48px;top:110px;bottom:64px;background:#fff;border:1px solid #dce8de;border-radius:16px;box-shadow:0 12px 30px rgba(15,84,61,.08);overflow:hidden;'>
|
||
<div style='position:absolute;left:42px;top:36px;width:90px;height:90px;border-radius:50%;background:#facc15;box-shadow:0 10px 28px rgba(250,204,21,.35);'></div>
|
||
<div style='position:absolute;left:160px;top:72px;width:5px;height:170px;background:#fde047;transform:rotate(25deg);animation:ray 2.2s ease-in-out infinite;'></div>
|
||
<div style='position:absolute;left:204px;top:72px;width:5px;height:190px;background:#fde047;transform:rotate(18deg);animation:ray 2.2s ease-in-out infinite .25s;'></div>
|
||
<div style='position:absolute;left:270px;top:92px;width:280px;height:164px;border-radius:50% 8% 50% 8%;background:linear-gradient(135deg,#16a34a,#bbf7d0);transform:rotate(-8deg);box-shadow:inset 0 0 30px rgba(255,255,255,.45),0 14px 28px rgba(0,84,61,.12);'></div>
|
||
<div style='position:absolute;left:394px;top:164px;width:150px;height:6px;background:#166534;transform:rotate(-8deg);border-radius:4px;'></div>
|
||
<div style='position:absolute;left:52px;bottom:54px;padding:14px 18px;border-radius:14px;background:#eff6ff;border:1px solid #bfdbfe;font-weight:700;'>CO₂ + H₂O</div>
|
||
<div style='position:absolute;left:116px;bottom:125px;width:26px;height:26px;border-radius:50%;background:#38bdf8;animation:bubbleIn 3s ease-in-out infinite;'></div>
|
||
<div style='position:absolute;left:156px;bottom:160px;width:20px;height:20px;border-radius:50%;background:#22c55e;animation:bubbleIn 3s ease-in-out infinite .6s;'></div>
|
||
<div style='position:absolute;right:168px;top:120px;width:28px;height:28px;border-radius:50%;background:#e0f2fe;border:2px solid #38bdf8;animation:oxygenOut 2.8s ease-in-out infinite;'></div>
|
||
<div style='position:absolute;right:34px;bottom:54px;padding:16px 22px;border-radius:14px;background:#ecfdf5;border:1px solid #bbf7d0;box-shadow:0 8px 18px rgba(15,84,61,.08);'>
|
||
<strong style='color:#00543d;'>产物</strong><br/>葡萄糖 + O₂
|
||
</div>
|
||
</div>
|
||
</div>"""
|
||
return self._animation_result(prompt, anim_type, html, "光合作用过程动画")
|
||
|
||
def _fallback_chemistry_animation(self, prompt: str, anim_type: str) -> dict:
|
||
title, _ = self._animation_meta(prompt, anim_type)
|
||
html = f"""<div style='width:100%;height:100%;position:relative;overflow:hidden;background:#f6fbf7;font-family:Microsoft YaHei,sans-serif;color:#143b31;'>
|
||
<style>
|
||
@keyframes collideA {{ 0%,100% {{ transform:translate(0,0); }} 50% {{ transform:translate(220px,40px); }} }}
|
||
@keyframes collideB {{ 0%,100% {{ transform:translate(0,0); }} 50% {{ transform:translate(-220px,-40px); }} }}
|
||
@keyframes flash {{ 0%,45%,100% {{ opacity:.15; transform:scale(.7); }} 50% {{ opacity:1; transform:scale(1.5); }} }}
|
||
@keyframes product {{ 0%,55% {{ opacity:0; transform:scale(.5); }} 80%,100% {{ opacity:1; transform:scale(1); }} }}
|
||
</style>
|
||
<div style='position:absolute;left:36px;right:36px;top:24px;height:64px;border-bottom:1px solid #d9e7dc;'>
|
||
<h2 style='margin:0 0 6px;font-size:26px;color:#00543d;'>{title}</h2>
|
||
<p style='margin:0;color:#6b7f76;font-size:14px;'>观察微粒碰撞、重组并形成生成物</p>
|
||
</div>
|
||
<div style='position:absolute;left:48px;right:48px;top:110px;bottom:64px;background:#fff;border:1px solid #dce8de;border-radius:16px;box-shadow:0 12px 30px rgba(15,84,61,.08);'>
|
||
<div style='position:absolute;left:80px;top:122px;width:74px;height:74px;border-radius:50%;background:#93c5fd;animation:collideA 3s ease-in-out infinite;border:3px solid #2563eb;'></div>
|
||
<div style='position:absolute;right:100px;top:162px;width:68px;height:68px;border-radius:50%;background:#fdba74;animation:collideB 3s ease-in-out infinite;border:3px solid #f97316;'></div>
|
||
<div style='position:absolute;left:50%;top:150px;width:96px;height:96px;margin-left:-48px;border-radius:50%;background:#fef3c7;animation:flash 3s ease-in-out infinite;border:2px solid #f59e0b;'></div>
|
||
<div style='position:absolute;left:50%;top:138px;margin-left:-82px;display:flex;gap:12px;animation:product 3s ease-in-out infinite;'>
|
||
<div style='width:62px;height:62px;border-radius:50%;background:#86efac;border:3px solid #00a870;'></div>
|
||
<div style='width:62px;height:62px;border-radius:50%;background:#c4b5fd;border:3px solid #7c3aed;'></div>
|
||
</div>
|
||
<div style='position:absolute;left:60px;bottom:44px;font-size:20px;color:#00543d;font-weight:700;'>反应物</div>
|
||
<div style='position:absolute;left:50%;bottom:42px;transform:translateX(-50%);font-size:32px;color:#00a870;'>→</div>
|
||
<div style='position:absolute;right:70px;bottom:44px;font-size:20px;color:#00543d;font-weight:700;'>生成物</div>
|
||
</div>
|
||
</div>"""
|
||
return self._animation_result(prompt, anim_type, html, "化学反应碰撞动画")
|
||
|
||
def _fallback_orbit_animation(self, prompt: str, anim_type: str) -> dict:
|
||
title, _ = self._animation_meta(prompt, anim_type)
|
||
html = f"""<div style='width:100%;height:100%;position:relative;overflow:hidden;background:#f6fbf7;font-family:Microsoft YaHei,sans-serif;color:#143b31;'>
|
||
<style>
|
||
@keyframes spin {{ from {{ transform:rotate(0deg); }} to {{ transform:rotate(360deg); }} }}
|
||
@keyframes rayPulse {{ 0%,100% {{ opacity:.28; }} 50% {{ opacity:.82; }} }}
|
||
@keyframes focusGlow {{ 0%,100% {{ transform:scale(1); box-shadow:0 6px 14px rgba(0,84,61,.08); }} 50% {{ transform:scale(1.02); box-shadow:0 10px 22px rgba(0,84,61,.16); }} }}
|
||
.season-radio {{ position:absolute; opacity:0; pointer-events:none; }}
|
||
.season-btn {{ height:46px; border-radius:13px; border:1px solid #d9e7dc; background:#fff; color:#516b61; display:flex; align-items:center; justify-content:center; gap:6px; font-weight:700; cursor:pointer; transition:all .22s ease; box-shadow:0 4px 12px rgba(15,84,61,.05); user-select:none; }}
|
||
.season-btn:hover {{ transform:translateY(-1px); border-color:#00a870; color:#00543d; }}
|
||
.season-btn:active {{ transform:translateY(0); }}
|
||
.earth-dot {{ position:absolute; width:70px; height:70px; border-radius:50%; background:linear-gradient(135deg,#60a5fa 0%,#60a5fa 48%,#22c55e 49%,#86efac 100%); border:4px solid #fff; box-shadow:0 10px 24px rgba(0,84,61,.18); transition:all .35s ease; }}
|
||
.axis {{ position:absolute; left:50%; top:-8px; width:3px; height:86px; background:#00543d; transform:translateX(-50%) rotate(-23deg); border-radius:2px; opacity:.78; }}
|
||
.earth-dot::after {{ content:''; position:absolute; inset:0; border-radius:50%; background:linear-gradient(90deg,rgba(255,255,255,.38),transparent 55%,rgba(0,0,0,.14)); animation:spin 5s linear infinite; }}
|
||
.season-card {{ position:absolute; right:28px; top:34px; width:240px; min-height:132px; padding:18px; border:1px solid #d9e7dc; border-radius:16px; background:#f8faf8; box-shadow:0 8px 20px rgba(15,84,61,.08); opacity:0; transform:translateY(8px); transition:all .25s ease; pointer-events:none; }}
|
||
.season-card strong {{ display:block; color:#00543d; font-size:22px; margin-bottom:8px; }}
|
||
.season-card span {{ color:#6b7f76; font-size:14px; line-height:1.7; }}
|
||
#season-spring:checked ~ .stage .earth-dot {{ left:116px; top:126px; }}
|
||
#season-summer:checked ~ .stage .earth-dot {{ left:306px; top:44px; }}
|
||
#season-autumn:checked ~ .stage .earth-dot {{ left:496px; top:126px; }}
|
||
#season-winter:checked ~ .stage .earth-dot {{ left:306px; top:208px; }}
|
||
#season-spring:checked ~ .stage .spring-card,
|
||
#season-summer:checked ~ .stage .summer-card,
|
||
#season-autumn:checked ~ .stage .autumn-card,
|
||
#season-winter:checked ~ .stage .winter-card {{ opacity:1; transform:translateY(0); }}
|
||
#season-spring:checked ~ .stage .spring-card strong,
|
||
#season-summer:checked ~ .stage .summer-card strong,
|
||
#season-autumn:checked ~ .stage .autumn-card strong,
|
||
#season-winter:checked ~ .stage .winter-card strong {{ animation:focusGlow 2.2s ease-in-out infinite; }}
|
||
#season-spring:checked ~ .tabs label[for='season-spring'],
|
||
#season-summer:checked ~ .tabs label[for='season-summer'],
|
||
#season-autumn:checked ~ .tabs label[for='season-autumn'],
|
||
#season-winter:checked ~ .tabs label[for='season-winter'] {{ background:#00543d; color:#fff; border-color:#00543d; box-shadow:0 8px 18px rgba(0,84,61,.18); }}
|
||
</style>
|
||
<div style='position:absolute;left:36px;right:36px;top:24px;height:64px;border-bottom:1px solid #d9e7dc;'>
|
||
<h2 style='margin:0 0 6px;font-size:26px;color:#00543d;'>{title}</h2>
|
||
<p style='margin:0;color:#6b7f76;font-size:14px;'>点击春夏秋冬,观察地球公转位置与太阳直射变化</p>
|
||
</div>
|
||
<input class='season-radio' id='season-spring' name='season' type='radio' checked>
|
||
<input class='season-radio' id='season-summer' name='season' type='radio'>
|
||
<input class='season-radio' id='season-autumn' name='season' type='radio'>
|
||
<input class='season-radio' id='season-winter' name='season' type='radio'>
|
||
<div class='stage' style='position:absolute;left:48px;right:48px;top:110px;bottom:126px;background:#fff;border:1px solid #dce8de;border-radius:16px;box-shadow:0 12px 30px rgba(15,84,61,.08);overflow:hidden;z-index:1;'>
|
||
<div style='position:absolute;left:54px;top:24px;padding:8px 12px;border-radius:18px;background:#e8f7ef;color:#00543d;font-size:13px;font-weight:700;'>太阳直射点随公转变化</div>
|
||
<div style='position:absolute;left:300px;top:126px;width:92px;height:92px;border-radius:50%;background:#facc15;box-shadow:0 8px 26px rgba(250,204,21,.35);'></div>
|
||
<div style='position:absolute;left:116px;top:72px;width:450px;height:250px;border:1.5px dashed #9ca3af;border-radius:50%;'></div>
|
||
<div style='position:absolute;left:356px;top:166px;width:240px;height:4px;background:linear-gradient(90deg,#fde68a,transparent);transform:rotate(-18deg);transform-origin:left center;animation:rayPulse 2.4s ease-in-out infinite;'></div>
|
||
<div style='position:absolute;left:356px;top:166px;width:240px;height:4px;background:linear-gradient(90deg,#fde68a,transparent);transform:rotate(18deg);transform-origin:left center;animation:rayPulse 2.4s ease-in-out infinite .4s;'></div>
|
||
<div class='earth-dot'><div class='axis'></div></div>
|
||
<div class='season-card spring-card'><strong>春分</strong><span>太阳直射赤道附近,南北半球昼夜接近等长,气温逐渐回升。</span></div>
|
||
<div class='season-card summer-card'><strong>夏至</strong><span>北半球获得更多太阳辐射,白昼较长,正午太阳高度较高。</span></div>
|
||
<div class='season-card autumn-card'><strong>秋分</strong><span>太阳再次直射赤道附近,昼夜接近等长,气温逐渐降低。</span></div>
|
||
<div class='season-card winter-card'><strong>冬至</strong><span>北半球太阳高度较低,白昼较短,获得太阳辐射较少。</span></div>
|
||
<div style='position:absolute;left:58px;bottom:26px;color:#6b7f76;font-size:13px;'>提示:地轴方向基本保持不变,这是四季变化的重要原因。</div>
|
||
</div>
|
||
<div class='tabs' style='position:absolute;left:74px;right:74px;bottom:56px;display:grid;grid-template-columns:repeat(4,1fr);gap:12px;z-index:2;'>
|
||
<label class='season-btn' for='season-spring'>春分</label>
|
||
<label class='season-btn' for='season-summer'>夏至</label>
|
||
<label class='season-btn' for='season-autumn'>秋分</label>
|
||
<label class='season-btn' for='season-winter'>冬至</label>
|
||
</div>
|
||
</div>"""
|
||
return self._animation_result(prompt, anim_type, html, "地球公转与四季动画")
|
||
|
||
def _fallback_motion_animation(self, prompt: str, anim_type: str) -> dict:
|
||
title, _ = self._animation_meta(prompt, anim_type)
|
||
html = f"""<div style='width:100%;height:100%;position:relative;overflow:hidden;background:#f6fbf7;font-family:Microsoft YaHei,sans-serif;color:#0f172a;'>
|
||
<style>
|
||
@keyframes roll {{ 0% {{ transform:translateX(0) rotate(0deg); }} 100% {{ transform:translateX(520px) rotate(720deg); }} }}
|
||
@keyframes vector {{ 0%,100% {{ width:70px; }} 50% {{ width:150px; }} }}
|
||
@keyframes meter {{ 0% {{ width:12%; }} 100% {{ width:86%; }} }}
|
||
</style>
|
||
<div style='position:absolute;left:36px;right:36px;top:24px;height:64px;border-bottom:1px solid #d9e7dc;'>
|
||
<h2 style='margin:0 0 6px;font-size:26px;color:#00543d;'>{title}</h2>
|
||
<p style='margin:0;color:#6b7f76;font-size:14px;'>观察物体运动方向与速度大小变化</p>
|
||
</div>
|
||
<div style='position:absolute;left:48px;right:48px;top:110px;bottom:64px;background:#fff;border:1px solid #dce8de;border-radius:16px;box-shadow:0 12px 30px rgba(15,84,61,.08);'>
|
||
<div style='position:absolute;left:48px;top:220px;width:640px;height:8px;background:#8aa39a;transform:rotate(-10deg);transform-origin:left center;border-radius:6px;'></div>
|
||
<div style='position:absolute;left:72px;top:148px;width:56px;height:56px;border-radius:50%;background:#f97316;animation:roll 4s ease-in-out infinite alternate;box-shadow:inset -8px -8px 0 rgba(0,0,0,.16),0 10px 20px rgba(249,115,22,.2);'></div>
|
||
<div style='position:absolute;left:146px;top:110px;height:5px;background:#00a870;animation:vector 2.4s ease-in-out infinite;border-radius:4px;'></div>
|
||
<div style='position:absolute;left:288px;top:94px;color:#00543d;font-weight:700;'>速度方向</div>
|
||
<div style='position:absolute;left:56px;right:56px;bottom:72px;height:12px;background:#e2e8f0;border-radius:6px;overflow:hidden;'>
|
||
<div style='height:100%;background:linear-gradient(90deg,#86efac,#00a870);animation:meter 4s ease-in-out infinite alternate;'></div>
|
||
</div>
|
||
<div style='position:absolute;left:56px;bottom:34px;color:#6b7f76;'>速度随时间变化:慢 → 快</div>
|
||
</div>
|
||
</div>"""
|
||
return self._animation_result(prompt, anim_type, html, "物体运动与速度变化动画")
|
||
|
||
|
||
def _fallback_exercise(self, prompt, exercise_type, subject, knowledge_points, count):
|
||
total = max(1, min(count or 5, 10))
|
||
raw_topic = (prompt or "").strip()[:30] or "课堂练习"
|
||
# 去掉结尾的练习/题型字样,避免标题拼成「…练习练习」
|
||
topic = re.sub(
|
||
r"(?:的)?(?:互动)?(?:练习题|题目集|练习|题目|试题|习题|题库|选择题|填空题|判断题|应用题|解答题|计算题|题)+$",
|
||
"", raw_topic,
|
||
).strip() or raw_topic
|
||
subj = (subject or "").strip()
|
||
bank = self._question_bank(topic, subj)
|
||
if not bank:
|
||
bank = self._generic_question_bank(topic, subj)
|
||
questions = []
|
||
for idx in range(total):
|
||
q = dict(bank[idx % len(bank)])
|
||
q["id"] = idx + 1
|
||
q["difficulty"] = ["easy", "medium", "hard"][idx % 3]
|
||
q["html"] = self._exercise_html(q)
|
||
questions.append(q)
|
||
return {"title": (topic + "练习")[:40], "questions": questions}
|
||
|
||
def _question_bank(self, topic, subject):
|
||
t = (topic or "").lower()
|
||
s = (subject or "").lower()
|
||
combined = t + " " + s
|
||
topic_bank = self._topic_question_bank(t, subject or "")
|
||
if topic_bank:
|
||
return topic_bank
|
||
is_math = any(k in combined for k in ("数学", "math", "分数", "加减", "乘除", "方程", "几何", "面积", "比例"))
|
||
is_chinese = any(k in combined for k in ("语文", "古诗", "词语", "阅读", "修辞", "拼音", "汉字"))
|
||
is_english = any(k in combined for k in ("english", "英语", "grammar", "vocabulary", "tense"))
|
||
is_science = any(k in combined for k in ("科学", "物理", "化学", "生物", "地理", "光合作用", "重力", "电路"))
|
||
if is_math:
|
||
return self._math_questions(topic)
|
||
if is_chinese:
|
||
return self._chinese_questions(topic)
|
||
if is_english:
|
||
return self._english_questions(topic)
|
||
if is_science:
|
||
return self._science_questions(topic)
|
||
return []
|
||
|
||
def _generic_question_bank(self, topic, subject):
|
||
return [
|
||
{
|
||
"type": "choice",
|
||
"question": f"关于「{topic}」,下列说法正确的是?",
|
||
"options": [f"「{topic}」是本课的重要学习内容", f"「{topic}」与本课无关", f"「{topic}」只需死记硬背", f"「{topic}」无需理解"],
|
||
"answer": f"「{topic}」是本课的重要学习内容",
|
||
"explanation": f"「{topic}」是核心知识点,需要重点掌握并灵活运用。",
|
||
},
|
||
{
|
||
"type": "choice",
|
||
"question": f"学习「{topic}」时,最有效的方法是?",
|
||
"options": ["理解原理并结合实例练习", "只看不练", "抄袭同学的答案", "等考试前突击"],
|
||
"answer": "理解原理并结合实例练习",
|
||
"explanation": "理解性学习加反复练习是掌握知识的最佳途径。",
|
||
},
|
||
{
|
||
"type": "choice",
|
||
"question": f"下列哪项不属于「{topic}」的应用场景?",
|
||
"options": [f"课堂讲解中举例说明", f"生活实践中观察现象", "与本课完全无关的其他话题", f"课后作业中运用"],
|
||
"answer": "与本课完全无关的其他话题",
|
||
"explanation": "知识的应用应当围绕课堂、生活和作业场景,而非无关话题。",
|
||
},
|
||
{
|
||
"type": "fill_blank",
|
||
"question": f"请简述「{topic}」的核心含义(填空):{topic}是指______。",
|
||
"options": [],
|
||
"answer": f"本课程中关于{topic}的核心概念和方法",
|
||
"explanation": f"理解{topic}的定义是进一步学习的基础。",
|
||
},
|
||
{
|
||
"type": "true_false",
|
||
"question": f"判断:「{topic}」只需要死记硬背就能掌握。",
|
||
"options": ["正确", "错误"],
|
||
"answer": "错误",
|
||
"explanation": "任何知识都需要在理解的基础上灵活运用,死记硬背无法真正掌握。",
|
||
},
|
||
]
|
||
|
||
def _topic_question_bank(self, topic, subject):
|
||
"""主题感知题库:为高频学科主题提供有实质知识的题目。
|
||
|
||
采用"最长关键词命中"策略:当多个题组都可能命中时(例如"二次函数"
|
||
同时命中"函数"和"二次函数"两组关键词),优先返回关键词最长、最精确
|
||
的题组,避免宽泛关键词(如"函数")错误抢占细分知识点(如"二次函数")。
|
||
"""
|
||
t = (topic or "")
|
||
combined = t + " " + (subject or "")
|
||
best = None
|
||
best_len = 0
|
||
for keys, qs in self._exercise_kb():
|
||
for k in keys:
|
||
if k and k in combined and len(k) > best_len:
|
||
best = qs
|
||
best_len = len(k)
|
||
return best or []
|
||
|
||
def _exercise_kb(self):
|
||
"""主题感知题库:覆盖历史/地理/生物/化学/物理/数学/语文/英语高频知识点。"""
|
||
return [
|
||
# ========== 历史 ==========
|
||
(["朝代", "历代", "王朝", "唐朝", "唐代", "汉朝", "汉代", "宋朝", "宋代", "明朝", "明代", "清朝", "清代", "秦朝"], [
|
||
{"type": "choice", "question": "我国历史上第一个统一的中央集权封建王朝是?", "options": ["夏朝", "商朝", "秦朝", "汉朝"], "answer": "秦朝", "explanation": "公元前221年,秦始皇嬴政建立秦朝,是中国历史上第一个统一的中央集权封建王朝。"},
|
||
{"type": "choice", "question": "贞观之治的皇帝是?", "options": ["唐高祖李渊", "唐太宗李世民", "唐高宗李治", "唐玄宗李隆基"], "answer": "唐太宗李世民", "explanation": "唐太宗李世民在位期间(627-649年)开创了贞观之治,是唐朝鼎盛的开端。"},
|
||
{"type": "true_false", "question": "判断:四大发明中的活字印刷术是北宋毕昇发明的。", "options": ["正确", "错误"], "answer": "正确", "explanation": "北宋庆历年间(约1045年),毕昇发明了胶泥活字印刷术,比欧洲早约400年。"},
|
||
{"type": "fill_blank", "question": "丝绸之路的起点是长安(今____),终点到达中亚、西亚乃至欧洲。", "options": [], "answer": "西安", "explanation": "汉代张骞出使西域开辟的丝绸之路,起点是西汉都城长安(今陕西西安)。"},
|
||
]),
|
||
(["丝绸之路"], [
|
||
{"type": "choice", "question": "开通陆上丝绸之路的历史人物是?", "options": ["张骞", "玄奘", "郑和", "班超"], "answer": "张骞", "explanation": "汉武帝派张骞两次出使西域,开通了连接东西方的丝绸之路。"},
|
||
{"type": "choice", "question": "下列哪项不是丝绸之路运输的商品?", "options": ["丝绸", "瓷器", "茶叶", "电视机"], "answer": "电视机", "explanation": "古代丝绸之路主要运输丝绸、瓷器、茶叶、香料等,电视机是现代工业产品。"},
|
||
{"type": "true_false", "question": "判断:郑和下西洋属于海上丝绸之路的活动。", "options": ["正确", "错误"], "answer": "正确", "explanation": "明朝郑和七次下西洋,是海上丝绸之路的壮举,比哥伦布航海早约半个世纪。"},
|
||
]),
|
||
(["工业革命"], [
|
||
{"type": "choice", "question": "第一次工业革命的标志是?", "options": ["蒸汽机的改良与广泛使用", "电力的广泛应用", "计算机的发明", "内燃机的发明"], "answer": "蒸汽机的改良与广泛使用", "explanation": "18世纪60年代,瓦特改良蒸汽机,标志着第一次工业革命(蒸汽时代)的到来。"},
|
||
{"type": "choice", "question": "第二次工业革命的核心动力是?", "options": ["蒸汽", "电力", "核能", "太阳能"], "answer": "电力", "explanation": "19世纪70年代开始的第二次工业革命以电力的广泛应用为标志,人类进入电气时代。"},
|
||
{"type": "true_false", "question": "判断:珍妮纺纱机的发明拉开了第一次工业革命的序幕。", "options": ["正确", "错误"], "answer": "正确", "explanation": "1765年哈格里夫斯发明珍妮纺纱机,是第一次工业革命开始的标志之一。"},
|
||
]),
|
||
(["抗日战争", "抗战"], [
|
||
{"type": "choice", "question": "抗日战争全面爆发的标志是?", "options": ["九一八事变", "七七事变(卢沟桥事变)", "西安事变", "八一三事变"], "answer": "七七事变(卢沟桥事变)", "explanation": "1937年7月7日的卢沟桥事变标志着中国全民族抗战的开始。"},
|
||
{"type": "choice", "question": "抗日战争中敌后战场的总指挥是?", "options": ["蒋介石", "毛泽东", "朱德", "彭德怀"], "answer": "毛泽东", "explanation": "中国共产党领导八路军、新四军开辟敌后战场,毛泽东是战略总指挥。"},
|
||
{"type": "fill_blank", "question": "抗日战争从1931年九一八事变开始,到____年日本投降结束,历时14年。", "options": [], "answer": "1945", "explanation": "1945年8月15日日本宣布无条件投降,9月2日正式签署投降书。"},
|
||
]),
|
||
(["文艺复兴"], [
|
||
{"type": "choice", "question": "文艺复兴运动最早发源于?", "options": ["法国", "英国", "意大利", "德国"], "answer": "意大利", "explanation": "14世纪文艺复兴首先在意大利的佛罗伦萨等城市兴起,随后扩展到欧洲各地。"},
|
||
{"type": "choice", "question": "文艺复兴的核心思想是?", "options": ["神权至上", "人文主义", "君权神授", "禁欲主义"], "answer": "人文主义", "explanation": "人文主义主张以人为中心,肯定人的价值与尊严,反对中世纪的神学束缚。"},
|
||
{"type": "true_false", "question": "判断:莎士比亚是文艺复兴时期英国杰出的戏剧家。", "options": ["正确", "错误"], "answer": "正确", "explanation": "莎士比亚(1564-1616)是英国文艺复兴时期最伟大的剧作家,代表作有《哈姆雷特》等。"},
|
||
]),
|
||
(["辛亥革命"], [
|
||
{"type": "choice", "question": "辛亥革命爆发于哪一年?", "options": ["1898年", "1911年", "1919年", "1921年"], "answer": "1911年", "explanation": "1911年(农历辛亥年)武昌起义爆发,标志着辛亥革命的开始。"},
|
||
{"type": "choice", "question": "辛亥革命的历史意义不包括?", "options": ["推翻了两千多年的封建君主专制", "建立了中华民国", "使中国完全摆脱半殖民地", "传播了民主共和观念"], "answer": "使中国完全摆脱半殖民地", "explanation": "辛亥革命推翻了清朝统治和封建帝制,但没有改变中国半殖民地半封建的社会性质。"},
|
||
{"type": "fill_blank", "question": "辛亥革命的领导者是____(人名),被尊称为国父。", "options": [], "answer": "孙中山", "explanation": "孙中山领导同盟会发动辛亥革命,建立中华民国,被尊称为中华民国国父。"},
|
||
]),
|
||
# ========== 地理 ==========
|
||
(["气候", "气候类型", "气温降水"], [
|
||
{"type": "choice", "question": "影响气候的基本因素不包括?", "options": ["纬度位置", "海陆位置", "地形", "学生人数"], "answer": "学生人数", "explanation": "气候主要受纬度、海陆、地形、洋流等自然因素影响,与学生人数无关。"},
|
||
{"type": "choice", "question": "我国气候的主要特征是?", "options": ["单一热带气候", "季风气候显著", "全是温带海洋性", "全是极地气候"], "answer": "季风气候显著", "explanation": "我国气候复杂多样,季风气候显著是最大特征,雨热同期有利于农业。"},
|
||
{"type": "true_false", "question": "判断:赤道附近地区终年高温多雨。", "options": ["正确", "错误"], "answer": "正确", "explanation": "赤道地区受赤道低气压带控制,全年高温多雨,属于热带雨林气候。"},
|
||
]),
|
||
(["地形", "地貌", "地势"], [
|
||
{"type": "choice", "question": "我国地形的主要特征是?", "options": ["以平原为主", "山地高原面积广大", "全是盆地", "以丘陵为主"], "answer": "山地高原面积广大", "explanation": "我国地形复杂多样,山区(含山地、高原、丘陵)面积约占全国陆地面积的三分之二。"},
|
||
{"type": "choice", "question": "下列不属于五种基本地形的是?", "options": ["平原", "高原", "盆地", "冰川"], "answer": "冰川", "explanation": "五种基本地形是山地、平原、高原、盆地、丘陵,冰川不属于基本地形类型。"},
|
||
{"type": "fill_blank", "question": "我国地势第一级阶梯与第二级阶梯的分界线是____山脉。", "options": [], "answer": "昆仑—祁连—横断", "explanation": "昆仑山—祁连山—横断山脉是一、二级阶梯分界线;大兴安岭—太行山—巫山—雪峰山是二、三级阶梯分界线。"},
|
||
]),
|
||
(["河流", "水系", "长江", "黄河"], [
|
||
{"type": "choice", "question": "我国最长的河流是?", "options": ["黄河", "长江", "珠江", "黑龙江"], "answer": "长江", "explanation": "长江全长约6300公里,是中国第一长河,世界第三长河。"},
|
||
{"type": "choice", "question": "黄河下游河床高于地面的河段称为?", "options": ["地上河", "峡谷", "三角洲", "冲积扇"], "answer": "地上河", "explanation": "黄河中游流经黄土高原携带大量泥沙,下游泥沙沉积使河床抬高形成地上河。"},
|
||
{"type": "true_false", "question": "判断:长江发源于青藏高原的唐古拉山脉。", "options": ["正确", "错误"], "answer": "正确", "explanation": "长江发源于青藏高原唐古拉山脉主峰各拉丹冬,最终注入东海。"},
|
||
]),
|
||
(["地球", "地球运动", "自转公转"], [
|
||
{"type": "choice", "question": "地球自转的方向是?", "options": ["自东向西", "自西向东", "自南向北", "自北向南"], "answer": "自西向东", "explanation": "地球自西向东自转,周期约24小时,产生昼夜交替和时间差异。"},
|
||
{"type": "choice", "question": "地球上产生四季更替的原因是?", "options": ["地球自转", "地球公转且地轴倾斜", "月球引力", "太阳活动"], "answer": "地球公转且地轴倾斜", "explanation": "地球绕太阳公转,且地轴与公转轨道面成66.5°夹角,导致太阳直射点移动,形成四季。"},
|
||
{"type": "fill_blank", "question": "地球公转一周的时间约为____天。", "options": [], "answer": "365", "explanation": "地球公转一周约365.25天,即一年,故每四年设一个闰年。"},
|
||
]),
|
||
# ========== 生物 ==========
|
||
(["细胞"], [
|
||
{"type": "choice", "question": "植物细胞特有的结构是?", "options": ["细胞膜", "细胞质", "细胞壁", "细胞核"], "answer": "细胞壁", "explanation": "细胞壁、叶绿体和大的液泡是植物细胞特有的结构。"},
|
||
{"type": "choice", "question": "细胞工厂的动力车间是?", "options": ["线粒体", "叶绿体", "核糖体", "内质网"], "answer": "线粒体", "explanation": "线粒体是细胞的动力车间,为细胞生命活动提供约95%的能量。"},
|
||
{"type": "true_false", "question": "判断:动物细胞没有细胞壁。", "options": ["正确", "错误"], "answer": "正确", "explanation": "动物细胞最外层是细胞膜,没有细胞壁、叶绿体和大的中央液泡。"},
|
||
]),
|
||
(["光合作用"], [
|
||
{"type": "choice", "question": "光合作用进行的场所是?", "options": ["线粒体", "叶绿体", "细胞核", "液泡"], "answer": "叶绿体", "explanation": "光合作用在叶绿体中进行,叶绿体含有叶绿素能吸收光能。"},
|
||
{"type": "choice", "question": "光合作用的原料不包括?", "options": ["二氧化碳", "水", "光能", "氧气"], "answer": "氧气", "explanation": "氧气是光合作用的产物而非原料。原料是二氧化碳和水,条件是光照。"},
|
||
{"type": "fill_blank", "question": "光合作用产生有机物,同时释放____。", "options": [], "answer": "氧气", "explanation": "光合作用产生有机物和氧气,维持了生物圈的碳氧平衡。"},
|
||
]),
|
||
(["生态系统"], [
|
||
{"type": "choice", "question": "生态系统中数量最多的是?", "options": ["生产者", "消费者", "分解者", "非生物部分"], "answer": "生产者", "explanation": "生产者(绿色植物)通过光合作用将无机物转化为有机物,是生态系统的基石。"},
|
||
{"type": "choice", "question": "下列食物链正确的是?", "options": ["草→兔→鹰", "阳光→草→兔", "鹰→兔→草", "兔→草"], "answer": "草→兔→鹰", "explanation": "食物链从生产者开始,箭头指向捕食者,不包括非生物部分和分解者。"},
|
||
{"type": "true_false", "question": "判断:生态系统中物质是循环的,能量是单向流动的。", "options": ["正确", "错误"], "answer": "正确", "explanation": "物质在生物与无机环境间循环,能量沿食物链单向流动、逐级递减。"},
|
||
]),
|
||
(["遗传", "基因", "DNA"], [
|
||
{"type": "choice", "question": "生物体遗传的基本单位是?", "options": ["细胞", "基因", "染色体", "蛋白质"], "answer": "基因", "explanation": "基因是控制生物性状的遗传单位,是有遗传效应的DNA片段。"},
|
||
{"type": "choice", "question": "DNA分子的空间结构是?", "options": ["单螺旋", "双螺旋", "直线形", "环形"], "answer": "双螺旋", "explanation": "1953年沃森和克里克提出DNA分子的双螺旋结构模型。"},
|
||
{"type": "true_false", "question": "判断:子代的性状完全由父方或母方一方决定。", "options": ["正确", "错误"], "answer": "错误", "explanation": "子代遗传物质来自父母双方各一半,性状由父母双方的基因共同决定。"},
|
||
]),
|
||
(["人体", "消化", "呼吸", "循环"], [
|
||
{"type": "choice", "question": "人体消化和吸收的主要器官是?", "options": ["胃", "小肠", "大肠", "口腔"], "answer": "小肠", "explanation": "小肠内有多种消化液,内壁有绒毛和微绒毛增大吸收面积,是消化吸收的主要场所。"},
|
||
{"type": "choice", "question": "人体与外界进行气体交换的器官是?", "options": ["心脏", "肺", "肝脏", "肾脏"], "answer": "肺", "explanation": "肺是呼吸系统的主要器官,肺泡是气体交换的场所。"},
|
||
{"type": "fill_blank", "question": "血液循环中,体循环的起点是____心室。", "options": [], "answer": "左", "explanation": "体循环从左心室出发,经主动脉到达全身各处,再经上下腔静脉回到右心房。"},
|
||
]),
|
||
# ========== 化学 ==========
|
||
(["元素周期表", "元素"], [
|
||
{"type": "choice", "question": "元素周期表中,原子序数等于该原子的?", "options": ["中子数", "质子数", "电子数", "质量数"], "answer": "质子数", "explanation": "原子序数在数值上等于原子的质子数(核电荷数)。"},
|
||
{"type": "choice", "question": "同一周期从左到右,元素性质的变化规律是?", "options": ["金属性增强", "非金属性增强", "原子序数减小", "无明显规律"], "answer": "非金属性增强", "explanation": "同一周期从左到右,金属性逐渐减弱,非金属性逐渐增强。"},
|
||
{"type": "true_false", "question": "判断:水是由水分子构成的,不是由氢气和氧气组成的。", "options": ["正确", "错误"], "answer": "正确", "explanation": "水(H₂O)由水分子构成,水分子由氢原子和氧原子组成,不含氢气和氧气分子。"},
|
||
]),
|
||
(["化学反应"], [
|
||
{"type": "choice", "question": "化学变化的基本特征是?", "options": ["状态改变", "产生新物质", "颜色改变", "放热"], "answer": "产生新物质", "explanation": "化学变化的本质特征是有新物质生成,其他现象(发光放热、变色等)可作为判断的辅助依据。"},
|
||
{"type": "choice", "question": "下列属于化学变化的是?", "options": ["冰熔化", "蜡烛燃烧", "石块粉碎", "水蒸发"], "answer": "蜡烛燃烧", "explanation": "蜡烛燃烧生成二氧化碳和水等新物质,是化学变化;其余为物理变化。"},
|
||
{"type": "fill_blank", "question": "化学反应遵守质量守恒定律,反应前后原子的种类和____不变。", "options": [], "answer": "数目", "explanation": "质量守恒定律:化学反应前后,原子的种类、数目、质量都不变。"},
|
||
]),
|
||
(["酸碱盐", "酸碱", "pH"], [
|
||
{"type": "choice", "question": "pH=7的溶液呈?", "options": ["酸性", "碱性", "中性", "无法判断"], "answer": "中性", "explanation": "pH<7呈酸性,pH=7呈中性,pH>7呈碱性。纯水pH约为7。"},
|
||
{"type": "choice", "question": "下列物质属于酸的是?", "options": ["NaOH", "HCl", "NaCl", "CaO"], "answer": "HCl", "explanation": "酸是电离时产生的阳离子全部是H⁺的化合物,HCl(盐酸)是常见的强酸。"},
|
||
{"type": "true_false", "question": "判断:稀盐酸和氢氧化钠溶液混合会发生中和反应,生成盐和水。", "options": ["正确", "错误"], "answer": "正确", "explanation": "HCl + NaOH = NaCl + H₂O,酸碱中和反应生成盐和水。"},
|
||
]),
|
||
(["原子", "分子", "物质构成"], [
|
||
{"type": "choice", "question": "原子的核心结构是?", "options": ["原子核和核外电子", "质子和中子", "原子核和分子", "电子和中子"], "answer": "原子核和核外电子", "explanation": "原子由带正电的原子核(质子+中子)和带负电的核外电子构成。"},
|
||
{"type": "choice", "question": "保持水的化学性质的最小粒子是?", "options": ["氢原子", "氧原子", "水分子", "质子"], "answer": "水分子", "explanation": "分子是保持物质化学性质的最小粒子,保持水的化学性质的是水分子。"},
|
||
{"type": "fill_blank", "question": "原子中,质子带正电,中子不带电,电子带____电。", "options": [], "answer": "负", "explanation": "原子中质子带正电、中子不带电、电子带负电,整个原子呈电中性。"},
|
||
]),
|
||
# ========== 物理 ==========
|
||
(["力学", "力", "牛顿"], [
|
||
{"type": "choice", "question": "牛顿第一定律的内容是?", "options": ["力是维持物体运动的原因", "物体在不受力时保持静止或匀速直线运动", "力越大加速度越大", "作用力等于反作用力"], "answer": "物体在不受力时保持静止或匀速直线运动", "explanation": "牛顿第一定律(惯性定律):一切物体在不受外力时,总保持静止状态或匀速直线运动状态。"},
|
||
{"type": "choice", "question": "下列现象中属于利用惯性的是?", "options": ["系安全带", "跳远时助跑", "限速行驶", "保持车距"], "answer": "跳远时助跑", "explanation": "跳远助跑后起跳,人由于惯性继续向前,能跳得更远。安全带、限速、保持车距是防止惯性带来的危害。"},
|
||
{"type": "true_false", "question": "判断:物体不受力时一定静止。", "options": ["正确", "错误"], "answer": "错误", "explanation": "不受力时,物体可以静止,也可以做匀速直线运动(若原来在运动)。"},
|
||
]),
|
||
(["电学", "电流", "电路", "欧姆"], [
|
||
{"type": "choice", "question": "欧姆定律的表达式是?", "options": ["I=U/R", "I=UR", "I=U+R", "I=U-R"], "answer": "I=U/R", "explanation": "欧姆定律:导体中的电流与导体两端电压成正比,与导体电阻成反比,即I=U/R。"},
|
||
{"type": "choice", "question": "一段导体两端电压为6V,电阻为3Ω,通过的电流是?", "options": ["2A", "0.5A", "18A", "9A"], "answer": "2A", "explanation": "由I=U/R=6V/3Ω=2A。"},
|
||
{"type": "fill_blank", "question": "串联电路中,电流处处____。", "options": [], "answer": "相等", "explanation": "串联电路中电流处处相等(I=I₁=I₂),电压等于各部分电压之和。"},
|
||
]),
|
||
(["光学", "光", "反射折射"], [
|
||
{"type": "choice", "question": "光在真空中的传播速度约为?", "options": ["3×10⁵ km/s", "3×10⁸ m/s", "340 m/s", "3×10⁶ m/s"], "answer": "3×10⁸ m/s", "explanation": "光在真空中的传播速度c≈3×10⁸ m/s(即30万千米/秒),是宇宙中最大的速度。"},
|
||
{"type": "choice", "question": "平面镜成像的特点是?", "options": ["像比物小", "倒立的实像", "正立等大的虚像", "像比物大"], "answer": "正立等大的虚像", "explanation": "平面镜成正立、等大、左右相反的虚像,像与物到镜面的距离相等。"},
|
||
{"type": "true_false", "question": "判断:光从空气斜射入水中时,折射角小于入射角。", "options": ["正确", "错误"], "answer": "正确", "explanation": "光从空气(光疏介质)斜射入水(光密介质)时,折射光线靠近法线,折射角小于入射角。"},
|
||
]),
|
||
(["运动", "速度", "加速度"], [
|
||
{"type": "choice", "question": "速度的国际单位是?", "options": ["km/h", "m/s", "cm/s", "km/s"], "answer": "m/s", "explanation": "速度的国际单位是米/秒(m/s),常用单位还有千米/时(km/h),1 m/s = 3.6 km/h。"},
|
||
{"type": "choice", "question": "匀速直线运动的特点是?", "options": ["速度不断增大", "速度保持不变", "方向不断改变", "加速度不为零"], "answer": "速度保持不变", "explanation": "匀速直线运动中速度大小和方向都不变,加速度为零。"},
|
||
{"type": "fill_blank", "question": "一辆汽车以20 m/s的速度行驶,5秒内通过的路程是____米。", "options": [], "answer": "100", "explanation": "路程=速度×时间=20 m/s×5s=100 m。"},
|
||
]),
|
||
# ========== 数学 ==========
|
||
(["几何", "三角形", "四边形", "多边形", "圆周", "圆"], [
|
||
{"type": "choice", "question": "三角形内角和等于多少度?", "options": ["90°", "180°", "270°", "360°"], "answer": "180°", "explanation": "任意三角形三个内角的和等于180°,这是三角形的基本性质。"},
|
||
{"type": "choice", "question": "一个三角形三个内角度数之比为1:2:3,这个三角形是?", "options": ["锐角三角形", "直角三角形", "钝角三角形", "等腰三角形"], "answer": "直角三角形", "explanation": "三个角为30°、60°、90°,最大角90°,所以是直角三角形。"},
|
||
{"type": "fill_blank", "question": "直角三角形两直角边分别为3和4,斜边长为____。", "options": [], "answer": "5", "explanation": "勾股定理:c=√(3²+4²)=√25=5。"},
|
||
{"type": "true_false", "question": "判断:圆周率π是一个无理数,约等于3.14159。", "options": ["正确", "错误"], "answer": "正确", "explanation": "π是圆周长与直径的比值,是无理数,常用的近似值为3.14。"},
|
||
{"type": "choice", "question": "n边形内角和公式是?", "options": ["(n-1)×180°", "(n-2)×180°", "n×180°", "(n+2)×180°"], "answer": "(n-2)×180°", "explanation": "n边形内角和=(n-2)×180°,如四边形内角和=(4-2)×180°=360°。"},
|
||
]),
|
||
(["函数", "一次函数", "反比例", "正比例"], [
|
||
{"type": "choice", "question": "正比例函数 y=kx (k≠0) 的图象是?", "options": ["抛物线", "过原点的直线", "双曲线", "折线"], "answer": "过原点的直线", "explanation": "正比例函数y=kx的图象是一条经过坐标原点(0,0)的直线,k>0时过一三象限。"},
|
||
{"type": "choice", "question": "一次函数 y=2x+3 的图象与y轴的交点是?", "options": ["(0,2)", "(0,3)", "(3,0)", "(2,0)"], "answer": "(0,3)", "explanation": "令x=0,则y=3,图象与y轴交于(0,3),其中3就是截距b的值。"},
|
||
{"type": "fill_blank", "question": "反比例函数 y=k/x (k≠0) 的图象是____曲线。", "options": [], "answer": "双", "explanation": "反比例函数图象是双曲线,k>0时在一、三象限,k<0时在二、四象限。"},
|
||
]),
|
||
(["方程", "一元一次", "二元一次", "不等式"], [
|
||
{"type": "choice", "question": "方程 2x+3=7 的解是?", "options": ["x=1", "x=2", "x=3", "x=5"], "answer": "x=2", "explanation": "2x+3=7,2x=4,x=2。验证:2×2+3=7 ✓"},
|
||
{"type": "choice", "question": "解二元一次方程组的基本思想是?", "options": ["画图法", "消元法", "配方", "因式分解"], "answer": "消元法", "explanation": "解二元一次方程组的基本思想是消元(代入消元或加减消元),化二元为一元。"},
|
||
{"type": "fill_blank", "question": "不等式 3x-1>5 的解集是 x>____。", "options": [], "answer": "2", "explanation": "3x-1>5,3x>6,x>2。注意不等式两边同除正数,不等号方向不变。"},
|
||
]),
|
||
(["一元二次方程", "二次方程"], [
|
||
{"type": "choice", "question": "方程 x²-5x+6=0 的两个根是?", "options": ["x=1, x=6", "x=2, x=3", "x=-2, x=-3", "x=1, x=5"], "answer": "x=2, x=3", "explanation": "(x-2)(x-3)=0,所以x₁=2,x₂=3。也可用求根公式验证。"},
|
||
{"type": "choice", "question": "一元二次方程 ax²+bx+c=0 (a≠0) 的求根公式是?", "options": ["x=(-b±√(b²-4ac))/2a", "x=(-b±√(b²+4ac))/2a", "x=(-b±√(b²-4ac))/a", "x=(-b±√(4ac-b²))/2a"], "answer": "x=(-b±(b²-4ac))/2a".replace("(b²-4ac)", "√(b²-4ac)"), "explanation": "求根公式中判别式Δ=b²-4ac,Δ>0两不等实根,Δ=0两等实根,Δ<0无实根。"},
|
||
{"type": "fill_blank", "question": "一元二次方程 x²-4=0 的解是 x=____。", "options": [], "answer": "±2", "explanation": "x²=4,所以x=2或x=-2,即x=±2。"},
|
||
]),
|
||
(["二次函数", "抛物线"], [
|
||
{"type": "choice", "question": "二次函数 y=ax²+bx+c (a≠0) 的图象是?", "options": ["直线", "抛物线", "双曲线", "圆"], "answer": "抛物线", "explanation": "二次函数的图象是抛物线,开口方向由a的符号决定。"},
|
||
{"type": "choice", "question": "抛物线 y=ax² 的开口方向取决于?", "options": ["a的大小", "a的符号", "b的值", "c的值"], "answer": "a的符号", "explanation": "a>0开口向上,a<0开口向下。|a|越大开口越小。"},
|
||
{"type": "fill_blank", "question": "二次函数 y=x²-2x-3 的对称轴是直线 x=____。", "options": [], "answer": "1", "explanation": "对称轴x=-b/(2a)=-(-2)/(2×1)=1。"},
|
||
]),
|
||
(["概率", "统计", "平均数", "中位数"], [
|
||
{"type": "choice", "question": "抛一枚均匀硬币,正面朝上的概率是?", "options": ["1", "1/2", "1/4", "1/3"], "answer": "1/2", "explanation": "硬币有两个面,正面和反面朝上机会均等,概率各为1/2。"},
|
||
{"type": "choice", "question": "一组数据2, 3, 5, 7, 8的中位数是?", "options": ["3", "5", "7", "5.5"], "answer": "5", "explanation": "数据已排序,共5个数,中位数是第3个数即5。"},
|
||
{"type": "fill_blank", "question": "一组数据1, 2, 3, 4的平均数是____。", "options": [], "answer": "2.5", "explanation": "平均数=(1+2+3+4)/4=10/4=2.5。"},
|
||
]),
|
||
# ========== 语文 ==========
|
||
(["荷塘月色"], [
|
||
{"type": "choice", "question": "《荷塘月色》的作者是?", "options": ["鲁迅", "朱自清", "巴金", "老舍"], "answer": "朱自清", "explanation": "《荷塘月色》是朱自清1927年创作的散文名篇,描绘了月下荷塘的静谧美景。"},
|
||
{"type": "choice", "question": "《荷塘月色》的主要写作手法是?", "options": ["通感修辞", "夸张", "讽刺", "象征对比"], "answer": "通感修辞", "explanation": "荷塘上的月色用通感(如仿佛远处高楼上渺茫的歌声)将视觉、听觉交融,是名篇特色。"},
|
||
{"type": "fill_blank", "question": "《荷塘月色》开头「这几天心里颇不宁静」奠定了全文____的基调。", "options": [], "answer": "淡淡的哀愁", "explanation": "心里颇不宁静反映了作者在特定时代下矛盾复杂的心境,奠定全文朦胧、淡淡的哀愁基调。"},
|
||
]),
|
||
(["古诗词", "古诗", "唐诗", "唐诗宋词"], [
|
||
{"type": "choice", "question": "「床前明月光」出自哪位诗人?", "options": ["杜甫", "李白", "白居易", "王维"], "answer": "李白", "explanation": "床前明月光,疑是地上霜,出自李白《静夜思》,表达思乡之情。"},
|
||
{"type": "choice", "question": "被誉为「诗圣」的诗人是?", "options": ["李白", "杜甫", "王维", "苏轼"], "answer": "杜甫", "explanation": "杜甫被称为诗圣,其诗被称为诗史,代表作有三吏三别。"},
|
||
{"type": "true_false", "question": "判断:宋词分为婉约派和豪放派两大流派。", "options": ["正确", "错误"], "answer": "正确", "explanation": "宋词主要分婉约派(李清照、柳永)和豪放派(苏轼、辛弃疾)两大流派。"},
|
||
]),
|
||
(["记叙文"], [
|
||
{"type": "choice", "question": "记叙文的六要素不包括?", "options": ["时间", "地点", "人物", "论据"], "answer": "论据", "explanation": "记叙文六要素:时间、地点、人物、起因、经过、结果。论据是议论文的要素。"},
|
||
{"type": "choice", "question": "记叙文中最常见的叙述方式是?", "options": ["顺叙", "倒叙", "插叙", "补叙"], "answer": "顺叙", "explanation": "顺叙是按照事件发生发展的先后顺序来写,是最基本最常见的叙述方式。"},
|
||
{"type": "fill_blank", "question": "记叙文中,把后发生的事提到前面写的叙述方式叫____。", "options": [], "answer": "倒叙", "explanation": "倒叙是把事件的结局或后面发生的事先写出来,再按顺序叙述。"},
|
||
]),
|
||
(["议论文"], [
|
||
{"type": "choice", "question": "议论文的三要素是?", "options": ["论点、论据、论证", "时间、地点、人物", "起因、经过、结果", "标题、开头、结尾"], "answer": "论点、论据、论证", "explanation": "议论文三要素:论点(观点)、论据(支撑观点的材料)、论证(用论据证明论点的过程)。"},
|
||
{"type": "choice", "question": "下列属于道理论据的是?", "options": ["统计数据", "名人名言", "历史事件", "亲身经历"], "answer": "名人名言", "explanation": "论据分事实论据(事例、数据)和道理论据(名言警句、科学原理)。名人名言属道理论据。"},
|
||
{"type": "true_false", "question": "判断:议论文的中心论点只能出现在文章开头。", "options": ["正确", "错误"], "answer": "错误", "explanation": "中心论点可在开头提出,也可在中间或结尾归纳,甚至标题即为论点。"},
|
||
]),
|
||
# ========== 英语 ==========
|
||
(["时态", "tense", "现在时", "过去时", "进行时", "完成时", "现在完成时"], [
|
||
{"type": "choice", "question": "Which sentence uses the present perfect tense?", "options": ["I eat apples.", "I ate apples.", "I have eaten apples.", "I am eating apples."], "answer": "I have eaten apples.", "explanation": "现在完成时结构为have/has+过去分词,强调过去动作对现在的影响。"},
|
||
{"type": "choice", "question": "「Yesterday」常与哪种时态连用?", "options": ["一般现在时", "一般过去时", "现在进行时", "现在完成时"], "answer": "一般过去时", "explanation": "yesterday是明确的过去时间状语,常与一般过去时连用。"},
|
||
{"type": "fill_blank", "question": "Fill: She ____ (go) to school every day. (用一般现在时)", "options": [], "answer": "goes", "explanation": "主语She是第三人称单数,一般现在时动词加-s/-es,go→goes。"},
|
||
]),
|
||
(["从句", "clause", "定语从句", "宾语从句"], [
|
||
{"type": "choice", "question": "在句子 'I know that he is a teacher.' 中,that 引导的是?", "options": ["定语从句", "宾语从句", "状语从句", "主语从句"], "answer": "宾语从句", "explanation": "that引导的从句作动词know的宾语,是宾语从句,that在宾语从句中不充当成分。"},
|
||
{"type": "choice", "question": "定语从句中,修饰人并在从句中作主语的关系代词常用?", "options": ["which", "who", "whose", "where"], "answer": "who", "explanation": "先行词指人且在从句中作主语时,关系代词用who或that(如The man who lives here is my teacher)。"},
|
||
{"type": "fill_blank", "question": "This is the book ____ I bought yesterday. (填关系代词)", "options": [], "answer": "that/which", "explanation": "先行词book指物,在从句中作bought的宾语,关系代词用that或which(可省略)。"},
|
||
]),
|
||
]
|
||
|
||
|
||
|
||
def _math_questions(self, topic):
|
||
import random
|
||
from fractions import Fraction
|
||
random.seed(hash(topic) & 0xFFFF)
|
||
qs = []
|
||
has_frac = "分数" in topic
|
||
for i in range(5):
|
||
if has_frac:
|
||
n1, d1 = random.randint(1, 5), random.randint(2, 9)
|
||
n2, d2 = random.randint(1, 5), random.randint(2, 9)
|
||
q = f"计算 {n1}/{d1} + {n2}/{d2} = ?"
|
||
ans = Fraction(n1, d1) + Fraction(n2, d2)
|
||
ans_s = f"{ans.numerator}/{ans.denominator}" if ans.denominator != 1 else str(ans.numerator)
|
||
wrong = [f"{n1+n2}/{max(d1,d2)}", f"{n1+n2}/{d1+d2}", f"{n1*n2}/{d1*d2}"]
|
||
opts = list(set([ans_s] + [w for w in wrong if w != ans_s][:3]))
|
||
while len(opts) < 4:
|
||
w = f"{random.randint(1,9)}/{random.randint(2,9)}"
|
||
if w not in opts:
|
||
opts.append(w)
|
||
random.shuffle(opts)
|
||
qs.append({
|
||
"type": "choice", "question": q,
|
||
"options": [f"{chr(65+j)}. {o}" for j, o in enumerate(opts)],
|
||
"answer": f"{chr(65+opts.index(ans_s))}. {ans_s}",
|
||
"explanation": f"同分母分数相加,分子相加分母不变;异分母先通分。{n1}/{d1}+{n2}/{d2}={ans_s}。",
|
||
})
|
||
else:
|
||
a, b = random.randint(12, 89), random.randint(12, 89)
|
||
if a < b:
|
||
a, b = b, a
|
||
ans = a + b
|
||
q = f"计算 {a} + {b} = ?"
|
||
wrong = []
|
||
while len(wrong) < 3:
|
||
w = ans + random.choice(range(-20, 21))
|
||
if w != ans and w >= 0 and w not in wrong:
|
||
wrong.append(w)
|
||
opts = [ans] + wrong
|
||
random.shuffle(opts)
|
||
qs.append({
|
||
"type": "choice", "question": q,
|
||
"options": [f"{chr(65+j)}. {o}" for j, o in enumerate(opts)],
|
||
"answer": f"{chr(65+opts.index(ans))}. {ans}",
|
||
"explanation": f"{a} + {b} = {ans}。注意进位的正确处理。",
|
||
})
|
||
return qs
|
||
|
||
def _chinese_questions(self, topic):
|
||
return [
|
||
{
|
||
"type": "choice",
|
||
"question": f"下列关于「{topic}」的表述,最准确的是?",
|
||
"options": ["需要结合具体语境理解", "只需字面意思即可", "与作者意图无关", "可以随意理解"],
|
||
"answer": "需要结合具体语境理解",
|
||
"explanation": "语文学习强调结合语境和作者意图来理解内容。",
|
||
},
|
||
{
|
||
"type": "choice",
|
||
"question": f"学习「{topic}」时,下列哪种方法最有助于深入理解?",
|
||
"options": ["反复诵读并结合语境分析", "只看一遍就能掌握", "抄写生字词就可以", "背诵答案即可"],
|
||
"answer": "反复诵读并结合语境分析",
|
||
"explanation": "多读多思是语文学习的基本方法,结合语境分析能加深理解。",
|
||
},
|
||
{
|
||
"type": "fill_blank",
|
||
"question": f"请用一句话概括「{topic}」的主要内容:______。",
|
||
"options": [],
|
||
"answer": f"本文围绕{topic}展开,表达了作者的思想感情和主要观点。",
|
||
"explanation": f"概括内容需要抓住{topic}的核心要点。",
|
||
},
|
||
{
|
||
"type": "true_false",
|
||
"question": f"判断:「{topic}」的学习只需要记住知识点就够了。",
|
||
"options": ["正确", "错误"],
|
||
"answer": "错误",
|
||
"explanation": "语文学习需要理解、感悟和运用,仅记住知识点是不够的。",
|
||
},
|
||
{
|
||
"type": "choice",
|
||
"question": f"下列哪个词语最适合用来描述「{topic}」?",
|
||
"options": ["生动形象、富有感染力", "干瘦枯燥、条理不清", "词不达意、语义不明", "重复啰嗦、缺乏新意"],
|
||
"answer": "生动形象、富有感染力",
|
||
"explanation": "词语的选择应当符合语境,生动形象的表达更有说服力。",
|
||
},
|
||
]
|
||
|
||
def _english_questions(self, topic):
|
||
return [
|
||
{
|
||
"type": "choice",
|
||
"question": 'Which sentence about "' + topic + '" is correct?',
|
||
"options": ["It is important to understand the concept in context.", "Memorization alone is sufficient.", "It has no practical application.", "It should be skipped in exams."],
|
||
"answer": "It is important to understand the concept in context.",
|
||
"explanation": "Understanding concepts in context is key to language learning.",
|
||
},
|
||
{
|
||
"type": "fill_blank",
|
||
"question": 'Fill in the blank: The key to learning "' + topic + '" is to ______ regularly.',
|
||
"options": [],
|
||
"answer": "practice",
|
||
"explanation": "Regular practice is essential for mastering any language topic.",
|
||
},
|
||
{
|
||
"type": "true_false",
|
||
"question": 'True or False: "' + topic + '" is only useful for exams.',
|
||
"options": ["True", "False"],
|
||
"answer": "False",
|
||
"explanation": "Language knowledge applies to real-world communication, not just exams.",
|
||
},
|
||
{
|
||
"type": "choice",
|
||
"question": 'What is the best way to practice "' + topic + '"?',
|
||
"options": ["Use it in sentences and conversations", "Only read about it", "Copy answers from others", "Wait until the exam"],
|
||
"answer": "Use it in sentences and conversations",
|
||
"explanation": "Active usage in context reinforces learning.",
|
||
},
|
||
{
|
||
"type": "choice",
|
||
"question": 'Which word is most closely related to "' + topic + '"?',
|
||
"options": ["comprehension", "irrelevant", "unnecessary", "optional"],
|
||
"answer": "comprehension",
|
||
"explanation": "Understanding and comprehension are central to mastering any topic.",
|
||
},
|
||
]
|
||
|
||
def _science_questions(self, topic):
|
||
return [
|
||
{
|
||
"type": "choice",
|
||
"question": f"关于「{topic}」,下列说法正确的是?",
|
||
"options": ["需要通过观察和实验来验证", "只需凭空想象", "与实际现象无关", "可以随意解释"],
|
||
"answer": "需要通过观察和实验来验证",
|
||
"explanation": "科学知识来源于观察和实验,需要实践验证。",
|
||
},
|
||
{
|
||
"type": "choice",
|
||
"question": f"学习「{topic}」最重要的方法是?",
|
||
"options": ["理解原理并动手实验", "死记公式", "背诵定义", "猜测结果"],
|
||
"answer": "理解原理并动手实验",
|
||
"explanation": "科学学习强调理论结合实践,动手实验能加深理解。",
|
||
},
|
||
{
|
||
"type": "fill_blank",
|
||
"question": f"「{topic}」的核心原理是:______。",
|
||
"options": [],
|
||
"answer": "通过观察、实验和推理总结出的规律",
|
||
"explanation": "科学原理是对自然现象规律的总结。",
|
||
},
|
||
{
|
||
"type": "true_false",
|
||
"question": f"判断:「{topic}」可以通过实验来验证。",
|
||
"options": ["正确", "错误"],
|
||
"answer": "正确",
|
||
"explanation": "科学理论可以通过实验来验证其正确性。",
|
||
},
|
||
{
|
||
"type": "choice",
|
||
"question": f"下列哪项属于科学探究「{topic}」的基本步骤?",
|
||
"options": ["提出问题→假设→实验→结论", "直接给出结论", "只看书本", "把问题留给老师"],
|
||
"answer": "提出问题→假设→实验→结论",
|
||
"explanation": "科学探究遵循问题→假设→实验→结论的基本流程。",
|
||
},
|
||
]
|
||
|
||
def _exercise_html(self, q):
|
||
qtype = q.get("type", "choice")
|
||
stem = (q.get("question") or "").replace("'", "\'")
|
||
opts = q.get("options") or []
|
||
answer = (q.get("answer") or "").replace("'", "\'")
|
||
if qtype in ("choice", "true_false") and opts:
|
||
btns = ""
|
||
for o in opts:
|
||
safe = o.replace("'", "\'")
|
||
btns += "<button class='eo' data-a='" + safe + "' onclick='ep(this)'>" + o + "</button>"
|
||
return (
|
||
"<style>.eq{width:100%;height:100%;box-sizing:border-box;padding:32px 40px;background:#f0fdf4;font-family:'Microsoft YaHei',sans-serif;overflow-y:auto}"
|
||
".eq h3{color:#166534;margin:0 0 16px;font-size:20px;line-height:1.6}"
|
||
".eo{display:block;width:100%;margin:8px 0;padding:14px 18px;border:2px solid #bbf7d0;border-radius:10px;background:#fff;font-size:17px;cursor:pointer;transition:.2s;text-align:left}"
|
||
".eo:hover{border-color:#22c55e}.eo.ok{background:#dcfce7;border-color:#16a34a}.eo.bad{background:#fef2f2;border-color:#ef4444}</style>"
|
||
"<div class='eq'><h3>" + stem + "</h3>" + btns
|
||
+ "<input type='hidden' id='ea' value='" + answer + "'></div>"
|
||
+ "<script>function ep(b){var a=document.getElementById('ea').value;"
|
||
"document.querySelectorAll('.eo').forEach(function(e){e.classList.remove('ok','bad')});"
|
||
"if(b.getAttribute('data-a')==a||b.textContent==a){b.classList.add('ok')}else{b.classList.add('bad');"
|
||
"document.querySelectorAll('.eo').forEach(function(e){if(e.getAttribute('data-a')==a||e.textContent==a)e.classList.add('ok')})}}"
|
||
+ "</script>"
|
||
)
|
||
if qtype == "fill_blank":
|
||
return (
|
||
"<style>.eq{width:100%;height:100%;box-sizing:border-box;padding:32px 40px;background:#f0fdf4;font-family:'Microsoft YaHei',sans-serif;display:flex;flex-direction:column;justify-content:center}"
|
||
".eq h3{color:#166534;margin:0 0 20px;font-size:20px;line-height:1.6}"
|
||
".ei{padding:14px 18px;border:2px solid #bbf7d0;border-radius:10px;font-size:18px;width:80%;margin-bottom:16px}"
|
||
".eb{padding:12px 32px;background:#16a34a;color:#fff;border:none;border-radius:10px;font-size:17px;cursor:pointer}"
|
||
".ef{margin-top:16px;font-size:15px;display:none}</style>"
|
||
"<div class='eq'><h3>" + stem + "</h3>"
|
||
+ "<input class='ei' id='fu' placeholder='请输入答案'>"
|
||
+ "<button class='eb' id='fb'>提交</button>"
|
||
+ "<p class='ef' id='fr'></p></div>"
|
||
+ "<script>(function(){var ans='" + answer.replace("'", "") + "';"
|
||
"document.getElementById('fb').addEventListener('click',function(){"
|
||
"var u=document.getElementById('fu').value.trim().toLowerCase();"
|
||
"var a=ans.toLowerCase();var f=document.getElementById('fr');"
|
||
"f.style.display='block';"
|
||
"if(u.indexOf(a)>=0||a.indexOf(u)>=0){f.style.color='#16a34a';f.textContent='✓ 回答正确!'}"
|
||
"else{f.style.color='#ef4444';f.textContent='✗ 正确答案:'+ans}})})();</script>"
|
||
)
|
||
return "<div class='eq' style='width:100%;height:100%;padding:32px;background:#f0fdf4;font-family:sans-serif'><h3 style='color:#166534'>" + stem + "</h3></div>"
|
||
|
||
def _fallback_lesson_plan(self, title, subject, grade, objectives, duration):
|
||
t = title or "大单元教案"
|
||
subj = (subject or "").strip()
|
||
prof = self._lesson_plan_profile(subj)
|
||
new_dur = max(10, duration - 20)
|
||
practice_dur = max(8, duration // 4)
|
||
return {
|
||
"title": t,
|
||
"objectives": objectives or prof["objectives"](t),
|
||
"key_points": prof["key_points"](t),
|
||
"difficulties": prof["difficulties"](t),
|
||
"phases": [
|
||
{"name": "导入", "duration": 5, "activities": [prof["intro"](t)], "teacher_actions": ["创设情境,激发兴趣。", "提出核心问题。"], "student_actions": ["观察思考。", "联系已有经验。"], "resources": prof["intro_resources"]},
|
||
{"name": "新授", "duration": new_dur, "activities": prof["new_activities"](t), "teacher_actions": prof["new_teacher"], "student_actions": prof["new_student"], "resources": prof["new_resources"]},
|
||
{"name": "练习巩固", "duration": practice_dur, "activities": prof["practice"](t), "teacher_actions": ["巡视指导,个别辅导。"], "student_actions": prof["practice_student"], "resources": prof["practice_resources"]},
|
||
{"name": "总结", "duration": 5, "activities": [prof["summary"](t)], "teacher_actions": ["梳理知识脉络,提炼方法。"], "student_actions": ["回顾反思,记录收获。"], "resources": ["板书", "思维导图"]},
|
||
],
|
||
"homework": prof["homework"](t),
|
||
"reflection": f"根据课堂反馈与学生掌握情况,调整「{t}」后续教学的深度、广度与练习分层策略。",
|
||
}
|
||
|
||
def _lesson_plan_profile(self, subject: str) -> dict:
|
||
s = subject
|
||
profiles = {
|
||
"数学": {
|
||
"objectives": lambda t: [f"理解「{t}」的概念内涵与几何或代数意义", f"掌握「{t}」的基本公式与运算法则", "能运用所学知识分析并解决实际问题"],
|
||
"key_points": lambda t: [f"「{t}」的定义与核心性质", "公式推导与规范书写"],
|
||
"difficulties": lambda t: [f"「{t}」的灵活运用与变形", "数形结合思想的建立"],
|
||
"intro": lambda t: f"借助生活实例或已有知识,引出「{t}」的研究必要性。",
|
||
"new_activities": lambda t: [f"通过探究活动归纳「{t}」的概念与性质。", "推导公式并分析适用条件。"],
|
||
"new_teacher": ["引导探究,板书推导过程。", "组织小组讨论。"],
|
||
"new_student": ["动手操作或画图探究。", "小组合作归纳结论。"],
|
||
"intro_resources": ["课件", "实物教具", "几何画板"],
|
||
"new_resources": ["学案", "探究任务单", "动态演示"],
|
||
"practice": lambda t: ["基础题巩固概念。", "变式训练提升能力。", "实际问题应用。"],
|
||
"practice_student": ["独立完成,规范步骤。", "同桌互批,错题订正。"],
|
||
"practice_resources": ["分层练习", "答题卡"],
|
||
"summary": lambda t: f"梳理「{t}」的知识结构图,提炼解题方法与易错点。",
|
||
"homework": lambda t: [f"完成「{t}」分层作业(基础+提升)。", "整理本节易错题集。"],
|
||
},
|
||
"物理": {
|
||
"objectives": lambda t: [f"理解「{t}」的物理意义与适用条件", "掌握相关公式并能进行简单计算", "经历实验探究过程,培养科学思维"],
|
||
"key_points": lambda t: [f"「{t}」的概念建立与公式表达", "实验现象的观察与分析"],
|
||
"difficulties": lambda t: [f"「{t}」物理模型的构建", "实验方案的设计与数据分析"],
|
||
"intro": lambda t: f"通过演示实验或生活现象,引发对「{t}」的思考。",
|
||
"new_activities": lambda t: ["分组实验探究规律。", "分析实验数据,归纳结论。"],
|
||
"new_teacher": ["演示关键实验,强调安全。", "指导小组实验,引导分析。"],
|
||
"new_student": ["分组实验,记录数据。", "分析数据,得出结论。"],
|
||
"intro_resources": ["演示器材", "课件", "视频"],
|
||
"new_resources": ["学生实验器材", "数据记录表"],
|
||
"practice": lambda t: ["基础概念辨析。", "公式应用计算。", "实验数据分析题。"],
|
||
"practice_student": ["独立解题,规范作图。", "小组互查实验报告。"],
|
||
"practice_resources": ["练习卷", "实验报告单"],
|
||
"summary": lambda t: f"构建「{t}」的知识网络,强调实验方法与物理思想。",
|
||
"homework": lambda t: [f"完成「{t}」课后练习。", "撰写实验探究报告。"],
|
||
},
|
||
}
|
||
# 化学/生物共用理科探究模板
|
||
sci = {
|
||
"objectives": lambda t: [f"理解「{t}」的基本概念与原理", "掌握核心知识并能解释相关现象", "培养实验观察与科学探究能力"],
|
||
"key_points": lambda t: [f"「{t}」的核心概念与特征", "实验现象的观察与解释"],
|
||
"difficulties": lambda t: [f"「{t}」微观机制的理解", "知识在真实情境中的迁移应用"],
|
||
"intro": lambda t: f"通过实验现象或生活实例,引出「{t}」的探究问题。",
|
||
"new_activities": lambda t: ["实验观察与操作。", "小组讨论归纳知识要点。"],
|
||
"new_teacher": ["组织实验,强调规范操作。", "引导分析现象背后的原理。"],
|
||
"new_student": ["动手实验,如实记录。", "讨论交流,形成结论。"],
|
||
"intro_resources": ["实验器材", "课件", "标本/模型"],
|
||
"new_resources": ["实验材料", "学习单"],
|
||
"practice": lambda t: ["概念辨析与判断。", "现象解释与应用。"],
|
||
"practice_student": ["独立完成练习。", "小组互评实验记录。"],
|
||
"practice_resources": ["练习卡", "实验记录册"],
|
||
"summary": lambda t: f"梳理「{t}」的知识体系,强调科学方法与探究思路。",
|
||
"homework": lambda t: [f"完成「{t}」巩固练习。", "预习下一节内容并完成预习单。"],
|
||
}
|
||
for k in ("化学", "生物"):
|
||
profiles[k] = sci
|
||
profiles["语文"] = {
|
||
"objectives": lambda t: [f"品味「{t}」的语言特色与表达技巧", "理解文章主旨与作者情感", "学习并运用相关的写作方法"],
|
||
"key_points": lambda t: [f"「{t}」的文本内容与结构梳理", "关键语句的赏析与理解"],
|
||
"difficulties": lambda t: [f"「{t}」深层意蕴与作者情感的体悟", "写作手法在表达中的迁移运用"],
|
||
"intro": lambda t: f"借助背景介绍或朗读导入,唤起对「{t}」的阅读期待。",
|
||
"new_activities": lambda t: ["初读感知,整体把握文意。", "精读品味,赏析关键语段。"],
|
||
"new_teacher": ["范读指导,组织品析。", "点拨赏析方法。"],
|
||
"new_student": ["朗读体会,圈点批注。", "小组交流赏析心得。"],
|
||
"intro_resources": ["课件", "背景资料", "音频朗读"],
|
||
"new_resources": ["文本", "批注学习单"],
|
||
"practice": lambda t: ["仿写练习。", "片段赏析与表达。"],
|
||
"practice_student": ["独立完成仿写。", "分享交流,互评互改。"],
|
||
"practice_resources": ["仿写学习单", "评价量表"],
|
||
"summary": lambda t: f"总结「{t}」的写法与主旨,提炼可借鉴的表达技巧。",
|
||
"homework": lambda t: [f"围绕「{t}」完成读写结合小练笔。", "积累本文好词好句。"],
|
||
}
|
||
profiles["英语"] = {
|
||
"objectives": lambda t: [f"掌握「{t}」相关核心词汇与句型", "能在真实情境中运用所学进行交流", "提升听、说、读、写综合语言能力"],
|
||
"key_points": lambda t: [f"「{t}」的核心词汇与目标句型", "语言功能的得体运用"],
|
||
"difficulties": lambda t: [f"「{t}」相关语法结构的正确使用", "在真实交际中的灵活表达"],
|
||
"intro": lambda t: f"通过歌曲、游戏或情境对话导入「{t}」话题。",
|
||
"new_activities": lambda t: ["词汇与句型学习。", "情境对话操练与角色扮演。"],
|
||
"new_teacher": ["创设情境,示范句型。", "组织pair work与group work。"],
|
||
"new_student": ["跟读模仿,记忆词汇。", "结对操练,大胆表达。"],
|
||
"intro_resources": ["课件", "音频视频", "单词卡"],
|
||
"new_resources": ["对话任务单", "角色卡片"],
|
||
"practice": lambda t: ["词汇句型巩固。", "情境交际任务。"],
|
||
"practice_student": ["完成听力与填空。", "小组表演对话。"],
|
||
"practice_resources": ["练习单", "评价表"],
|
||
"summary": lambda t: f"回顾「{t}」核心语言知识,梳理交际策略。",
|
||
"homework": lambda t: [f"完成「{t}」听读作业。", "用目标句型写一段对话。"],
|
||
}
|
||
profiles["历史"] = {
|
||
"objectives": lambda t: [f"了解「{t}」的基本史实与发展脉络", "分析历史事件的因果关系与影响", "培养历史思维与家国情怀"],
|
||
"key_points": lambda t: [f"「{t}」的关键事件、人物与时间", "历史发展规律与时代特征"],
|
||
"difficulties": lambda t: [f"对「{t}」历史现象的多角度分析", "史论结合,以史为鉴"],
|
||
"intro": lambda t: f"通过史料、图片或视频导入「{t}」的时代背景。",
|
||
"new_activities": lambda t: ["梳理时间线与重大事件。", "研读史料,分析因果关系。"],
|
||
"new_teacher": ["提供史料,引导分析。", "组织讨论,启发思考。"],
|
||
"new_student": ["阅读史料,做时间轴。", "小组讨论,发表见解。"],
|
||
"intro_resources": ["课件", "历史地图", "史料摘录"],
|
||
"new_resources": ["史料学习单", "时间轴模板"],
|
||
"practice": lambda t: ["史实梳理填空。", "材料分析题。"],
|
||
"practice_student": ["独立完成基础题。", "小组研讨材料题。"],
|
||
"practice_resources": ["练习卷", "材料研读单"],
|
||
"summary": lambda t: f"构建「{t}」的知识框架,总结历史启示。",
|
||
"homework": lambda t: [f"完成「{t}」巩固练习。", "撰写一段历史小短评。"],
|
||
}
|
||
profiles["地理"] = {
|
||
"objectives": lambda t: [f"认识「{t}」的空间分布与基本特征", "理解各地理要素的相互关系", "树立人地协调与可持续发展观念"],
|
||
"key_points": lambda t: [f"「{t}」的位置、分布与主要特征", "自然与人文地理要素的相互作用"],
|
||
"difficulties": lambda t: [f"「{t}」空间格局的综合分析", "读图分析与地理推理"],
|
||
"intro": lambda t: f"借助地图、卫星图或景观图导入「{t}」。",
|
||
"new_activities": lambda t: ["读图分析空间分布。", "小组探究地理要素关系。"],
|
||
"new_teacher": ["指导读图方法。", "组织探究活动。"],
|
||
"new_student": ["读图标注,提取信息。", "小组合作,归纳特征。"],
|
||
"intro_resources": ["课件", "地图", "景观图片"],
|
||
"new_resources": ["空白地图", "探究任务单"],
|
||
"practice": lambda t: ["读图填图训练。", "综合分析题。"],
|
||
"practice_student": ["独立完成读图题。", "小组研讨分析题。"],
|
||
"practice_resources": ["练习图册", "分析学习单"],
|
||
"summary": lambda t: f"整理「{t}」知识结构,强调人地关系与可持续发展。",
|
||
"homework": lambda t: [f"完成「{t}」读图与练习。", "调查身边的地理现象。"],
|
||
}
|
||
if s in profiles:
|
||
return profiles[s]
|
||
# 通用模板
|
||
return {
|
||
"objectives": lambda t: [f"理解「{t}」的核心内容与基本要求", "掌握关键知识与基本方法", "培养分析思维与合作探究能力"],
|
||
"key_points": lambda t: [f"「{t}」的核心知识建构", "方法与技能的训练"],
|
||
"difficulties": lambda t: [f"「{t}」的深入理解与灵活运用", "知识的迁移与综合"],
|
||
"intro": lambda t: f"创设情境,引出「{t}」的学习主题。",
|
||
"new_activities": lambda t: [f"围绕「{t}」进行讲解与互动探究。"],
|
||
"new_teacher": ["讲解引导,组织探究。"],
|
||
"new_student": ["主动思考,合作探究。"],
|
||
"intro_resources": ["课件", "情境素材"],
|
||
"new_resources": ["学习单", "探究材料"],
|
||
"practice": lambda t: ["分层练习巩固。", "互评与订正。"],
|
||
"practice_student": ["独立完成,规范作答。"],
|
||
"practice_resources": ["练习卡"],
|
||
"summary": lambda t: f"梳理「{t}」知识结构,提炼重点方法。",
|
||
"homework": lambda t: [f"完成「{t}」巩固作业。", "预习下节内容。"],
|
||
}
|
||
|
||
def _fallback_essay_grade(self, essay_text, total_score):
|
||
text = essay_text or ""
|
||
char_count = len(text)
|
||
import re as _re2
|
||
sentences = [s.strip() for s in _re2.split(r"[。!?]", text) if s.strip()]
|
||
sent_count = len(sentences)
|
||
has_dialog = "“" in text or "说" in text
|
||
has_detail = any(w in text for w in ("有一次", "记得", "那天", "当时", "忽然", "只见"))
|
||
avg_sent = char_count / max(sent_count, 1)
|
||
score_ratio = 0.82 if avg_sent > 25 else 0.74
|
||
score = max(1, int(total_score * score_ratio))
|
||
annotations = []
|
||
if has_detail:
|
||
for kw in ("有一次", "记得", "那天", "当时"):
|
||
pos = text.find(kw)
|
||
if pos >= 0:
|
||
snippet = text[max(0,pos-3):pos+12]
|
||
annotations.append({"text": snippet, "comment": "这里用具体事例展开,增加了文章的真实感。", "type": "good"})
|
||
break
|
||
if char_count < 100:
|
||
annotations.append({"text": text[:20], "comment": "文章字数偏少,建议增加具体描写。", "type": "error"})
|
||
strengths = []
|
||
weaknesses = []
|
||
suggestions = []
|
||
if has_detail:
|
||
strengths.append("能用具体事例展开叙述,增加了文章的感染力。")
|
||
else:
|
||
weaknesses.append("缺乏具体事例和细节描写,内容较空洞。")
|
||
suggestions.append("增加一到两个具体事例,展开细节描写。")
|
||
if has_dialog:
|
||
strengths.append("运用了对话描写,使人物形象更加鲜活。")
|
||
else:
|
||
suggestions.append("可以加入对话描写,让人物更鲜活。")
|
||
if sent_count <= 2:
|
||
weaknesses.append("句子偏少,结构较单谬。")
|
||
suggestions.append("增加句子数量,丰富文章层次。")
|
||
if avg_sent > 40:
|
||
weaknesses.append("部分句子较长,建议适当断句。")
|
||
suggestions.append("将过长的句子拆分为短句,提高可读性。")
|
||
content_ratio = 0.32 if has_detail else 0.25
|
||
content_s = int(total_score * content_ratio)
|
||
structure_s = int(total_score * 0.22)
|
||
language_s = int(total_score * 0.18)
|
||
writing_s = score - content_s - structure_s - language_s
|
||
if not strengths:
|
||
strengths = ["主题明确,有基本的表达。"]
|
||
if not weaknesses:
|
||
weaknesses = ["可以在细节上进一步丰富。"]
|
||
if not suggestions:
|
||
suggestions = ["继续保持认真观察和真诚表达。"]
|
||
level = "优秀" if score_ratio > 0.80 else ("良好" if score_ratio > 0.72 else "中等")
|
||
return {
|
||
"total_score": score,
|
||
"scores": {
|
||
"content": {"score": content_s, "max": int(total_score * 0.3), "comment": "内容较为充实。" if has_detail else "内容基本完整,建议增加细节。"},
|
||
"structure": {"score": structure_s, "max": int(total_score * 0.25), "comment": "结构较清晰。" if sent_count > 2 else "结构较为单谬,建议增加层次。"},
|
||
"language": {"score": language_s, "max": int(total_score * 0.25), "comment": "表达较通顺。" if avg_sent <= 40 else "部分句子较长,可适当断句。"},
|
||
"writing": {"score": writing_s, "max": int(total_score * 0.2), "comment": "书写规范尚可。"},
|
||
},
|
||
"overall_comment": "作文围绕主题展开," + ("能用具体事例进行叙述," if has_detail else "但缺乏具体细节,") + "表达有一定的真实感。" + ("建议继续加强细节描写和语言表现力,让文章更有感染力。" if not has_detail else "建议在语言精炼和结构层次上进一步提升。"),
|
||
"strengths": strengths,
|
||
"weaknesses": weaknesses,
|
||
"suggestions": suggestions,
|
||
"annotations": annotations,
|
||
"level": level,
|
||
}
|
||
|
||
def _fallback_exam(self, subject, grade, knowledge_points, difficulty, question_types, count, total_score):
|
||
total = max(1, min(count or 10, 20))
|
||
each = max(1, total_score // max(total, 1))
|
||
subj = subject or "综合"
|
||
kp = "、".join(knowledge_points) if knowledge_points else "基础知识"
|
||
bank = self._question_bank(kp, subj)
|
||
if not bank:
|
||
bank = self._generic_question_bank(kp, subj)
|
||
questions = []
|
||
for idx in range(total):
|
||
q = dict(bank[idx % len(bank)])
|
||
q["id"] = idx + 1
|
||
q["type"] = q.get("type", (question_types or ["choice"])[0])
|
||
q["score"] = each
|
||
q["difficulty"] = difficulty if isinstance(difficulty, str) else ["easy", "medium", "hard"][idx % 3]
|
||
q["content"] = q.pop("question", None) or q.get("content", "")
|
||
q["analysis"] = q.get("explanation", q.get("analysis", ""))
|
||
if "explanation" in q:
|
||
q.pop("explanation", None)
|
||
if "html" in q:
|
||
q.pop("html", None)
|
||
questions.append(q)
|
||
return {"title": f"{subj}{grade or ''}智能试卷", "questions": questions}
|
||
|
||
def _courseware_category(self, prompt: str) -> str:
|
||
prompt = self._repair_text(prompt)
|
||
if any(k in prompt for k in ["圆柱", "圆柱的体积", "体积公式", "底面积", "高"]):
|
||
return "cylinder"
|
||
return "general"
|
||
|
||
def _courseware_shell(self, title: str, subtitle: str, body: str, footer_left: str = "") -> str:
|
||
title_html = html_lib.escape(title)
|
||
subtitle_html = html_lib.escape(subtitle)
|
||
footer_html = html_lib.escape(footer_left)
|
||
return f"""<div style='width:100%;height:100%;position:relative;overflow:hidden;background:#f6fbf7;font-family:Microsoft YaHei,PingFang SC,sans-serif;color:#143b31;'>
|
||
<style>
|
||
.zj-top{{position:absolute;left:36px;right:36px;top:24px;height:64px;display:flex;align-items:center;justify-content:space-between;border-bottom:1px solid #d9e7dc;}}
|
||
.zj-top h2{{margin:0 0 6px;font-size:26px;color:#00543d;}}
|
||
.zj-top p{{margin:0;color:#6b7f76;font-size:14px;}}
|
||
.zj-badge{{padding:8px 14px;border-radius:18px;background:#e8f7ef;color:#007a52;font-size:13px;font-weight:700;}}
|
||
.zj-stage{{position:absolute;left:48px;right:48px;top:110px;bottom:64px;background:#fff;border:1px solid #dce8de;border-radius:16px;box-shadow:0 12px 30px rgba(15,84,61,.08);overflow:hidden;}}
|
||
.zj-footer{{position:absolute;left:36px;right:36px;bottom:20px;display:flex;align-items:center;justify-content:space-between;color:#6b7f76;font-size:13px;}}
|
||
.zj-chip{{display:inline-flex;align-items:center;gap:6px;padding:10px 14px;border-radius:999px;border:1px solid #cfe0d3;background:#fff;color:#00543d;font-size:13px;cursor:pointer;}}
|
||
.zj-card{{background:#fff;border:1px solid #dce8de;border-radius:14px;box-shadow:0 10px 24px rgba(15,84,61,.06);}}
|
||
.zj-btn{{border:none;border-radius:12px;padding:10px 16px;background:#00543d;color:#fff;cursor:pointer;font-size:14px;}}
|
||
.zj-btn.alt{{background:#fff;color:#00543d;border:1px solid #cfe0d3;}}
|
||
.zj-pill{{padding:6px 12px;border-radius:999px;background:#ecfdf5;color:#00543d;font-size:12px;}}
|
||
.zj-grid2{{display:grid;grid-template-columns:1fr 1fr;gap:16px;}}
|
||
.zj-grid3{{display:grid;grid-template-columns:repeat(3,1fr);gap:12px;}}
|
||
</style>
|
||
<div class='zj-top'>
|
||
<div><h2>{title_html}</h2><p>{subtitle_html}</p></div>
|
||
<div class='zj-badge'>AI生成</div>
|
||
</div>
|
||
<div class='zj-stage'>{body}</div>
|
||
<div class='zj-footer'><div>{footer_html}</div><div>页面内可点击交互</div></div>
|
||
</div>"""
|
||
|
||
def _fallback_courseware(self, prompt: str, subject: str, grade: str, page_count: int) -> dict:
|
||
prompt = self._repair_text(prompt)
|
||
if self._courseware_category(prompt) == "cylinder":
|
||
return self._fallback_cylinder_courseware(prompt, subject, grade, page_count)
|
||
return self._fallback_general_courseware(prompt, subject, grade, page_count)
|
||
|
||
def _normalize_courseware_result(self, result: dict, prompt: str, subject: str, grade: str, page_count: int, aspect_ratio: str) -> dict | None:
|
||
if not isinstance(result, dict):
|
||
return None
|
||
pages = result.get("pages")
|
||
if not isinstance(pages, list) or len(pages) < 5:
|
||
return None
|
||
normalized_pages = []
|
||
for page in pages:
|
||
if not isinstance(page, dict):
|
||
continue
|
||
content = page.get("content")
|
||
if not isinstance(content, str):
|
||
continue
|
||
cleaned = content.replace("100vh", "100%").replace("100vw", "100%")
|
||
if not cleaned.strip().lower().startswith("<div"):
|
||
cleaned = f"<div style='width:100%;height:100%;position:relative;overflow:hidden;'>{cleaned}</div>"
|
||
normalized_pages.append({
|
||
"type": page.get("type") or "content",
|
||
"title": self._repair_text(str(page.get("title") or "")),
|
||
"content": cleaned,
|
||
"notes": self._repair_text(str(page.get("notes") or "")),
|
||
})
|
||
if len(normalized_pages) < 5:
|
||
return None
|
||
return {
|
||
"title": self._repair_text(str(result.get("title") or prompt[:24] or "课件")),
|
||
"summary": self._repair_text(str(result.get("summary") or "")),
|
||
"tags": [self._repair_text(str(tag)) for tag in (result.get("tags") or []) if str(tag).strip()],
|
||
"pages": normalized_pages[:max(5, min(page_count, len(normalized_pages)))],
|
||
"aspect_ratio": aspect_ratio,
|
||
"subject": subject,
|
||
"grade": grade,
|
||
}
|
||
|
||
def _courseware_topic(self, prompt: str) -> str:
|
||
prompt = self._repair_text(prompt)
|
||
text = prompt.lower()
|
||
if any(k in prompt for k in ["地球公转", "四季", "春分", "夏至", "秋分", "冬至", "昼夜长短", "太阳直射"]) or any(k in text for k in ["orbit", "season"]):
|
||
return "orbit"
|
||
if any(k in prompt for k in ["圆柱", "体积", "底面积", "圆锥", "长方体"]):
|
||
return "cylinder"
|
||
return "general"
|
||
|
||
def _courseware_shell_clean(self, title: str, subtitle: str, body: str, footer_left: str = "") -> str:
|
||
title_html = html_lib.escape(title)
|
||
subtitle_html = html_lib.escape(subtitle)
|
||
footer_html = html_lib.escape(footer_left)
|
||
return (
|
||
"<div style='width:100%;height:100%;position:relative;overflow:hidden;background:#f6fbf7;font-family:Microsoft YaHei,PingFang SC,sans-serif;color:#143b31;'>"
|
||
"<style>"
|
||
".zj-top{position:absolute;left:28px;right:28px;top:16px;height:42px;display:flex;align-items:center;justify-content:space-between;border-bottom:1px solid #c9dacc;}"
|
||
".zj-top h2{margin:0;font-size:21px;line-height:1.15;color:#004737;font-weight:700;}"
|
||
".zj-top p{margin:3px 0 0;color:#617971;font-size:12px;}"
|
||
".zj-badge{width:78px;height:28px;border-radius:999px;border:1px solid #c9dacc;background:#fff;color:#004737;font-size:12px;font-weight:700;display:flex;align-items:center;justify-content:center;}"
|
||
".zj-stage{position:absolute;left:20px;right:20px;top:70px;bottom:20px;background:#fff;border:1px solid #c9dacc;border-radius:16px;box-shadow:0 16px 34px rgba(0,71,55,.08);overflow:auto;}"
|
||
".zj-footer{display:none;}"
|
||
".zj-chip{display:inline-flex;align-items:center;gap:6px;padding:10px 14px;border-radius:999px;border:1px solid #cfe0d3;background:#fff;color:#00543d;font-size:13px;cursor:pointer;}"
|
||
".zj-card{background:#fff;border:1px solid #dce8de;border-radius:14px;box-shadow:0 10px 24px rgba(15,84,61,.06);}"
|
||
".zj-btn{border:none;border-radius:12px;padding:10px 16px;background:#00543d;color:#fff;cursor:pointer;font-size:14px;}"
|
||
".zj-btn.alt{background:#fff;color:#00543d;border:1px solid #cfe0d3;}"
|
||
".zj-pill{padding:6px 12px;border-radius:999px;background:#ecfdf5;color:#00543d;font-size:12px;}"
|
||
".zj-grid2{display:grid;grid-template-columns:1fr 1fr;gap:16px;}"
|
||
".zj-grid3{display:grid;grid-template-columns:repeat(3,1fr);gap:12px;}"
|
||
".season-btn{height:46px;border-radius:13px;border:1px solid #d9e7dc;background:#fff;color:#516b61;display:flex;align-items:center;justify-content:center;gap:6px;font-weight:700;cursor:pointer;transition:all .22s ease;box-shadow:0 4px 12px rgba(15,84,61,.05);user-select:none;}"
|
||
".season-btn.active{border-color:#00a870;background:#ecfdf5;color:#00543d;box-shadow:0 10px 18px rgba(0,168,112,.10);}"
|
||
".orbit-wrap{position:relative;height:100%;min-height:300px;background:linear-gradient(180deg,#f8fefb,#eef9f1);border-radius:16px;border:1px solid #dce8de;overflow:hidden;}"
|
||
".orbit-sun{position:absolute;left:52px;top:50%;transform:translateY(-50%);width:118px;height:118px;border-radius:50%;background:radial-gradient(circle at 35% 35%,#fff8d6 0,#fde68a 46%,#f59e0b 100%);box-shadow:0 0 0 12px rgba(245,158,11,.08),0 18px 40px rgba(245,158,11,.18);}"
|
||
".orbit-path{position:absolute;left:160px;right:44px;top:38px;bottom:38px;border:2px dashed #9dd9c0;border-radius:50%;}"
|
||
".earth-dot{position:absolute;right:104px;top:50%;transform:translateY(-50%);width:94px;height:94px;border-radius:50%;background:radial-gradient(circle at 34% 34%,#a7f3d0 0,#34d399 58%,#0f766e 100%);box-shadow:0 18px 32px rgba(15,118,110,.18);}"
|
||
".earth-dot::after{content:'';position:absolute;inset:0;border-radius:50%;background:linear-gradient(90deg,rgba(255,255,255,.42),transparent 55%,rgba(0,0,0,.12));animation:spin 5s linear infinite;}"
|
||
"@keyframes spin{from{transform:rotate(0deg);}to{transform:rotate(360deg);}}"
|
||
"</style>"
|
||
"<script>(function(){function setActive(group,key){if(!group)return;group.querySelectorAll('[data-switch]').forEach(function(btn){btn.classList.toggle('active',btn.getAttribute('data-switch')===key)});group.querySelectorAll('[data-panel]').forEach(function(panel){panel.style.display=panel.getAttribute('data-panel')===key?'':'none'});}document.addEventListener('click',function(e){var tab=e.target.closest&&e.target.closest('[data-switch]');if(tab){var group=tab.closest('[data-tab-group]');setActive(group,tab.getAttribute('data-switch'));return;}var toggle=e.target.closest&&e.target.closest('[data-toggle]');if(toggle){var target=toggle.getAttribute('data-toggle');var host=toggle.closest('[data-toggle-group]')||toggle.parentElement||document;var panel=host.querySelector('[data-target=\"'+target+'\"]');if(panel){panel.style.display=panel.style.display==='none'?'':'none';toggle.classList.toggle('active');}return;}});})();</script>"
|
||
"<div class='zj-top'><div><h2>" + title_html + "</h2><p>" + subtitle_html + "</p></div><div class='zj-badge'>AI生成</div></div>"
|
||
"<div class='zj-stage'>"
|
||
+ body +
|
||
"</div>"
|
||
"<div class='zj-footer'><div>" + footer_html + "</div><div>页面内可点击交互</div></div>"
|
||
"</div>"
|
||
)
|
||
|
||
def _fallback_cylinder_courseware(self, prompt: str, subject: str, grade: str, page_count: int) -> dict:
|
||
prompt = self._repair_text(prompt)
|
||
title = "圆柱的体积(第一课时)"
|
||
pages = [
|
||
{
|
||
"type": "title",
|
||
"title": "封面",
|
||
"content": self._courseware_shell_clean(
|
||
title,
|
||
f"{subject or '数学'} · {grade or '六年级'}",
|
||
"""
|
||
<div style='position:absolute;inset:0;background:linear-gradient(135deg,#f4fbf7 0%,#ffffff 52%,#eaf7ef 100%);'>
|
||
<div style='position:absolute;left:54px;top:52px;padding:8px 14px;border-radius:18px;background:rgba(0,84,61,.08);color:#00543d;font-size:13px;font-weight:700;'>课堂导入</div>
|
||
<div style='position:absolute;left:54px;right:54px;top:76px;bottom:26px;display:grid;grid-template-columns:1.1fr .9fr;gap:26px;align-items:center;'>
|
||
<div>
|
||
<div class='zj-pill'>数学 · 形体与测量</div>
|
||
<h1 style='margin:18px 0 0;font-size:42px;line-height:1.08;color:#12372d;'>圆柱的体积</h1>
|
||
<p style='margin:18px 0 0;font-size:20px;line-height:1.7;color:#33544b;max-width:460px;'>围绕“底面积 × 高”展开探究,通过观察、切分、比较和例题,把抽象公式变成可理解、可操作的课堂体验。</p>
|
||
<div style='margin-top:24px;display:flex;gap:12px;flex-wrap:wrap;'>
|
||
<span class='zj-chip'>层次清楚</span>
|
||
<span class='zj-chip'>页面可切换</span>
|
||
<span class='zj-chip'>支持课堂互动</span>
|
||
</div>
|
||
</div>
|
||
<div style='position:relative;height:100%;min-height:260px;'>
|
||
<div style='position:absolute;left:48px;top:30px;width:240px;height:240px;border-radius:50%;background:radial-gradient(circle at 50% 40%,#ecfdf5 0,#d1fae5 45%,#86efac 100%);box-shadow:0 18px 40px rgba(0,168,112,.14);'></div>
|
||
<div style='position:absolute;left:92px;top:62px;width:148px;height:182px;border-left:3px solid #00a870;border-right:3px solid #00a870;background:linear-gradient(90deg,rgba(0,168,112,.10),rgba(255,255,255,.58),rgba(0,168,112,.10));border-radius:26px;'></div>
|
||
<div style='position:absolute;left:92px;top:36px;width:148px;height:52px;border-radius:50%;background:#d1fae5;border:3px solid #00a870;'></div>
|
||
<div style='position:absolute;left:92px;top:212px;width:148px;height:52px;border-radius:50%;background:#a7f3d0;border:3px solid #00a870;'></div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
""",
|
||
"封面页",
|
||
),
|
||
"notes": "封面",
|
||
},
|
||
{
|
||
"type": "content",
|
||
"title": "知识回顾",
|
||
"content": self._courseware_shell_clean(
|
||
"知识回顾",
|
||
"先把旧知识拉回来,再进入新课探究",
|
||
"""
|
||
<div class='zj-grid2' data-tab-group='rev' style='position:absolute;left:22px;right:22px;top:22px;bottom:22px;display:grid;grid-template-columns:240px 1fr;gap:16px;'>
|
||
<div style='display:grid;gap:12px;align-content:start;'>
|
||
<button type='button' class='season-btn active' data-switch='rev1'>
|
||
<span style='font-size:18px;'>长方体体积</span>
|
||
</button>
|
||
<button type='button' class='season-btn' data-switch='rev2'>
|
||
<span style='font-size:18px;'>正方体体积</span>
|
||
</button>
|
||
<button type='button' class='season-btn' data-switch='rev3'>
|
||
<span style='font-size:18px;'>迁移到新知</span>
|
||
</button>
|
||
</div>
|
||
<div class='zj-card' style='padding:22px;display:flex;flex-direction:column;justify-content:center;'>
|
||
<div class='rev-panel rev1' data-panel='rev1'>
|
||
<div class='zj-pill'>旧知 1</div>
|
||
<h3 style='margin:14px 0 10px;font-size:28px;color:#00543d;'>长方体体积公式</h3>
|
||
<p style='margin:0;font-size:18px;line-height:1.8;color:#33544b;'>体积和底面积、高有关。先看“底面铺了多少”,再看“竖起来有多高”。</p>
|
||
</div>
|
||
<div class='rev-panel rev2' data-panel='rev2' style='display:none;'>
|
||
<div class='zj-pill'>旧知 2</div>
|
||
<h3 style='margin:14px 0 10px;font-size:28px;color:#00543d;'>正方体体积公式</h3>
|
||
<p style='margin:0;font-size:18px;line-height:1.8;color:#33544b;'>棱长相等,体积计算更简单,但核心思想仍是“每层面积 × 层数”。</p>
|
||
</div>
|
||
<div class='rev-panel rev3' data-panel='rev3' style='display:none;'>
|
||
<div class='zj-pill'>新课前置</div>
|
||
<h3 style='margin:14px 0 10px;font-size:28px;color:#00543d;'>圆柱能否转化?</h3>
|
||
<p style='margin:0;font-size:18px;line-height:1.8;color:#33544b;'>如果把圆柱沿着高切开,再拼成近似长方体,体积关系会更清楚。</p>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
""",
|
||
"点击卡片切换旧知",
|
||
),
|
||
"notes": "知识回顾",
|
||
},
|
||
{
|
||
"type": "interactive",
|
||
"title": "引入新知",
|
||
"content": self._courseware_shell_clean(
|
||
"引入新知",
|
||
"先看形状和高度,再让学生猜一猜体积和什么有关",
|
||
"""
|
||
<div style='position:absolute;left:22px;right:22px;top:22px;bottom:22px;' class='zj-grid2'>
|
||
<div style='position:relative;min-height:300px;background:linear-gradient(180deg,#f8fefb,#eef9f1);border-radius:16px;border:1px solid #dce8de;overflow:hidden;'>
|
||
<div style='position:absolute;left:24px;top:24px;padding:8px 12px;border-radius:16px;background:#00543d;color:#fff;font-size:13px;font-weight:700;'>情境导入</div>
|
||
<div style='position:absolute;left:86px;top:82px;width:160px;height:230px;border-left:4px solid #00a870;border-right:4px solid #00a870;background:linear-gradient(90deg,rgba(0,168,112,.14),rgba(255,255,255,.65),rgba(0,168,112,.14));border-radius:30px;'></div>
|
||
<div style='position:absolute;left:86px;top:50px;width:160px;height:62px;border-radius:50%;background:#d1fae5;border:4px solid #00a870;'></div>
|
||
<div style='position:absolute;left:86px;top:250px;width:160px;height:62px;border-radius:50%;background:#a7f3d0;border:4px solid #00a870;'></div>
|
||
</div>
|
||
<div class='zj-card' style='padding:20px;display:flex;flex-direction:column;justify-content:center;'>
|
||
<div class='zj-pill'>课堂提问</div>
|
||
<h3 style='margin:14px 0 12px;font-size:28px;color:#12372d;'>你觉得圆柱体积和什么有关?</h3>
|
||
<div style='display:grid;gap:10px;margin-top:10px;'>
|
||
<div class='zj-card' style='padding:14px 16px;'>A. 只和底面形状有关</div>
|
||
<div class='zj-card' style='padding:14px 16px;'>B. 和底面积、高都有关系</div>
|
||
<div class='zj-card' style='padding:14px 16px;'>C. 只和颜色有关</div>
|
||
</div>
|
||
<div style='margin-top:18px;padding:14px 16px;border-left:4px solid #00a870;background:#f7faf8;border-radius:10px;color:#33544b;'>课堂结论:体积计算要抓住“底面积 × 高”这条主线。</div>
|
||
</div>
|
||
</div>
|
||
""",
|
||
"学生先猜想,再看图验证",
|
||
),
|
||
"notes": "引入",
|
||
},
|
||
{
|
||
"type": "content",
|
||
"title": "公式探究",
|
||
"content": self._courseware_shell_clean(
|
||
"公式探究",
|
||
"用切、拼、比的方式,把圆柱体积想清楚",
|
||
"""
|
||
<div class='zj-grid2' data-tab-group='step' style='position:absolute;left:22px;right:22px;top:22px;bottom:22px;display:grid;grid-template-columns:240px 1fr;gap:16px;'>
|
||
<div style='display:grid;gap:10px;align-content:start;'>
|
||
<button type='button' class='season-btn active' data-switch='step1'>
|
||
<span style='font-size:18px;'>切一切</span>
|
||
</button>
|
||
<button type='button' class='season-btn' data-switch='step2'>
|
||
<span style='font-size:18px;'>拼一拼</span>
|
||
</button>
|
||
<button type='button' class='season-btn' data-switch='step3'>
|
||
<span style='font-size:18px;'>想一想</span>
|
||
</button>
|
||
</div>
|
||
<div class='zj-card' style='padding:22px;position:relative;overflow:auto;'>
|
||
<div class='step-panel s1' data-panel='step1'>
|
||
<div class='zj-pill'>切分观察</div>
|
||
<h3 style='margin:14px 0 10px;font-size:30px;color:#00543d;'>把圆柱切成很多薄片</h3>
|
||
<p style='margin:0;font-size:18px;line-height:1.8;color:#33544b;max-width:560px;'>圆柱越像被分成很多很薄的小层,每一层越接近一个圆形小面。层数越多,拼出的形状就越接近长方体。</p>
|
||
</div>
|
||
<div class='step-panel s2' data-panel='step2' style='display:none;'>
|
||
<div class='zj-pill'>转化比较</div>
|
||
<h3 style='margin:14px 0 10px;font-size:30px;color:#00543d;'>拼成近似长方体</h3>
|
||
<p style='margin:0;font-size:18px;line-height:1.8;color:#33544b;max-width:560px;'>切开后重新排列,底面形状变了,但每一层的面积总和没有变,高也没有变,所以体积也没有变。</p>
|
||
<div style='margin-top:18px;font-size:34px;font-weight:800;color:#12372d;'>V = 底面积 × 高</div>
|
||
</div>
|
||
<div class='step-panel s3' data-panel='step3' style='display:none;'>
|
||
<div class='zj-pill'>归纳结论</div>
|
||
<h3 style='margin:14px 0 10px;font-size:30px;color:#00543d;'>圆柱体积公式</h3>
|
||
<div style='font-size:38px;font-weight:800;color:#12372d;'>V = S × h</div>
|
||
<p style='margin:12px 0 0;font-size:18px;line-height:1.8;color:#33544b;'>S 表示底面积,h 表示高。知道这两个量,就能求圆柱的体积。</p>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
""",
|
||
"切、拼、比,逐步导出公式",
|
||
),
|
||
"notes": "公式探究",
|
||
},
|
||
{
|
||
"type": "interactive",
|
||
"title": "例题讲解",
|
||
"content": self._courseware_shell_clean(
|
||
"例题讲解",
|
||
"把公式落到计算里,按步骤说清楚",
|
||
"""
|
||
<div class='zj-grid2' data-tab-group='ex' style='position:absolute;left:22px;right:22px;top:22px;bottom:22px;display:grid;grid-template-columns:1fr 320px;gap:16px;'>
|
||
<div class='zj-card' style='padding:22px;'>
|
||
<div class='zj-pill'>例题</div>
|
||
<h3 style='margin:14px 0 12px;font-size:28px;color:#12372d;'>一个圆柱的底面积是 28.26 cm²,高是 10 cm,体积是多少?</h3>
|
||
<div class='zj-grid3' style='margin-top:16px;'>
|
||
<button type='button' class='season-btn active' data-switch='ex1'>
|
||
<span style='font-size:18px;'>第 1 步</span>
|
||
</button>
|
||
<button type='button' class='season-btn' data-switch='ex2'>
|
||
<span style='font-size:18px;'>第 2 步</span>
|
||
</button>
|
||
<button type='button' class='season-btn' data-switch='ex3'>
|
||
<span style='font-size:18px;'>第 3 步</span>
|
||
</button>
|
||
</div>
|
||
<div style='margin-top:18px;padding:18px;border-radius:14px;background:#f7faf8;border:1px solid #dce8de;'>
|
||
<div class='ex-panel ex1' data-panel='ex1'>
|
||
<div style='font-size:16px;color:#6b7f76;'>已知</div>
|
||
<div style='margin-top:8px;font-size:20px;color:#12372d;font-weight:700;'>S = 28.26 cm²,h = 10 cm</div>
|
||
</div>
|
||
<div class='ex-panel ex2' data-panel='ex2' style='display:none;'>
|
||
<div style='font-size:16px;color:#6b7f76;'>公式</div>
|
||
<div style='margin-top:8px;font-size:28px;color:#12372d;font-weight:800;'>V = S × h</div>
|
||
<div style='margin-top:8px;font-size:18px;color:#33544b;'>V = 28.26 × 10</div>
|
||
</div>
|
||
<div class='ex-panel ex3' data-panel='ex3' style='display:none;'>
|
||
<div style='font-size:16px;color:#6b7f76;'>结果</div>
|
||
<div style='margin-top:8px;font-size:32px;color:#00543d;font-weight:800;'>V = 282.6 cm³</div>
|
||
<div style='margin-top:8px;font-size:18px;color:#33544b;'>答:这个圆柱的体积是 282.6 立方厘米。</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<div class='zj-card' style='padding:20px;display:flex;flex-direction:column;gap:12px;justify-content:center;'>
|
||
<div class='zj-pill'>解题提醒</div>
|
||
<div style='font-size:18px;line-height:1.8;color:#33544b;'>圆柱体积公式中的 <strong>S</strong> 是底面积,不是底面周长。单位也要对应写成立方单位。</div>
|
||
<div style='padding:14px 16px;border-radius:12px;background:#ecfdf5;color:#00543d;'>写答语时要带上单位,例如 cm³、m³。</div>
|
||
</div>
|
||
</div>
|
||
""",
|
||
"按步骤理解计算过程",
|
||
),
|
||
"notes": "例题",
|
||
},
|
||
{
|
||
"type": "exercise",
|
||
"title": "巩固练习",
|
||
"content": self._courseware_shell_clean(
|
||
"巩固练习",
|
||
"点击选项,立即查看判断结果",
|
||
"""
|
||
<div class='zj-grid2' data-tab-group='q1' style='position:absolute;left:22px;right:22px;top:22px;bottom:22px;display:grid;grid-template-columns:1fr 320px;gap:16px;'>
|
||
<div class='zj-card' style='padding:22px;'>
|
||
<div class='zj-pill'>选择题</div>
|
||
<h3 style='margin:14px 0 12px;font-size:28px;color:#12372d;'>圆柱体积公式正确的是哪一个?</h3>
|
||
<div style='display:grid;gap:10px;margin-top:16px;'>
|
||
<button type='button' class='season-btn active' data-switch='qa' style='justify-content:flex-start;padding:0 16px;'>
|
||
<span style='font-size:16px;'>A. V = 底面周长 × 高</span>
|
||
</button>
|
||
<button type='button' class='season-btn' data-switch='qb' style='justify-content:flex-start;padding:0 16px;'>
|
||
<span style='font-size:16px;'>B. V = 底面积 × 高</span>
|
||
</button>
|
||
<button type='button' class='season-btn' data-switch='qc' style='justify-content:flex-start;padding:0 16px;'>
|
||
<span style='font-size:16px;'>C. V = 半径 × 高</span>
|
||
</button>
|
||
</div>
|
||
</div>
|
||
<div class='zj-card' style='padding:20px;display:flex;align-items:center;justify-content:center;text-align:left;'>
|
||
<div class='q-panel qa' data-panel='qa'>
|
||
<div class='zj-pill'>反馈</div>
|
||
<div style='margin-top:12px;font-size:18px;line-height:1.8;color:#b45309;'>A 不是正确答案。圆柱体积不是把周长直接相乘。</div>
|
||
</div>
|
||
<div class='q-panel qb' data-panel='qb' style='display:none;'>
|
||
<div class='zj-pill'>反馈</div>
|
||
<div style='margin-top:12px;font-size:18px;line-height:1.8;color:#00543d;'>回答正确。圆柱体积就是“底面积 × 高”。</div>
|
||
</div>
|
||
<div class='q-panel qc' data-panel='qc' style='display:none;'>
|
||
<div class='zj-pill'>反馈</div>
|
||
<div style='margin-top:12px;font-size:18px;line-height:1.8;color:#b45309;'>C 不是正确答案。半径只是求底面积的一个中间量。</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
""",
|
||
"选择答案后直接反馈",
|
||
),
|
||
"notes": "练习",
|
||
},
|
||
{
|
||
"type": "summary",
|
||
"title": "总结",
|
||
"content": self._courseware_shell_clean(
|
||
"总结与作业",
|
||
"把公式、方法和易错点一次收好",
|
||
"""
|
||
<div class='zj-grid2' style='position:absolute;left:22px;right:22px;top:22px;bottom:22px;display:grid;grid-template-columns:1fr 340px;gap:16px;'>
|
||
<div class='zj-card' style='padding:22px;'>
|
||
<div class='zj-pill'>本课总结</div>
|
||
<div style='display:grid;gap:12px;margin-top:16px;'>
|
||
<div class='zj-card' style='padding:14px 16px;'>1. 圆柱体积公式:V = S × h</div>
|
||
<div class='zj-card' style='padding:14px 16px;'>2. 核心方法:切、拼、转化成近似长方体</div>
|
||
<div class='zj-card' style='padding:14px 16px;'>3. 易错提醒:S 是底面积,不是底面周长</div>
|
||
</div>
|
||
</div>
|
||
<div class='zj-card' style='padding:20px;display:flex;flex-direction:column;justify-content:center;gap:12px;' data-toggle-group='homework-block'>
|
||
<div class='zj-pill'>课后任务</div>
|
||
<div style='font-size:18px;line-height:1.8;color:#33544b;'>1. 完成课本练习。<br>2. 找一个生活中的圆柱体,量一量底面和高,试着算体积。</div>
|
||
<button type='button' class='zj-btn alt' data-toggle='hw' style='display:inline-flex;align-items:center;justify-content:center;'>点击展开提醒</button>
|
||
<div class='hw-panel' data-target='hw' style='padding:14px 16px;border-radius:12px;background:#f7faf8;border:1px solid #dce8de;display:none;'>记得带单位,写成立方厘米、立方分米或立方米。</div>
|
||
</div>
|
||
</div>
|
||
""",
|
||
"总结方法并布置作业",
|
||
),
|
||
"notes": "总结",
|
||
},
|
||
]
|
||
total = max(5, min(page_count or 7, len(pages)))
|
||
return {"title": title, "summary": "围绕底面积与高探究圆柱体积", "tags": ["圆柱", "体积", "互动课件"], "pages": pages[:total]}
|
||
|
||
def _fallback_orbit_courseware(self, prompt: str, subject: str, grade: str, page_count: int) -> dict:
|
||
prompt = self._repair_text(prompt)
|
||
title = "地球公转与四季变化"
|
||
pages = [
|
||
{
|
||
"type": "title",
|
||
"title": "封面",
|
||
"content": self._courseware_shell_clean(
|
||
title,
|
||
f"{subject or '科学'} · {grade or '六年级'}",
|
||
"""
|
||
<div style='position:absolute;inset:0;background:linear-gradient(135deg,#f4fbf7 0%,#ffffff 52%,#eaf7ef 100%);'>
|
||
<div style='position:absolute;left:54px;top:52px;padding:8px 14px;border-radius:18px;background:rgba(0,84,61,.08);color:#00543d;font-size:13px;font-weight:700;'>情境导入</div>
|
||
<div style='position:absolute;left:54px;right:54px;top:76px;bottom:26px;display:grid;grid-template-columns:1.1fr .9fr;gap:26px;align-items:center;'>
|
||
<div>
|
||
<div class='zj-pill'>科学 · 地球与宇宙</div>
|
||
<h1 style='margin:18px 0 0;font-size:42px;line-height:1.08;color:#12372d;'>地球公转与四季变化</h1>
|
||
<p style='margin:18px 0 0;font-size:20px;line-height:1.7;color:#33544b;max-width:460px;'>通过轨道、倾角和阳光照射方向的变化,把春夏秋冬的形成过程讲清楚、看明白、能点击。</p>
|
||
<div style='margin-top:24px;display:flex;gap:12px;flex-wrap:wrap;'>
|
||
<span class='zj-chip'>轨道示意</span>
|
||
<span class='zj-chip'>四季按钮</span>
|
||
<span class='zj-chip'>课堂可演示</span>
|
||
</div>
|
||
</div>
|
||
<div style='position:relative;height:100%;min-height:260px;'>
|
||
<div class='orbit-wrap'>
|
||
<div class='orbit-sun'></div>
|
||
<div class='orbit-path'></div>
|
||
<div class='earth-dot'></div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
""",
|
||
"封面页",
|
||
),
|
||
"notes": "封面",
|
||
},
|
||
{
|
||
"type": "content",
|
||
"title": "知识回顾",
|
||
"content": self._courseware_shell_clean(
|
||
"知识回顾",
|
||
"先把轨道、地轴倾斜和太阳光照这三个要点记住",
|
||
"""
|
||
<div style='position:absolute;left:22px;right:22px;top:22px;bottom:22px;' class='zj-grid3'>
|
||
<div class='zj-card' style='padding:18px;'>
|
||
<div class='zj-pill'>要点 1</div>
|
||
<h3 style='margin:14px 0 8px;font-size:22px;color:#00543d;'>地球在公转</h3>
|
||
<p style='margin:0;font-size:16px;line-height:1.7;color:#33544b;'>地球绕太阳转一圈,需要大约一年。</p>
|
||
</div>
|
||
<div class='zj-card' style='padding:18px;'>
|
||
<div class='zj-pill'>要点 2</div>
|
||
<h3 style='margin:14px 0 8px;font-size:22px;color:#00543d;'>地轴是倾斜的</h3>
|
||
<p style='margin:0;font-size:16px;line-height:1.7;color:#33544b;'>地轴倾斜让同一地区在不同时间受到的阳光不同。</p>
|
||
</div>
|
||
<div class='zj-card' style='padding:18px;'>
|
||
<div class='zj-pill'>要点 3</div>
|
||
<h3 style='margin:14px 0 8px;font-size:22px;color:#00543d;'>太阳光照变化</h3>
|
||
<p style='margin:0;font-size:16px;line-height:1.7;color:#33544b;'>阳光照射角度和昼夜长短,会随着公转位置变化。</p>
|
||
</div>
|
||
</div>
|
||
""",
|
||
"三个关键概念先对齐",
|
||
),
|
||
"notes": "知识回顾",
|
||
},
|
||
{
|
||
"type": "interactive",
|
||
"title": "四季探究",
|
||
"content": self._courseware_shell_clean(
|
||
"四季探究",
|
||
"点击春夏秋冬,观察光照和气候变化",
|
||
"""
|
||
<div class='zj-grid2' data-tab-group='season' style='position:absolute;left:22px;right:22px;top:22px;bottom:22px;display:grid;grid-template-columns:230px 1fr;gap:16px;overflow:auto;'>
|
||
<div style='display:grid;gap:10px;align-content:start;'>
|
||
<button type='button' class='season-btn active' data-switch='spring'>春天</button>
|
||
<button type='button' class='season-btn' data-switch='summer'>夏天</button>
|
||
<button type='button' class='season-btn' data-switch='autumn'>秋天</button>
|
||
<button type='button' class='season-btn' data-switch='winter'>冬天</button>
|
||
</div>
|
||
<div class='zj-card' style='padding:22px;position:relative;overflow:auto;'>
|
||
<div class='season-panel' data-panel='spring'>
|
||
<div class='zj-pill'>春季</div>
|
||
<h3 style='margin:14px 0 10px;font-size:30px;color:#00543d;'>阳光开始变强,气温慢慢回升</h3>
|
||
<p style='margin:0;font-size:18px;line-height:1.8;color:#33544b;max-width:600px;'>春季时太阳直射位置逐渐北移,北半球白天变长,天气回暖,植物也开始生长。</p>
|
||
</div>
|
||
<div class='season-panel' data-panel='summer' style='display:none;'>
|
||
<div class='zj-pill'>夏季</div>
|
||
<h3 style='margin:14px 0 10px;font-size:30px;color:#00543d;'>阳光更强,白天更长</h3>
|
||
<p style='margin:0;font-size:18px;line-height:1.8;color:#33544b;max-width:600px;'>夏季时北半球受到的太阳光更直接,地面获得的热量更多,所以气温更高、白昼更长。</p>
|
||
</div>
|
||
<div class='season-panel' data-panel='autumn' style='display:none;'>
|
||
<div class='zj-pill'>秋季</div>
|
||
<h3 style='margin:14px 0 10px;font-size:30px;color:#00543d;'>阳光减弱,天气转凉</h3>
|
||
<p style='margin:0;font-size:18px;line-height:1.8;color:#33544b;max-width:600px;'>秋季太阳直射位置继续变化,白天变短,气温下降,作物开始成熟和收获。</p>
|
||
</div>
|
||
<div class='season-panel' data-panel='winter' style='display:none;'>
|
||
<div class='zj-pill'>冬季</div>
|
||
<h3 style='margin:14px 0 10px;font-size:30px;color:#00543d;'>阳光较弱,白天更短</h3>
|
||
<p style='margin:0;font-size:18px;line-height:1.8;color:#33544b;max-width:600px;'>冬季时太阳光照更斜,地面接收热量少,所以天气寒冷,白昼也更短。</p>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<style>
|
||
.season-btn.active{border-color:#00a870;background:#ecfdf5;color:#00543d;box-shadow:0 10px 18px rgba(0,168,112,.10);}
|
||
</style>
|
||
""",
|
||
"四季按钮可直接切换",
|
||
),
|
||
"notes": "四季探究",
|
||
},
|
||
{
|
||
"type": "content",
|
||
"title": "现象解释",
|
||
"content": self._courseware_shell_clean(
|
||
"现象解释",
|
||
"同样是太阳,为什么不同季节感觉不一样?",
|
||
"""
|
||
<div style='position:absolute;left:22px;right:22px;top:22px;bottom:22px;display:grid;grid-template-columns:1fr 1fr;gap:16px;'>
|
||
<div class='zj-card' style='padding:20px;'>
|
||
<div class='zj-pill'>原因 1</div>
|
||
<h3 style='margin:14px 0 10px;font-size:28px;color:#00543d;'>阳光直射角度不同</h3>
|
||
<p style='margin:0;font-size:18px;line-height:1.8;color:#33544b;'>角度越直,单位面积接收到的热量越多;角度越斜,热量越分散。</p>
|
||
</div>
|
||
<div class='zj-card' style='padding:20px;'>
|
||
<div class='zj-pill'>原因 2</div>
|
||
<h3 style='margin:14px 0 10px;font-size:28px;color:#00543d;'>白昼长短不同</h3>
|
||
<p style='margin:0;font-size:18px;line-height:1.8;color:#33544b;'>白天越长,太阳照射时间越久,地面吸收热量也更多。</p>
|
||
</div>
|
||
</div>
|
||
""",
|
||
"从光照与时间两个角度解释",
|
||
),
|
||
"notes": "现象解释",
|
||
},
|
||
{
|
||
"type": "summary",
|
||
"title": "总结",
|
||
"content": self._courseware_shell_clean(
|
||
"总结与作业",
|
||
"把公转、地轴倾斜和四季变化连起来理解",
|
||
"""
|
||
<div class='zj-grid2' style='position:absolute;left:22px;right:22px;top:22px;bottom:22px;display:grid;grid-template-columns:1fr 340px;gap:16px;'>
|
||
<div class='zj-card' style='padding:22px;'>
|
||
<div class='zj-pill'>本课总结</div>
|
||
<div style='display:grid;gap:12px;margin-top:16px;'>
|
||
<div class='zj-card' style='padding:14px 16px;'>1. 地球围绕太阳公转,形成一年四季的周期变化。</div>
|
||
<div class='zj-card' style='padding:14px 16px;'>2. 地轴倾斜,让不同季节接受的阳光不同。</div>
|
||
<div class='zj-card' style='padding:14px 16px;'>3. 阳光直射角度和白昼长短,决定了冷热变化。</div>
|
||
</div>
|
||
</div>
|
||
<div class='zj-card' style='padding:20px;display:flex;flex-direction:column;justify-content:center;gap:12px;' data-toggle-group='season-homework'>
|
||
<div class='zj-pill'>课后任务</div>
|
||
<div style='font-size:18px;line-height:1.8;color:#33544b;'>1. 画一张地球绕太阳公转示意图。<br>2. 说一说春夏秋冬各自最明显的特点。</div>
|
||
<button type='button' class='zj-btn alt' data-toggle='season-hw' style='display:inline-flex;align-items:center;justify-content:center;'>点击展开提示</button>
|
||
<div class='season-hw-panel' data-target='season-hw' style='padding:14px 16px;border-radius:12px;background:#f7faf8;border:1px solid #dce8de;display:none;'>答题时可以先写“原因”,再写“现象”,这样更完整。</div>
|
||
</div>
|
||
</div>
|
||
""",
|
||
"把四季原因说完整",
|
||
),
|
||
"notes": "总结",
|
||
},
|
||
]
|
||
total = max(5, min(page_count or 6, len(pages)))
|
||
return {"title": title, "summary": "围绕地球公转与太阳光照变化解释四季形成", "tags": ["地球公转", "四季", "科学探究"], "pages": pages[:total]}
|
||
|
||
def _fallback_general_courseware(self, prompt: str, subject: str, grade: str, page_count: int) -> dict:
|
||
prompt = self._repair_text(prompt)
|
||
title = prompt[:24] or "互动课件"
|
||
pages = [
|
||
{
|
||
"type": "title",
|
||
"title": "封面",
|
||
"content": self._courseware_shell_clean(
|
||
title,
|
||
f"{subject or '综合'} · {grade or '课堂教学'}",
|
||
"""
|
||
<div style='position:absolute;inset:0;background:linear-gradient(135deg,#f4fbf7 0%,#ffffff 52%,#eaf7ef 100%);'>
|
||
<div style='position:absolute;left:54px;top:52px;padding:8px 14px;border-radius:18px;background:rgba(0,84,61,.08);color:#00543d;font-size:13px;font-weight:700;'>课堂导入</div>
|
||
<div style='position:absolute;left:54px;right:54px;top:76px;bottom:26px;display:grid;grid-template-columns:1.1fr .9fr;gap:26px;align-items:center;'>
|
||
<div>
|
||
<div class='zj-pill'>知识生成 · 互动呈现</div>
|
||
<h1 style='margin:18px 0 0;font-size:42px;line-height:1.08;color:#12372d;'>""" + html_lib.escape(title) + """</h1>
|
||
<p style='margin:18px 0 0;font-size:20px;line-height:1.7;color:#33544b;max-width:460px;'>用清爽、分层、可点击的页面组织课堂内容,适合投屏展示和逐步讲解。</p>
|
||
<div style='margin-top:24px;display:flex;gap:12px;flex-wrap:wrap;'>
|
||
<span class='zj-chip'>层次清楚</span>
|
||
<span class='zj-chip'>页面可切换</span>
|
||
<span class='zj-chip'>支持课堂互动</span>
|
||
</div>
|
||
</div>
|
||
<div style='position:relative;height:100%;min-height:260px;'>
|
||
<div class='orbit-wrap'>
|
||
<div class='orbit-sun'></div>
|
||
<div class='orbit-path'></div>
|
||
<div class='earth-dot'></div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
""",
|
||
"封面页",
|
||
),
|
||
"notes": "封面",
|
||
},
|
||
{
|
||
"type": "content",
|
||
"title": "核心概念",
|
||
"content": self._courseware_shell_clean(
|
||
"核心概念",
|
||
"先把问题拆开,再逐步组织内容",
|
||
"""
|
||
<div style='position:absolute;left:22px;right:22px;top:22px;bottom:22px;' class='zj-grid3'>
|
||
<div class='zj-card' style='padding:18px;'>
|
||
<div class='zj-pill'>第 1 点</div>
|
||
<h3 style='margin:14px 0 8px;font-size:22px;color:#00543d;'>要讲什么</h3>
|
||
<p style='margin:0;font-size:16px;line-height:1.7;color:#33544b;'>把主题拆成 2 到 3 个核心问题,再分别展开。</p>
|
||
</div>
|
||
<div class='zj-card' style='padding:18px;'>
|
||
<div class='zj-pill'>第 2 点</div>
|
||
<h3 style='margin:14px 0 8px;font-size:22px;color:#00543d;'>怎么讲</h3>
|
||
<p style='margin:0;font-size:16px;line-height:1.7;color:#33544b;'>先观察,再归纳,最后用例子验证。</p>
|
||
</div>
|
||
<div class='zj-card' style='padding:18px;'>
|
||
<div class='zj-pill'>第 3 点</div>
|
||
<h3 style='margin:14px 0 8px;font-size:22px;color:#00543d;'>怎么练</h3>
|
||
<p style='margin:0;font-size:16px;line-height:1.7;color:#33544b;'>用点击反馈、小练习和总结页收尾。</p>
|
||
</div>
|
||
</div>
|
||
""",
|
||
"搭好课堂结构",
|
||
),
|
||
"notes": "核心概念",
|
||
},
|
||
{
|
||
"type": "interactive",
|
||
"title": "互动探究",
|
||
"content": self._courseware_shell_clean(
|
||
"互动探究",
|
||
"点击左侧选项,右侧显示不同内容",
|
||
"""
|
||
<div class='zj-grid2' data-tab-group='g' style='position:absolute;left:22px;right:22px;top:22px;bottom:22px;display:grid;grid-template-columns:230px 1fr;gap:16px;overflow:auto;'>
|
||
<div style='display:grid;gap:10px;align-content:start;'>
|
||
<button type='button' class='season-btn active' data-switch='g1'>观察</button>
|
||
<button type='button' class='season-btn' data-switch='g2'>分析</button>
|
||
<button type='button' class='season-btn' data-switch='g3'>总结</button>
|
||
</div>
|
||
<div class='zj-card' style='padding:22px;position:relative;overflow:auto;'>
|
||
<div class='g-panel g1' data-panel='g1'>
|
||
<div class='zj-pill'>观察</div>
|
||
<h3 style='margin:14px 0 10px;font-size:30px;color:#00543d;'>先看现象,再找规律</h3>
|
||
<p style='margin:0;font-size:18px;line-height:1.8;color:#33544b;max-width:600px;'>围绕图片、数据或情境,先让学生说出“看见了什么”,再引导他们说“为什么会这样”。</p>
|
||
</div>
|
||
<div class='g-panel g2' data-panel='g2' style='display:none;'>
|
||
<div class='zj-pill'>分析</div>
|
||
<h3 style='margin:14px 0 10px;font-size:30px;color:#00543d;'>把线索整理成逻辑</h3>
|
||
<p style='margin:0;font-size:18px;line-height:1.8;color:#33544b;max-width:600px;'>把原因、结果和条件分开写清楚,再用一两句话把关系串起来。</p>
|
||
</div>
|
||
<div class='g-panel g3' data-panel='g3' style='display:none;'>
|
||
<div class='zj-pill'>总结</div>
|
||
<h3 style='margin:14px 0 10px;font-size:30px;color:#00543d;'>把关键结论收拢</h3>
|
||
<p style='margin:0;font-size:18px;line-height:1.8;color:#33544b;max-width:600px;'>最后把今天最重要的一句话写成结论,方便学生复述和记忆。</p>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
""",
|
||
"交互区可直接切换",
|
||
),
|
||
"notes": "互动探究",
|
||
},
|
||
{
|
||
"type": "exercise",
|
||
"title": "课堂练习",
|
||
"content": self._courseware_shell_clean(
|
||
"课堂练习",
|
||
"选择一个答案,马上看反馈",
|
||
"""
|
||
<div class='zj-grid2' data-tab-group='h' style='position:absolute;left:22px;right:22px;top:22px;bottom:22px;display:grid;grid-template-columns:1fr 320px;gap:16px;overflow:auto;'>
|
||
<div class='zj-card' style='padding:22px;overflow:auto;'>
|
||
<div class='zj-pill'>练习题</div>
|
||
<h3 style='margin:14px 0 12px;font-size:28px;color:#12372d;'>下面哪一种说法最符合今天的主题?</h3>
|
||
<div style='display:grid;gap:10px;margin-top:16px;'>
|
||
<button type='button' class='season-btn active' data-switch='h1' style='justify-content:flex-start;padding:0 16px;'>
|
||
<span style='font-size:16px;'>A. 只要记住一个结论就够了</span>
|
||
</button>
|
||
<button type='button' class='season-btn' data-switch='h2' style='justify-content:flex-start;padding:0 16px;'>
|
||
<span style='font-size:16px;'>B. 要把观察、分析和总结连起来</span>
|
||
</button>
|
||
<button type='button' class='season-btn' data-switch='h3' style='justify-content:flex-start;padding:0 16px;'>
|
||
<span style='font-size:16px;'>C. 课堂上不需要互动</span>
|
||
</button>
|
||
</div>
|
||
</div>
|
||
<div class='zj-card' style='padding:20px;display:flex;align-items:center;justify-content:center;text-align:left;'>
|
||
<div class='h-panel h1' data-panel='h1'>
|
||
<div class='zj-pill'>反馈</div>
|
||
<div style='margin-top:12px;font-size:18px;line-height:1.8;color:#b45309;'>A 不完整。课堂结构不能只靠一个结论支撑。</div>
|
||
</div>
|
||
<div class='h-panel h2' data-panel='h2' style='display:none;'>
|
||
<div class='zj-pill'>反馈</div>
|
||
<div style='margin-top:12px;font-size:18px;line-height:1.8;color:#00543d;'>回答正确。课堂内容应该有完整的观察、分析和总结链条。</div>
|
||
</div>
|
||
<div class='h-panel h3' data-panel='h3' style='display:none;'>
|
||
<div class='zj-pill'>反馈</div>
|
||
<div style='margin-top:12px;font-size:18px;line-height:1.8;color:#b45309;'>C 不符合互动课件的设计原则。</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
""",
|
||
"选择后看反馈",
|
||
),
|
||
"notes": "课堂练习",
|
||
},
|
||
{
|
||
"type": "summary",
|
||
"title": "总结",
|
||
"content": self._courseware_shell_clean(
|
||
"总结与作业",
|
||
"把今天的重点提炼成一页",
|
||
"""
|
||
<div class='zj-grid2' style='position:absolute;left:22px;right:22px;top:22px;bottom:22px;display:grid;grid-template-columns:1fr 340px;gap:16px;overflow:auto;'>
|
||
<div class='zj-card' style='padding:22px;overflow:auto;'>
|
||
<div class='zj-pill'>本课总结</div>
|
||
<div style='display:grid;gap:12px;margin-top:16px;'>
|
||
<div class='zj-card' style='padding:14px 16px;'>1. 先观察,再分析,再总结。</div>
|
||
<div class='zj-card' style='padding:14px 16px;'>2. 把复杂内容拆成若干清楚的部分。</div>
|
||
<div class='zj-card' style='padding:14px 16px;'>3. 用点击互动提高课堂参与感。</div>
|
||
</div>
|
||
</div>
|
||
<div class='zj-card' style='padding:20px;display:flex;flex-direction:column;justify-content:center;gap:12px;' data-toggle-group='general-homework'>
|
||
<div class='zj-pill'>课后任务</div>
|
||
<div style='font-size:18px;line-height:1.8;color:#33544b;'>1. 把本课内容整理成 3 条要点。<br>2. 试着为一个新的知识点设计 1 个互动问题。</div>
|
||
<button type='button' class='zj-btn alt' data-toggle='g-hw' style='display:inline-flex;align-items:center;justify-content:center;'>点击展开提示</button>
|
||
<div class='g-hw-panel' data-target='g-hw' style='padding:14px 16px;border-radius:12px;background:#f7faf8;border:1px solid #dce8de;display:none;'>提示:结论要短,任务要具体,方便学生完成。</div>
|
||
</div>
|
||
</div>
|
||
""",
|
||
"整理成可复述的结论",
|
||
),
|
||
"notes": "总结",
|
||
},
|
||
]
|
||
total = max(5, min(page_count or 5, len(pages)))
|
||
return {"title": title, "summary": "把主题拆分成观察、分析、总结的互动课件", "tags": ["互动课件", "可点击", "课堂讲解"], "pages": pages[:total]}
|
||
|
||
async def generate_courseware(self, prompt: str, subject: str = "", grade: str = "", page_count: int = 8, aspect_ratio: str = "16:9") -> dict:
|
||
prompt = self._repair_text(prompt)
|
||
topic = self._courseware_topic(prompt)
|
||
if topic == "cylinder":
|
||
return self._fallback_cylinder_courseware(prompt, subject or "数学", grade or "六年级", page_count)
|
||
if topic == "orbit":
|
||
return self._fallback_orbit_courseware(prompt, subject or "科学", grade or "六年级", page_count)
|
||
|
||
try:
|
||
result = await self._chat_json([
|
||
{"role": "system", "content": "请生成多页互动课件 JSON,包含 title、summary、tags、pages。每页 content 必须是可直接渲染的 <div> HTML 片段,页面结构清爽,适合教室投屏。"},
|
||
{"role": "user", "content": f"学科:{subject or '未指定'}\n年级:{grade or '未指定'}\n页数要求:{page_count}\n\n教师需求:{prompt}"},
|
||
], temperature=0.45, max_tokens=12000)
|
||
normalized = self._normalize_courseware_result(result, prompt, subject, grade, page_count, aspect_ratio)
|
||
if normalized:
|
||
return normalized
|
||
except Exception:
|
||
pass
|
||
return self._fallback_general_courseware(prompt, subject, grade, page_count)
|
||
|