Spaces:
Running on Zero
Running on Zero
File size: 6,470 Bytes
f8d4986 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 | """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"]
|