Spaces:
Running on Zero
Running on Zero
| """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"] | |