eduvis 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- eduvis/__init__.py +16 -0
- eduvis/__main__.py +14 -0
- eduvis/cli.py +302 -0
- eduvis/core/__init__.py +15 -0
- eduvis/core/elements/__init__.py +5 -0
- eduvis/core/elements/generic.py +141 -0
- eduvis/core/elements/math.py +183 -0
- eduvis/core/export_schema.py +219 -0
- eduvis/core/prompt.py +252 -0
- eduvis/core/registry.py +325 -0
- eduvis/core/schemas/__init__.py +5 -0
- eduvis/core/schemas/actions.py +62 -0
- eduvis/core/schemas/placement.py +81 -0
- eduvis/core/schemas/progression.py +91 -0
- eduvis/core/schemas/relationships.py +52 -0
- eduvis/core/validator.py +256 -0
- eduvis/renderers/__init__.py +1 -0
- eduvis/renderers/svg/__init__.py +5 -0
- eduvis/renderers/svg/element_registry.py +287 -0
- eduvis/renderers/svg/primitives.py +150 -0
- eduvis/renderers/svg/renderers_base.py +1010 -0
- eduvis/renderers/svg/renderers_math/__init__.py +25 -0
- eduvis/renderers/svg/renderers_math/equation.py +207 -0
- eduvis/renderers/svg/renderers_math/fraction.py +238 -0
- eduvis/renderers/svg/renderers_math/geometry.py +341 -0
- eduvis/renderers/svg/renderers_math/graph.py +142 -0
- eduvis/renderers/svg/renderers_math/number.py +456 -0
- eduvis/renderers/svg/renderers_math/solid.py +539 -0
- eduvis/renderers/svg/spec_renderer.py +523 -0
- eduvis/schemas/actions.schema.json +47 -0
- eduvis/schemas/lesson.schema.json +68 -0
- eduvis/schemas/placement.schema.json +79 -0
- eduvis/schemas/progression.schema.json +86 -0
- eduvis/schemas/relationships.schema.json +52 -0
- eduvis-0.1.0.dist-info/METADATA +802 -0
- eduvis-0.1.0.dist-info/RECORD +40 -0
- eduvis-0.1.0.dist-info/WHEEL +5 -0
- eduvis-0.1.0.dist-info/entry_points.txt +2 -0
- eduvis-0.1.0.dist-info/licenses/LICENSE +179 -0
- eduvis-0.1.0.dist-info/top_level.txt +1 -0
eduvis/__init__.py
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
"""EduVis — educational content schema."""
|
|
2
|
+
|
|
3
|
+
__version__ = "0.1.0"
|
|
4
|
+
|
|
5
|
+
from .core import ElementRegistry, ElementSpec, FieldSpec, validate_lesson, format_prompt_docs, get_all_schemas
|
|
6
|
+
|
|
7
|
+
__all__ = [
|
|
8
|
+
"__version__",
|
|
9
|
+
"ElementRegistry",
|
|
10
|
+
"ElementSpec",
|
|
11
|
+
"FieldSpec",
|
|
12
|
+
"validate_lesson",
|
|
13
|
+
"format_prompt_docs",
|
|
14
|
+
"get_all_schemas",
|
|
15
|
+
]
|
|
16
|
+
|
eduvis/__main__.py
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import sys
|
|
2
|
+
from pathlib import Path
|
|
3
|
+
|
|
4
|
+
# Support running directly as a script without parent package context (e.g. `python eduvis/__main__.py` or `uv run eduvis`)
|
|
5
|
+
if not __package__:
|
|
6
|
+
_ROOT = Path(__file__).resolve().parent.parent
|
|
7
|
+
if str(_ROOT) not in sys.path:
|
|
8
|
+
sys.path.insert(0, str(_ROOT))
|
|
9
|
+
from eduvis.cli import cli
|
|
10
|
+
else:
|
|
11
|
+
from .cli import cli
|
|
12
|
+
|
|
13
|
+
if __name__ == "__main__":
|
|
14
|
+
cli()
|
eduvis/cli.py
ADDED
|
@@ -0,0 +1,302 @@
|
|
|
1
|
+
"""
|
|
2
|
+
EduVis CLI — validate and render EduVis lesson YAML files.
|
|
3
|
+
|
|
4
|
+
Usage:
|
|
5
|
+
python -m eduvis validate lesson.yaml
|
|
6
|
+
python -m eduvis render lesson.yaml -o slides/
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import sys
|
|
12
|
+
from pathlib import Path
|
|
13
|
+
|
|
14
|
+
import json
|
|
15
|
+
|
|
16
|
+
import click
|
|
17
|
+
import yaml
|
|
18
|
+
|
|
19
|
+
# ── EduVis Core import ────────────────────────────────────────────────────────
|
|
20
|
+
# Allow running from any directory: resolve the eduvis package root.
|
|
21
|
+
_PACKAGE_ROOT = Path(__file__).resolve().parent.parent # project root
|
|
22
|
+
if str(_PACKAGE_ROOT) not in sys.path:
|
|
23
|
+
sys.path.insert(0, str(_PACKAGE_ROOT))
|
|
24
|
+
|
|
25
|
+
from eduvis.core import validate_lesson, format_prompt_docs, get_all_schemas # noqa: E402
|
|
26
|
+
|
|
27
|
+
# ── EduVis SVG renderer ──────────────────────────────────────────────────────
|
|
28
|
+
|
|
29
|
+
# ── Constants ─────────────────────────────────────────────────────────────────
|
|
30
|
+
_EDUVIS_META = frozenset({"id", "placement", "actions", "relationships"})
|
|
31
|
+
|
|
32
|
+
# Layout zone mapping: EduVis layout_zone → svg_spec zone key.
|
|
33
|
+
_ZONE_MAP = {
|
|
34
|
+
"center": "center",
|
|
35
|
+
"left": "left",
|
|
36
|
+
"right": "right",
|
|
37
|
+
"full": "full",
|
|
38
|
+
"bottom": "bottom",
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
_LAYOUT_FOR_ZONE = {
|
|
42
|
+
"left": "two-column",
|
|
43
|
+
"right": "two-column",
|
|
44
|
+
"center": "visual + full-width",
|
|
45
|
+
"full": "header + full-width",
|
|
46
|
+
"bottom": "header + full-width",
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
# Phase → header background color + badge label.
|
|
50
|
+
# Colors are dark enough for white title text.
|
|
51
|
+
_PHASE_STYLE: dict[str, dict] = {
|
|
52
|
+
"hook": {"color": "#BF360C", "label": "HOOK"},
|
|
53
|
+
"explore": {"color": "#00695C", "label": "EXPLORE"},
|
|
54
|
+
"explain": {"color": "#1565C0", "label": "EXPLAIN"},
|
|
55
|
+
"guided_practice": {"color": "#4A148C", "label": "GUIDED"},
|
|
56
|
+
"independent_practice": {"color": "#1B5E20", "label": "PRACTICE"},
|
|
57
|
+
"challenge": {"color": "#B71C1C", "label": "CHALLENGE"},
|
|
58
|
+
"reflect": {"color": "#37474F", "label": "REFLECT"},
|
|
59
|
+
"recall": {"color": "#4E342E", "label": "RECALL"},
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
# Difficulty overrides for independent_practice (color + label).
|
|
63
|
+
_DIFFICULTY_STYLE: dict[str, dict] = {
|
|
64
|
+
"starter": {"color": "#1B5E20", "label": "STARTER"},
|
|
65
|
+
"routine": {"color": "#004D40", "label": "ROUTINE"},
|
|
66
|
+
"challenge": {"color": "#B71C1C", "label": "CHALLENGE"},
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
# Memory role → accent color for the divider line and dot.
|
|
70
|
+
_ROLE_COLOR: dict[str, str] = {
|
|
71
|
+
"anchor": "#FFD700",
|
|
72
|
+
"example": "#00BCD4",
|
|
73
|
+
"practice": "#66BB6A",
|
|
74
|
+
"misconception_fix":"#EF5350",
|
|
75
|
+
"retrieval": "#FFA726",
|
|
76
|
+
"review": "#9E9E9E",
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
# ── Bridge helpers ────────────────────────────────────────────────────────────
|
|
81
|
+
|
|
82
|
+
def _element_to_spec(element: dict) -> dict:
|
|
83
|
+
"""Strip EduVis meta-fields; return only fields the SVG renderer needs."""
|
|
84
|
+
return {k: v for k, v in element.items() if k not in _EDUVIS_META}
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def _element_title(element: dict) -> str:
|
|
88
|
+
"""Build a human-readable slide title from an element's id and lesson_phase."""
|
|
89
|
+
eid = element.get("id", "")
|
|
90
|
+
placement = element.get("placement") or {}
|
|
91
|
+
phase = placement.get("lesson_phase", "")
|
|
92
|
+
if phase and eid:
|
|
93
|
+
return f"{phase.replace('_', ' ').title()}: {eid.replace('_', ' ')}"
|
|
94
|
+
return (eid or "slide").replace("_", " ").title()
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def _build_svg_spec_yaml(element: dict) -> str:
|
|
98
|
+
"""Convert one EduVis element dict to an svg_spec YAML string for rendering."""
|
|
99
|
+
placement = element.get("placement") or {}
|
|
100
|
+
zone = _ZONE_MAP.get(placement.get("layout_zone", "full"), "full")
|
|
101
|
+
layout = _LAYOUT_FOR_ZONE.get(zone, "header + full-width")
|
|
102
|
+
|
|
103
|
+
phase = placement.get("lesson_phase", "")
|
|
104
|
+
difficulty = placement.get("difficulty", "")
|
|
105
|
+
memory_role = placement.get("memory_role", "")
|
|
106
|
+
|
|
107
|
+
phase_style = _PHASE_STYLE.get(phase, {})
|
|
108
|
+
header_color = phase_style.get("color", "#111111")
|
|
109
|
+
phase_label = phase_style.get("label", "")
|
|
110
|
+
|
|
111
|
+
# Differentiate starter / routine within independent_practice
|
|
112
|
+
if phase == "independent_practice" and difficulty in _DIFFICULTY_STYLE:
|
|
113
|
+
diff_style = _DIFFICULTY_STYLE[difficulty]
|
|
114
|
+
header_color = diff_style["color"]
|
|
115
|
+
phase_label = diff_style["label"]
|
|
116
|
+
|
|
117
|
+
role_color = _ROLE_COLOR.get(memory_role, "")
|
|
118
|
+
|
|
119
|
+
spec_dict = {
|
|
120
|
+
"layout": layout,
|
|
121
|
+
"header_color": header_color,
|
|
122
|
+
"phase_label": phase_label,
|
|
123
|
+
"role_color": role_color,
|
|
124
|
+
"memory_role": memory_role,
|
|
125
|
+
"zones": {zone: [_element_to_spec(element)]},
|
|
126
|
+
}
|
|
127
|
+
return yaml.dump(spec_dict, allow_unicode=True, default_flow_style=False)
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
def _load_renderer():
|
|
131
|
+
"""Load the standalone EduVis SVG renderer."""
|
|
132
|
+
try:
|
|
133
|
+
from .renderers.svg import SVGSpecRenderer # noqa: PLC0415
|
|
134
|
+
return SVGSpecRenderer()
|
|
135
|
+
except ImportError as exc:
|
|
136
|
+
raise click.ClickException(
|
|
137
|
+
f"Cannot import EduVis SVG renderer: {exc}"
|
|
138
|
+
) from exc
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
# ── CLI ───────────────────────────────────────────────────────────────────────
|
|
142
|
+
|
|
143
|
+
@click.group()
|
|
144
|
+
def cli() -> None:
|
|
145
|
+
"""EduVis - educational content schema tools."""
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
@cli.command()
|
|
149
|
+
@click.option(
|
|
150
|
+
"--subjects",
|
|
151
|
+
default="math",
|
|
152
|
+
show_default=True,
|
|
153
|
+
help="Comma-separated subject tags to include (e.g. math,science). Use '*' for all.",
|
|
154
|
+
)
|
|
155
|
+
@click.option(
|
|
156
|
+
"--output", "-o",
|
|
157
|
+
default=None,
|
|
158
|
+
help="Write vocabulary to this file instead of stdout.",
|
|
159
|
+
)
|
|
160
|
+
def docs(subjects: str, output: str | None) -> None:
|
|
161
|
+
"""Print the full EduVis vocabulary for use in an LLM system prompt."""
|
|
162
|
+
subject_list = [s.strip() for s in subjects.split(",")]
|
|
163
|
+
vocab = format_prompt_docs(subject_list)
|
|
164
|
+
if output:
|
|
165
|
+
Path(output).write_text(vocab, encoding="utf-8")
|
|
166
|
+
click.echo(f"Vocabulary written to {output}")
|
|
167
|
+
else:
|
|
168
|
+
click.echo(vocab)
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
@cli.command()
|
|
172
|
+
@click.option(
|
|
173
|
+
"-o", "--output",
|
|
174
|
+
default="schemas",
|
|
175
|
+
show_default=True,
|
|
176
|
+
help="Directory to write JSON Schema files into.",
|
|
177
|
+
)
|
|
178
|
+
def schema(output: str) -> None:
|
|
179
|
+
"""Export all EduVis JSON Schemas to a directory."""
|
|
180
|
+
out_dir = Path(output)
|
|
181
|
+
out_dir.mkdir(parents=True, exist_ok=True)
|
|
182
|
+
|
|
183
|
+
schemas = get_all_schemas()
|
|
184
|
+
for name, schema_dict in schemas.items():
|
|
185
|
+
out_path = out_dir / f"{name}.schema.json"
|
|
186
|
+
out_path.write_text(json.dumps(schema_dict, indent=2), encoding="utf-8")
|
|
187
|
+
click.echo(f" OK {out_path}")
|
|
188
|
+
|
|
189
|
+
click.secho(f"\nDone -- {len(schemas)} schema(s) written to {out_dir}/", fg="green")
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
@cli.command()
|
|
193
|
+
@click.argument("lesson_file", type=click.Path(exists=True, dir_okay=False))
|
|
194
|
+
def validate(lesson_file: str) -> None:
|
|
195
|
+
"""Validate an EduVis lesson YAML file against all five pillars."""
|
|
196
|
+
with open(lesson_file, encoding="utf-8") as f:
|
|
197
|
+
try:
|
|
198
|
+
doc = yaml.safe_load(f)
|
|
199
|
+
except yaml.YAMLError as exc:
|
|
200
|
+
raise click.ClickException(f"YAML parse error: {exc}") from exc
|
|
201
|
+
|
|
202
|
+
if not isinstance(doc, dict):
|
|
203
|
+
raise click.ClickException("Lesson file must be a YAML mapping at the top level.")
|
|
204
|
+
|
|
205
|
+
warnings = validate_lesson(doc)
|
|
206
|
+
|
|
207
|
+
if not warnings:
|
|
208
|
+
click.secho(f"OK {lesson_file} -- valid, no warnings", fg="green")
|
|
209
|
+
else:
|
|
210
|
+
click.secho(f"WARN {lesson_file} -- {len(warnings)} warning(s):", fg="yellow")
|
|
211
|
+
for w in warnings:
|
|
212
|
+
click.echo(f" {w}")
|
|
213
|
+
sys.exit(1)
|
|
214
|
+
|
|
215
|
+
|
|
216
|
+
@cli.command()
|
|
217
|
+
@click.argument("lesson_file", type=click.Path(exists=True, dir_okay=False))
|
|
218
|
+
@click.option(
|
|
219
|
+
"-o", "--output",
|
|
220
|
+
default="output",
|
|
221
|
+
show_default=True,
|
|
222
|
+
help="Directory to write SVG files into.",
|
|
223
|
+
)
|
|
224
|
+
@click.option(
|
|
225
|
+
"--posting-group",
|
|
226
|
+
default="G1",
|
|
227
|
+
show_default=True,
|
|
228
|
+
help="Grade level for font sizing (G1, G2, or G3).",
|
|
229
|
+
)
|
|
230
|
+
@click.option(
|
|
231
|
+
"--skip-validation",
|
|
232
|
+
is_flag=True,
|
|
233
|
+
default=False,
|
|
234
|
+
help="Skip EduVis validation before rendering.",
|
|
235
|
+
)
|
|
236
|
+
def render(lesson_file: str, output: str, posting_group: str, skip_validation: bool) -> None:
|
|
237
|
+
"""Render an EduVis lesson YAML to one SVG per element."""
|
|
238
|
+
with open(lesson_file, encoding="utf-8") as f:
|
|
239
|
+
try:
|
|
240
|
+
doc = yaml.safe_load(f)
|
|
241
|
+
except yaml.YAMLError as exc:
|
|
242
|
+
raise click.ClickException(f"YAML parse error: {exc}") from exc
|
|
243
|
+
|
|
244
|
+
if not isinstance(doc, dict):
|
|
245
|
+
raise click.ClickException("Lesson file must be a YAML mapping at the top level.")
|
|
246
|
+
|
|
247
|
+
# ── Validate first ────────────────────────────────────────────────────────
|
|
248
|
+
if not skip_validation:
|
|
249
|
+
warnings = validate_lesson(doc)
|
|
250
|
+
if warnings:
|
|
251
|
+
click.secho(f"WARN {len(warnings)} validation warning(s):", fg="yellow")
|
|
252
|
+
for w in warnings:
|
|
253
|
+
click.echo(f" {w}")
|
|
254
|
+
click.echo()
|
|
255
|
+
|
|
256
|
+
# ── Load renderer ─────────────────────────────────────────────────────────
|
|
257
|
+
renderer = _load_renderer()
|
|
258
|
+
|
|
259
|
+
# ── Render ────────────────────────────────────────────────────────────────
|
|
260
|
+
content = doc.get("content")
|
|
261
|
+
if not isinstance(content, list) or not content:
|
|
262
|
+
raise click.ClickException("No 'content' list found in the lesson file.")
|
|
263
|
+
|
|
264
|
+
out_dir = Path(output)
|
|
265
|
+
out_dir.mkdir(parents=True, exist_ok=True)
|
|
266
|
+
|
|
267
|
+
lesson_title = (doc.get("lesson") or {}).get("title", "")
|
|
268
|
+
rendered = 0
|
|
269
|
+
skipped = 0
|
|
270
|
+
|
|
271
|
+
for element in content:
|
|
272
|
+
if not isinstance(element, dict):
|
|
273
|
+
continue
|
|
274
|
+
|
|
275
|
+
element_id = element.get("id", f"element_{rendered + skipped + 1}")
|
|
276
|
+
title = f"{lesson_title} — {_element_title(element)}" if lesson_title else _element_title(element)
|
|
277
|
+
|
|
278
|
+
svg_spec_yaml = _build_svg_spec_yaml(element)
|
|
279
|
+
|
|
280
|
+
try:
|
|
281
|
+
svg_text = renderer.render(
|
|
282
|
+
svg_spec_yaml,
|
|
283
|
+
title=title,
|
|
284
|
+
posting_group=posting_group,
|
|
285
|
+
)
|
|
286
|
+
except Exception as exc:
|
|
287
|
+
click.secho(f" FAIL {element_id}: render error -- {exc}", fg="red")
|
|
288
|
+
skipped += 1
|
|
289
|
+
continue
|
|
290
|
+
|
|
291
|
+
out_path = out_dir / f"{element_id}.svg"
|
|
292
|
+
out_path.write_text(svg_text, encoding="utf-8")
|
|
293
|
+
click.echo(f" OK {out_path}")
|
|
294
|
+
rendered += 1
|
|
295
|
+
|
|
296
|
+
click.echo()
|
|
297
|
+
click.secho(
|
|
298
|
+
f"Done -- {rendered} SVG(s) written to {out_dir}/",
|
|
299
|
+
fg="green" if not skipped else "yellow",
|
|
300
|
+
)
|
|
301
|
+
if skipped:
|
|
302
|
+
click.secho(f" {skipped} element(s) skipped due to render errors.", fg="yellow")
|
eduvis/core/__init__.py
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
"""
|
|
2
|
+
EduVis Core — renderer-agnostic educational content schema.
|
|
3
|
+
|
|
4
|
+
Public API:
|
|
5
|
+
ElementRegistry — query element specs, generate prompt docs, validate fields
|
|
6
|
+
validate_lesson() — validate a complete lesson document (all five pillars)
|
|
7
|
+
format_prompt_docs() — full five-pillar vocabulary for LLM system prompts
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from .registry import ElementRegistry, ElementSpec, FieldSpec
|
|
11
|
+
from .validator import validate_lesson
|
|
12
|
+
from .prompt import format_prompt_docs
|
|
13
|
+
from .export_schema import get_all_schemas
|
|
14
|
+
|
|
15
|
+
__all__ = ["ElementRegistry", "ElementSpec", "FieldSpec", "validate_lesson", "format_prompt_docs", "get_all_schemas"]
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
"""Generic element specs — available for all subjects."""
|
|
2
|
+
|
|
3
|
+
from ..registry import ElementSpec, FieldSpec
|
|
4
|
+
|
|
5
|
+
ELEMENT_SPECS: list[ElementSpec] = [
|
|
6
|
+
ElementSpec(
|
|
7
|
+
name="text_list",
|
|
8
|
+
subjects=["*"],
|
|
9
|
+
synopsis="items: [strings] (optional color per item)",
|
|
10
|
+
fields=[
|
|
11
|
+
FieldSpec("items", type="array", required=True,
|
|
12
|
+
description="List of bullet strings; prefix 'no-bullet:' to suppress bullet"),
|
|
13
|
+
FieldSpec("color", type="color", required=False,
|
|
14
|
+
description="Override bullet color for all items"),
|
|
15
|
+
FieldSpec("anchor", type="string", required=False, default="start",
|
|
16
|
+
enum=["start", "middle", "center"],
|
|
17
|
+
description="Text alignment"),
|
|
18
|
+
],
|
|
19
|
+
),
|
|
20
|
+
ElementSpec(
|
|
21
|
+
name="fact_boxes",
|
|
22
|
+
subjects=["*"],
|
|
23
|
+
synopsis="items: [{text, border_color}]",
|
|
24
|
+
fields=[
|
|
25
|
+
FieldSpec("items", type="array", required=True,
|
|
26
|
+
description="List of fact box dicts",
|
|
27
|
+
items=FieldSpec("item", type="object", properties=[
|
|
28
|
+
FieldSpec("text", type="string", description="Fact box content"),
|
|
29
|
+
FieldSpec("border_color", type="color", required=False,
|
|
30
|
+
description="Box border colour"),
|
|
31
|
+
])),
|
|
32
|
+
],
|
|
33
|
+
),
|
|
34
|
+
ElementSpec(
|
|
35
|
+
name="example_panel",
|
|
36
|
+
subjects=["*"],
|
|
37
|
+
synopsis="items: [{heading, body}] — side-by-side comparison panels",
|
|
38
|
+
fields=[
|
|
39
|
+
FieldSpec("items", type="array", required=True,
|
|
40
|
+
description="List of panel dicts (max 3 for readability)",
|
|
41
|
+
items=FieldSpec("item", type="object", properties=[
|
|
42
|
+
FieldSpec("heading", type="string", description="Bold panel title"),
|
|
43
|
+
FieldSpec("body", type="string",
|
|
44
|
+
description="Panel body; use \\n for explicit line breaks"),
|
|
45
|
+
])),
|
|
46
|
+
],
|
|
47
|
+
),
|
|
48
|
+
ElementSpec(
|
|
49
|
+
name="callout_box",
|
|
50
|
+
subjects=["*"],
|
|
51
|
+
synopsis="title, lines: [strings], border_color — highlighted callout",
|
|
52
|
+
fields=[
|
|
53
|
+
FieldSpec("title", type="string", required=False, description="Bold callout heading"),
|
|
54
|
+
FieldSpec("lines", type="array", required=True, description="Body text lines"),
|
|
55
|
+
FieldSpec("border_color", type="color", required=False, default="cyan"),
|
|
56
|
+
],
|
|
57
|
+
),
|
|
58
|
+
ElementSpec(
|
|
59
|
+
name="summary_list",
|
|
60
|
+
subjects=["*"],
|
|
61
|
+
synopsis="items: [strings] — identical to text_list, use on summary/takeaway slides",
|
|
62
|
+
fields=[
|
|
63
|
+
FieldSpec("items", type="array", required=True, description="Summary bullet strings"),
|
|
64
|
+
],
|
|
65
|
+
notes=["PREFER summary_list on final slides to signal lesson wrap-up."],
|
|
66
|
+
),
|
|
67
|
+
ElementSpec(
|
|
68
|
+
name="multiple_choice",
|
|
69
|
+
subjects=["*"],
|
|
70
|
+
synopsis="question: string, options: {A, B, C, D} — MCQ layout",
|
|
71
|
+
fields=[
|
|
72
|
+
FieldSpec("question", type="string", required=True, description="The MCQ stem"),
|
|
73
|
+
FieldSpec("options", type="object", required=True,
|
|
74
|
+
description="Exactly four options keyed A–D",
|
|
75
|
+
properties=[
|
|
76
|
+
FieldSpec("A", type="string"),
|
|
77
|
+
FieldSpec("B", type="string"),
|
|
78
|
+
FieldSpec("C", type="string"),
|
|
79
|
+
FieldSpec("D", type="string"),
|
|
80
|
+
]),
|
|
81
|
+
],
|
|
82
|
+
),
|
|
83
|
+
ElementSpec(
|
|
84
|
+
name="hint_list",
|
|
85
|
+
subjects=["*"],
|
|
86
|
+
synopsis="items: [strings], final: string — numbered hints",
|
|
87
|
+
fields=[
|
|
88
|
+
FieldSpec("items", type="array", required=True,
|
|
89
|
+
description="Hint steps (auto-numbered unless item starts with a digit or 'Step')"),
|
|
90
|
+
FieldSpec("final", type="string", required=False,
|
|
91
|
+
description="Confirmation method shown in a box at the bottom"),
|
|
92
|
+
],
|
|
93
|
+
),
|
|
94
|
+
ElementSpec(
|
|
95
|
+
name="number_line",
|
|
96
|
+
subjects=["*"],
|
|
97
|
+
synopsis="range: [min, max], highlight: [{value, label, color}] — annotated number line",
|
|
98
|
+
fields=[
|
|
99
|
+
FieldSpec("range", type="array", required=True,
|
|
100
|
+
description="[min, max] numeric bounds"),
|
|
101
|
+
FieldSpec("highlight", type="array", required=False,
|
|
102
|
+
description="List of {value, label, color} highlight markers",
|
|
103
|
+
items=FieldSpec("hl", type="object", properties=[
|
|
104
|
+
FieldSpec("value", type="number", description="Position on the line"),
|
|
105
|
+
FieldSpec("label", type="string", required=False),
|
|
106
|
+
FieldSpec("color", type="color", required=False),
|
|
107
|
+
FieldSpec("type", type="string", required=False,
|
|
108
|
+
enum=["jump"],
|
|
109
|
+
description="'jump' draws a curved hop arrow"),
|
|
110
|
+
])),
|
|
111
|
+
FieldSpec("direction_labels", type="object", required=False,
|
|
112
|
+
description="{left: 'Smaller', right: 'Larger'} axis end labels",
|
|
113
|
+
properties=[
|
|
114
|
+
FieldSpec("left", type="string", required=False),
|
|
115
|
+
FieldSpec("right", type="string", required=False),
|
|
116
|
+
]),
|
|
117
|
+
FieldSpec("caption", type="string", required=False,
|
|
118
|
+
description="Title above the line"),
|
|
119
|
+
],
|
|
120
|
+
),
|
|
121
|
+
ElementSpec(
|
|
122
|
+
name="mixed_card",
|
|
123
|
+
subjects=["*"],
|
|
124
|
+
synopsis="ribbon_type: solve|remember|review, ribbon_label: string, items: [{type: text|math_grid, ...}] — mixed card",
|
|
125
|
+
fields=[
|
|
126
|
+
FieldSpec("ribbon_type", type="string", required=False, default="solve",
|
|
127
|
+
enum=["solve", "remember", "review"]),
|
|
128
|
+
FieldSpec("ribbon_label", type="string", required=False),
|
|
129
|
+
FieldSpec("items", type="array", required=True,
|
|
130
|
+
description="List of sub-elements to render within the card",
|
|
131
|
+
items=FieldSpec("item", type="object", properties=[
|
|
132
|
+
FieldSpec("type", type="string", required=True, enum=["text", "math_grid"]),
|
|
133
|
+
FieldSpec("lines", type="array", required=False),
|
|
134
|
+
FieldSpec("mode", type="string", required=False),
|
|
135
|
+
FieldSpec("rows", type="array", required=False),
|
|
136
|
+
FieldSpec("headers", type="array", required=False),
|
|
137
|
+
FieldSpec("row_colors", type="array", required=False),
|
|
138
|
+
])),
|
|
139
|
+
],
|
|
140
|
+
),
|
|
141
|
+
]
|