marimo-team/marimo-learn
3
1# /// script2# requires-python = "==3.12"3# dependencies = [4# "anywidget",5# "marimo",6# "marimo-learn==0.13.0",7# ]8# ///9 10"""11Example Marimo Notebook: Education Widgets Demo12 13This notebook demonstrates all formative assessment widgets.14Run with: marimo edit demo.py15"""16 17import marimo18 19__generated_with = "0.23.1"20app = marimo.App()21 22 23@app.cell24def _():25 import marimo as mo26 from marimo_learn import (27 ConceptMapWidget,28 FlashcardWidget,29 LabelingWidget,30 MatchingWidget,31 MultipleChoiceWidget,32 NumericEntryWidget,33 OrderingWidget,34 PredictThenCheckWidget,35 )36 37 return (38 ConceptMapWidget,39 FlashcardWidget,40 LabelingWidget,41 MatchingWidget,42 MultipleChoiceWidget,43 NumericEntryWidget,44 OrderingWidget,45 PredictThenCheckWidget,46 mo,47 )48 49 50@app.cell51def _(mo):52 mo.md("""53 # Formative Assessment Widgets54 55 This notebook demonstrates several widgets from the [marimo-learn](https://pypi.org/project/marimo-learn/) package that you can use to embed formative assessments in your lessons. They also show how easy it is to build custom plugins for the marimo notebook to meet your teaching needs.56 """)57 return58 59 60@app.cell61def _(mo):62 mo.md("""63 ## Concept Map64 """)65 return66 67 68@app.cell69def _(ConceptMapWidget, mo):70 concept_map = mo.ui.anywidget(71 ConceptMapWidget(72 question="Map the relationships between these Python concepts:",73 concepts=["function", "parameter", "argument", "return value", "call site"],74 terms=["defines", "accepts", "supplies", "produces", "invokes"],75 correct_edges=[76 {"from": "function", "to": "parameter", "label": "accepts"},77 {"from": "function", "to": "return value", "label": "produces"},78 {"from": "call site", "to": "argument", "label": "supplies"},79 {"from": "argument", "to": "parameter", "label": "defines"},80 {"from": "call site", "to": "function", "label": "invokes"},81 ],82 )83 )84 concept_map85 return (concept_map,)86 87 88@app.cell89def _(concept_map, mo):90 def concept_map_msg(widget):91 val = widget.value.get("value") or {}92 score = val.get("score")93 total = val.get("total", 5)94 if score is None:95 return "Draw connections between concepts to see your score."96 msg = f"**{score}/{total}** correct connection{'s' if total != 1 else ''}"97 if val.get("correct"):98 msg += " — complete!"99 return msg100 101 mo.md(concept_map_msg(concept_map))102 return103 104 105@app.cell106def _(mo):107 mo.md("""108 ## Flashcard Deck109 """)110 return111 112 113@app.cell114def _(FlashcardWidget, mo):115 flashcard_deck = mo.ui.anywidget(116 FlashcardWidget(117 question="Python Concepts — rate yourself on each card:",118 cards=[119 {120 "front": "What does a list comprehension look like?",121 "back": "[expr for item in iterable if condition] — e.g., [x**2 for x in range(10) if x % 2 == 0]",122 },123 {124 "front": "What is the difference between a list and a tuple?",125 "back": "Lists are mutable (can be changed after creation); tuples are immutable (cannot be changed).",126 },127 {128 "front": "What does the `*args` parameter do in a function definition?",129 "back": "It collects any number of positional arguments into a tuple named `args`.",130 },131 {132 "front": "What is a Python generator?",133 "back": "A function that uses `yield` to produce values one at a time, pausing between each, without building the full sequence in memory.",134 },135 {136 "front": "What does `if __name__ == '__main__':` do?",137 "back": "It runs the indented code only when the file is executed directly, not when it is imported as a module.",138 },139 {140 "front": "What is the difference between `is` and `==` in Python?",141 "back": "`==` tests value equality; `is` tests identity (whether two names refer to the exact same object in memory).",142 },143 ],144 shuffle=True,145 )146 )147 flashcard_deck148 return (flashcard_deck,)149 150 151@app.cell152def _(flashcard_deck, mo):153 def flashcard_progress(widget):154 val = widget.value.get("value") or {}155 results = val.get("results", {})156 counts = {"got_it": 0, "almost": 0, "no": 0}157 for r in results.values():158 counts[r["rating"]] = counts.get(r["rating"], 0) + 1159 return len(results), counts, val.get("complete", False)160 161 _rated, _counts, _complete = flashcard_progress(flashcard_deck)162 mo.md(f"""163 **Progress:** {_rated} card(s) rated —164 ✓ Got it: {_counts["got_it"]} 165 ~ Almost: {_counts["almost"]} 166 ✗ No: {_counts["no"]}167 {" 🎉 Deck complete!" if _complete else ""}168 """)169 return170 171 172@app.cell173def _(mo):174 mo.md("""175 ## Labeling Question176 """)177 return178 179 180@app.cell181def _(LabelingWidget, mo):182 labeling_question = mo.ui.anywidget(183 LabelingWidget(184 question="Label the parts of this Python code:",185 labels=[186 "Variable declaration",187 "Function call",188 "String literal",189 "Arithmetic operation",190 ],191 text_lines=[192 "name = 'Alice'",193 "age = 25",194 "result = age + 5",195 "print(name)",196 ],197 correct_labels={198 0: [0, 2], # Line 0: labels 0 and 2199 1: [0], # Line 1: label 0200 2: [0, 3], # Line 2: labels 0 and 3201 3: [1], # Line 3: label 1202 },203 )204 )205 labeling_question206 return (labeling_question,)207 208 209@app.cell210def _(labeling_question, mo):211 _val = labeling_question.value.get("value") or {}212 _total = _val.get("total", 0)213 _msg = f"Score: {_val['score']}/{_total}" if _total > 0 else "Not submitted yet"214 mo.md(f"**{_msg}**")215 return216 217 218@app.cell219def _(mo):220 mo.md("""221 ## Matching Question222 """)223 return224 225 226@app.cell227def _(MatchingWidget, mo):228 matching_question = mo.ui.anywidget(229 MatchingWidget(230 question="Match the programming languages to their primary paradigms:",231 left=["Python", "Haskell", "C", "SQL"],232 right=["Functional", "Procedural", "Multi-paradigm", "Declarative"],233 correct_matches={0: 2, 1: 0, 2: 1, 3: 3},234 )235 )236 matching_question237 return (matching_question,)238 239 240@app.cell241def _(matching_question, mo):242 _val = matching_question.value.get("value") or {}243 _msg = f"Score: {_val['score']}/{_val['total']}" if _val else "Not answered yet"244 mo.md(f"**{_msg}**")245 return246 247 248@app.cell249def _(mo):250 mo.md("""251 ## Multiple Choice Question252 """)253 return254 255 256@app.cell257def _(MultipleChoiceWidget, mo):258 multiple_choice_question = mo.ui.anywidget(259 MultipleChoiceWidget(260 question="What is the capital of France?",261 options=["London", "Berlin", "Paris", "Madrid"],262 correct_answer=2,263 explanation="Paris has been the capital of France since the 12th century.",264 )265 )266 multiple_choice_question267 return (multiple_choice_question,)268 269 270@app.cell271def _(mo, multiple_choice_question):272 _val = multiple_choice_question.value.get("value") or {}273 if _val.get("answered"):274 _msg = "Score: 1/1" if _val.get("correct") else "Score: 0/1"275 else:276 _msg = "Not answered yet"277 mo.md(f"**{_msg}**")278 return279 280 281@app.cell282def _(mo):283 mo.md("""284 ## Ordering Question285 """)286 return287 288 289@app.cell290def _(OrderingWidget, mo):291 ordering_question = mo.ui.anywidget(292 OrderingWidget(293 question="Arrange these steps of the scientific method in the correct order:",294 items=[295 "Ask a question",296 "Do background research",297 "Construct a hypothesis",298 "Test with an experiment",299 "Analyze data",300 "Draw conclusions",301 ],302 shuffle=True,303 )304 )305 ordering_question306 return (ordering_question,)307 308 309@app.cell310def _(mo, ordering_question):311 _val = ordering_question.value.get("value") or {}312 if _val.get("correct") is not None and _val.get("order"):313 _msg = "Score: 1/1" if _val.get("correct") else "Score: 0/1"314 else:315 _msg = "Not answered yet"316 mo.md(f"**{_msg}**")317 return318 319 320@app.cell321def _(mo):322 mo.md("""323 ## Numeric Entry Question324 """)325 return326 327 328@app.cell329def _(NumericEntryWidget, mo):330 numeric_entry_question = mo.ui.anywidget(331 NumericEntryWidget(332 question="How many bits are in one byte?",333 correct_answer=8,334 tolerance=0.5,335 explanation="A byte consists of exactly 8 bits.",336 )337 )338 numeric_entry_question339 return (numeric_entry_question,)340 341 342@app.cell343def _(mo, numeric_entry_question):344 _val = numeric_entry_question.value.get("value") or {}345 if _val.get("answered"):346 _msg = "Score: 1/1" if _val.get("ok") else "Score: 0/1"347 else:348 _msg = "Not answered yet"349 mo.md(f"**{_msg}**")350 return351 352 353@app.cell354def _(mo):355 mo.md("""356 ## Predict-Then-Check Question357 """)358 return359 360 361@app.cell362def _(PredictThenCheckWidget, mo):363 predict_then_check_question = mo.ui.anywidget(364 PredictThenCheckWidget(365 question="What does this Python code print?",366 code='words = ["one", "two", "three"]\nprint(len(words))',367 output="3",368 options=["2", "3", "['one', 'two', 'three']", "None"],369 correct_answer=1,370 explanations=[371 "Wrong: len() counts all items; the list has three elements.",372 "Correct: len() returns the number of items, which is 3.",373 "Wrong: len() returns an integer count, not the list itself.",374 "Wrong: len() always returns an integer, never None.",375 ],376 )377 )378 predict_then_check_question379 return (predict_then_check_question,)380 381 382@app.cell383def _(mo, predict_then_check_question):384 _val = predict_then_check_question.value.get("value") or {}385 if _val.get("answered"):386 _msg = "Score: 1/1" if _val.get("correct") else "Score: 0/1"387 else:388 _msg = "Not answered yet"389 mo.md(f"**{_msg}**")390 return391 392 393@app.cell394def _(mo):395 mo.md("""396 ---397 398 ## Quiz Results Summary399 400 You can access the values from all widgets to create a summary or scoring system.401 """)402 return403 404 405@app.cell406def _(407 concept_map,408 flashcard_deck,409 matching_question,410 mo,411 multiple_choice_question,412 numeric_entry_question,413 ordering_question,414 predict_then_check_question,415):416 def widget_val(widget):417 return widget.value.get("value") or {}418 419 def calculate_score():420 score = 0421 total = 0422 for val, answered_key, correct_key in [423 (widget_val(concept_map), "score", "correct"),424 (widget_val(matching_question), "score", "correct"),425 (widget_val(multiple_choice_question), "answered", "correct"),426 (widget_val(ordering_question), "order", "correct"),427 (widget_val(numeric_entry_question), "answered", "ok"),428 (widget_val(predict_then_check_question), "answered", "correct"),429 ]:430 if val.get(answered_key) is not None and val.get(answered_key) is not False:431 total += 1432 if val.get(correct_key):433 score += 1434 fc = widget_val(flashcard_deck)435 if fc.get("results"):436 total += 1437 if fc.get("complete"):438 score += 1439 return score, total440 441 score, total = calculate_score()442 mo.md(f"""443 ### Current Score: {score}/{total}444 445 {"🎉 Perfect score!" if score == total and total > 0 else "Keep going!" if total > 0 else "Answer the questions above to see your score."}446 """)447 return448 449 450if __name__ == "__main__":451 app.run()452 