case-forge / core /render.py
nextmarte's picture
Case Forge app: authoring UI + ZeroGPU inference (qwen3-4b-v2)
f8d4986 verified
Raw
History Blame Contribute Delete
6.47 kB
"""Render a schema-valid case+note dict into classroom-ready Markdown.
The hero of Case Forge is that the output is *usable in class*, not a wall of
JSON. This turns the contract (`data/schema.py`) into a well-structured case and
teaching note, and surfaces the genre's quality checks as visible badges.
"""
from __future__ import annotations
# Section labels per language (UI chrome lives in shared/i18n; these are the
# document headings, kept next to the renderer that emits them).
_L = {
"case": {
"en": {
"decision": "The decision", "protagonist": "Protagonist",
"context": "Context", "data": "Data & evidence",
"exhibits": "Exhibits", "alternatives": "Paths to weigh",
"closing": "Where it stands", "references": "References",
},
"pt": {
"decision": "A decisão", "protagonist": "Protagonista",
"context": "Contexto", "data": "Dados e evidências",
"exhibits": "Anexos", "alternatives": "Caminhos a ponderar",
"closing": "Onde isso para", "references": "Referências",
},
},
"note": {
"en": {
"title": "Teaching note", "summary": "Summary",
"audience": "Audience", "relevance": "Managerial relevance",
"objectives": "Learning objectives", "sources": "Data sources",
"anchor": "Theoretical anchor", "plan": "Discussion plan",
"questions": "Discussion questions", "analysis": "Analysis & expected answers",
"closure": "Closure", "epilogue": "Epilogue", "biblio": "Bibliography",
"min": "min",
},
"pt": {
"title": "Nota de ensino", "summary": "Resumo",
"audience": "Público-alvo", "relevance": "Relevância gerencial",
"objectives": "Objetivos de aprendizagem", "sources": "Fontes dos dados",
"anchor": "Ancoragem teórica", "plan": "Plano de discussão",
"questions": "Questões de discussão", "analysis": "Análise e respostas esperadas",
"closure": "Fechamento", "epilogue": "Epílogo", "biblio": "Bibliografia",
"min": "min",
},
},
}
def _lang(obj: dict) -> str:
return obj.get("language") if obj.get("language") in ("pt", "en") else "pt"
def render_case(obj: dict) -> str:
"""The case itself, as Markdown — stops at the decision point."""
if not obj:
return ""
lang = _lang(obj)
L = _L["case"][lang]
c = obj.get("case") or {}
out: list[str] = []
out.append(f"# {obj.get('title', '').strip()}")
domain = obj.get("domain", "").strip()
if domain:
out.append(f"*{domain}*")
out.append("")
if c.get("hook"):
out.append(c["hook"].strip())
out.append("")
if c.get("protagonist"):
out.append(f"**{L['protagonist']}:** {c['protagonist'].strip()}")
out.append("")
if c.get("decision_point"):
out.append(f"> **{L['decision']}:** {c['decision_point'].strip()}")
out.append("")
if c.get("context"):
out.append(f"## {L['context']}")
out.append(c["context"].strip())
out.append("")
if c.get("data"):
out.append(f"## {L['data']}")
out += [f"- {d.strip()}" for d in c["data"]]
out.append("")
if c.get("exhibits"):
out.append(f"## {L['exhibits']}")
for ex in c["exhibits"]:
out.append(f"**{ex.get('title', '').strip()}**")
out.append("")
out.append(str(ex.get("content", "")).strip())
out.append("")
if c.get("alternatives"):
out.append(f"## {L['alternatives']}")
out += [f"- {a.strip()}" for a in c["alternatives"]]
out.append("")
if c.get("closing"):
out.append(f"## {L['closing']}")
out.append(c["closing"].strip())
out.append("")
if c.get("references"):
out.append(f"## {L['references']}")
out += [f"- {r.strip()}" for r in c["references"]]
out.append("")
return "\n".join(out).strip()
def render_note(obj: dict) -> str:
"""The teaching note, as Markdown — instructor-facing (the epilogue lives here)."""
if not obj:
return ""
lang = _lang(obj)
L = _L["note"][lang]
n = obj.get("teaching_note") or {}
out: list[str] = [f"# {L['title']}", ""]
def block(label_key: str, text):
if text:
out.append(f"## {L[label_key]}")
out.append(str(text).strip())
out.append("")
def bullets(label_key: str, items, ordered=False):
if items:
out.append(f"## {L[label_key]}")
for i, it in enumerate(items, 1):
out.append(f"{i}. {str(it).strip()}" if ordered else f"- {str(it).strip()}")
out.append("")
block("summary", n.get("summary"))
block("audience", n.get("audience"))
block("relevance", n.get("managerial_relevance"))
bullets("objectives", n.get("learning_objectives"), ordered=True)
block("sources", n.get("data_sources"))
bullets("anchor", n.get("theoretical_anchor"))
plan = n.get("discussion_plan") or []
if plan:
out.append(f"## {L['plan']}")
for b in plan:
mins = b.get("minutes")
head = f"**{b.get('block', '').strip()}**"
if mins:
head += f" — {mins} {L['min']}"
out.append(head)
if b.get("activity"):
out.append(f" {b['activity'].strip()}")
out.append("")
bullets("questions", n.get("discussion_questions"), ordered=True)
block("analysis", n.get("analysis"))
block("closure", n.get("closure"))
block("epilogue", n.get("epilogue"))
bullets("biblio", n.get("bibliography"))
return "\n".join(out).strip()
def quality_flags(obj: dict, errors: list[str], warnings: list[str]) -> dict[str, bool]:
"""Map the validator result to the four classroom quality checks the UI shows."""
note = (obj or {}).get("teaching_note") or {}
objs = note.get("learning_objectives") or []
leak = any("revelar a decisão" in w or "reveal" in w.lower() for w in warnings)
no_source = any("citar fonte" in w or "sourced" in w.lower() for w in warnings)
return {
"schema": not errors,
"noleak": not leak,
"objectives": 1 <= len(objs) <= 4,
"sourced": not no_source,
}
__all__ = ["render_case", "render_note", "quality_flags"]