rupaka 0.0.1__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.
Files changed (102) hide show
  1. rupaka/__init__.py +1 -0
  2. rupaka/_modidx.py +115 -0
  3. rupaka/agent.py +124 -0
  4. rupaka/blocks/axis/axis.css +4 -0
  5. rupaka/blocks/axis/axis.js +31 -0
  6. rupaka/blocks/axis/block.json +18 -0
  7. rupaka/blocks/axis/example.json +12 -0
  8. rupaka/blocks/bars/bars.css +8 -0
  9. rupaka/blocks/bars/bars.js +24 -0
  10. rupaka/blocks/bars/block.json +16 -0
  11. rupaka/blocks/bars/example.json +9 -0
  12. rupaka/blocks/caption/block.json +11 -0
  13. rupaka/blocks/caption/caption.css +2 -0
  14. rupaka/blocks/caption/caption.js +6 -0
  15. rupaka/blocks/caption/example.json +9 -0
  16. rupaka/blocks/choice/block.json +13 -0
  17. rupaka/blocks/choice/choice.css +2 -0
  18. rupaka/blocks/choice/choice.js +20 -0
  19. rupaka/blocks/choice/example.json +11 -0
  20. rupaka/blocks/gate/block.json +17 -0
  21. rupaka/blocks/gate/example.json +10 -0
  22. rupaka/blocks/gate/gate.css +14 -0
  23. rupaka/blocks/gate/gate.js +61 -0
  24. rupaka/blocks/grid/block.json +23 -0
  25. rupaka/blocks/grid/example.json +9 -0
  26. rupaka/blocks/grid/grid.css +8 -0
  27. rupaka/blocks/grid/grid.js +45 -0
  28. rupaka/blocks/plot/block.json +13 -0
  29. rupaka/blocks/plot/example.json +11 -0
  30. rupaka/blocks/plot/plot.css +2 -0
  31. rupaka/blocks/plot/plot.js +18 -0
  32. rupaka/blocks/point/block.json +19 -0
  33. rupaka/blocks/point/example.json +11 -0
  34. rupaka/blocks/point/point.css +7 -0
  35. rupaka/blocks/point/point.js +36 -0
  36. rupaka/blocks/ranklist/block.json +14 -0
  37. rupaka/blocks/ranklist/example.json +9 -0
  38. rupaka/blocks/ranklist/ranklist.css +13 -0
  39. rupaka/blocks/ranklist/ranklist.js +42 -0
  40. rupaka/blocks/readout/block.json +11 -0
  41. rupaka/blocks/readout/example.json +9 -0
  42. rupaka/blocks/readout/readout.css +1 -0
  43. rupaka/blocks/readout/readout.js +6 -0
  44. rupaka/blocks/row/block.json +10 -0
  45. rupaka/blocks/row/example.json +12 -0
  46. rupaka/blocks/row/row.css +2 -0
  47. rupaka/blocks/row/row.js +2 -0
  48. rupaka/blocks/slider/block.json +18 -0
  49. rupaka/blocks/slider/example.json +10 -0
  50. rupaka/blocks/slider/slider.css +3 -0
  51. rupaka/blocks/slider/slider.js +19 -0
  52. rupaka/blocks/stack/block.json +10 -0
  53. rupaka/blocks/stack/example.json +11 -0
  54. rupaka/blocks/stack/stack.css +2 -0
  55. rupaka/blocks/stack/stack.js +2 -0
  56. rupaka/blocks/toggle/block.json +14 -0
  57. rupaka/blocks/toggle/example.json +10 -0
  58. rupaka/blocks/toggle/toggle.css +5 -0
  59. rupaka/blocks/toggle/toggle.js +11 -0
  60. rupaka/blocks/wheel/block.json +19 -0
  61. rupaka/blocks/wheel/example.json +15 -0
  62. rupaka/blocks/wheel/wheel.css +20 -0
  63. rupaka/blocks/wheel/wheel.js +73 -0
  64. rupaka/check.py +207 -0
  65. rupaka/demos/nakshatra-pada.json +41 -0
  66. rupaka/demos/parabola-vertex.json +27 -0
  67. rupaka/demos/reciprocal-rank-fusion.json +20 -0
  68. rupaka/demos/softmax-temperature.json +17 -0
  69. rupaka/demos/vowel-sandhi.json +39 -0
  70. rupaka/fns/expr.js +56 -0
  71. rupaka/fns/expr.json +10 -0
  72. rupaka/fns/fmt.json +7 -0
  73. rupaka/fns/helpers.js +8 -0
  74. rupaka/fns/ids.json +7 -0
  75. rupaka/fns/lookup.js +8 -0
  76. rupaka/fns/lookup.json +11 -0
  77. rupaka/fns/pick.json +7 -0
  78. rupaka/fns/rrf.js +6 -0
  79. rupaka/fns/rrf.json +10 -0
  80. rupaka/fns/rules.js +14 -0
  81. rupaka/fns/rules.json +11 -0
  82. rupaka/fns/softmax.js +10 -0
  83. rupaka/fns/softmax.json +13 -0
  84. rupaka/fns/sort.json +7 -0
  85. rupaka/render.py +115 -0
  86. rupaka/spec.py +504 -0
  87. rupaka/static/boot.js +9 -0
  88. rupaka/static/fonts/LICENSES.txt +69 -0
  89. rupaka/static/fonts/libre-baskerville-latin-400-normal.woff2 +0 -0
  90. rupaka/static/fonts/libre-baskerville-latin-ext-400-normal.woff2 +0 -0
  91. rupaka/static/fonts/literata-latin-ext-wght-italic.woff2 +0 -0
  92. rupaka/static/fonts/literata-latin-ext-wght-normal.woff2 +0 -0
  93. rupaka/static/fonts/literata-latin-wght-italic.woff2 +0 -0
  94. rupaka/static/fonts/literata-latin-wght-normal.woff2 +0 -0
  95. rupaka/static/page.css +11 -0
  96. rupaka/static/rupaka.css +35 -0
  97. rupaka/static/rupaka.js +93 -0
  98. rupaka-0.0.1.dist-info/METADATA +116 -0
  99. rupaka-0.0.1.dist-info/RECORD +102 -0
  100. rupaka-0.0.1.dist-info/WHEEL +4 -0
  101. rupaka-0.0.1.dist-info/entry_points.txt +2 -0
  102. rupaka-0.0.1.dist-info/licenses/LICENSE +21 -0
rupaka/__init__.py ADDED
@@ -0,0 +1 @@
1
+ __version__ = "0.0.1"
rupaka/_modidx.py ADDED
@@ -0,0 +1,115 @@
1
+ # Autogenerated by nbdev
2
+
3
+ d = { 'settings': { 'branch': 'main',
4
+ 'doc_baseurl': '/rupaka',
5
+ 'doc_host': 'https://vedicreader.github.io',
6
+ 'git_url': 'https://github.com/vedicreader/rupaka',
7
+ 'lib_path': 'rupaka'},
8
+ 'syms': { 'rupaka.agent': { 'rupaka.agent._brief': ('agent.html#_brief', 'rupaka/agent.py'),
9
+ 'rupaka.agent._prop': ('agent.html#_prop', 'rupaka/agent.py'),
10
+ 'rupaka.agent._slug': ('agent.html#_slug', 'rupaka/agent.py'),
11
+ 'rupaka.agent._words': ('agent.html#_words', 'rupaka/agent.py'),
12
+ 'rupaka.agent.author': ('agent.html#author', 'rupaka/agent.py'),
13
+ 'rupaka.agent.catalog': ('agent.html#catalog', 'rupaka/agent.py'),
14
+ 'rupaka.agent.demo_prompt': ('agent.html#demo_prompt', 'rupaka/agent.py'),
15
+ 'rupaka.agent.demo_sp': ('agent.html#demo_sp', 'rupaka/agent.py'),
16
+ 'rupaka.agent.near': ('agent.html#near', 'rupaka/agent.py'),
17
+ 'rupaka.agent.parse_json': ('agent.html#parse_json', 'rupaka/agent.py'),
18
+ 'rupaka.agent.repair_prompt': ('agent.html#repair_prompt', 'rupaka/agent.py')},
19
+ 'rupaka.check': { 'rupaka.check._boxes': ('check.html#_boxes', 'rupaka/check.py'),
20
+ 'rupaka.check._check': ('check.html#_check', 'rupaka/check.py'),
21
+ 'rupaka.check._close': ('check.html#_close', 'rupaka/check.py'),
22
+ 'rupaka.check._inputs': ('check.html#_inputs', 'rupaka/check.py'),
23
+ 'rupaka.check._keys': ('check.html#_keys', 'rupaka/check.py'),
24
+ 'rupaka.check._loop_running': ('check.html#_loop_running', 'rupaka/check.py'),
25
+ 'rupaka.check._parity': ('check.html#_parity', 'rupaka/check.py'),
26
+ 'rupaka.check._play': ('check.html#_play', 'rupaka/check.py'),
27
+ 'rupaka.check._shot': ('check.html#_shot', 'rupaka/check.py'),
28
+ 'rupaka.check._starts': ('check.html#_starts', 'rupaka/check.py'),
29
+ 'rupaka.check._state': ('check.html#_state', 'rupaka/check.py'),
30
+ 'rupaka.check.browser': ('check.html#browser', 'rupaka/check.py'),
31
+ 'rupaka.check.browser_path': ('check.html#browser_path', 'rupaka/check.py'),
32
+ 'rupaka.check.can_check': ('check.html#can_check', 'rupaka/check.py'),
33
+ 'rupaka.check.check': ('check.html#check', 'rupaka/check.py'),
34
+ 'rupaka.check.contrast': ('check.html#contrast', 'rupaka/check.py'),
35
+ 'rupaka.check.in_thread': ('check.html#in_thread', 'rupaka/check.py')},
36
+ 'rupaka.render': { 'rupaka.render.Assets': ('render.html#assets', 'rupaka/render.py'),
37
+ 'rupaka.render.Demo': ('render.html#demo', 'rupaka/render.py'),
38
+ 'rupaka.render.Show': ('render.html#show', 'rupaka/render.py'),
39
+ 'rupaka.render.Show.__init__': ('render.html#show.__init__', 'rupaka/render.py'),
40
+ 'rupaka.render.Show.__repr__': ('render.html#show.__repr__', 'rupaka/render.py'),
41
+ 'rupaka.render.Show._repr_html_': ('render.html#show._repr_html_', 'rupaka/render.py'),
42
+ 'rupaka.render.Show._repr_mimebundle_': ('render.html#show._repr_mimebundle_', 'rupaka/render.py'),
43
+ 'rupaka.render.assets': ('render.html#assets', 'rupaka/render.py'),
44
+ 'rupaka.render.bundle': ('render.html#bundle', 'rupaka/render.py'),
45
+ 'rupaka.render.font_css': ('render.html#font_css', 'rupaka/render.py'),
46
+ 'rupaka.render.host': ('render.html#host', 'rupaka/render.py'),
47
+ 'rupaka.render.md': ('render.html#md', 'rupaka/render.py'),
48
+ 'rupaka.render.page': ('render.html#page', 'rupaka/render.py'),
49
+ 'rupaka.render.save': ('render.html#save', 'rupaka/render.py'),
50
+ 'rupaka.render.show': ('render.html#show', 'rupaka/render.py'),
51
+ 'rupaka.render.spec_json': ('render.html#spec_json', 'rupaka/render.py'),
52
+ 'rupaka.render.version': ('render.html#version', 'rupaka/render.py')},
53
+ 'rupaka.spec': { 'rupaka.spec._all_waits': ('spec.html#_all_waits', 'rupaka/spec.py'),
54
+ 'rupaka.spec._ancestors': ('spec.html#_ancestors', 'rupaka/spec.py'),
55
+ 'rupaka.spec._b': ('spec.html#_b', 'rupaka/spec.py'),
56
+ 'rupaka.spec._check_blocks': ('spec.html#_check_blocks', 'rupaka/spec.py'),
57
+ 'rupaka.spec._check_gate': ('spec.html#_check_gate', 'rupaka/spec.py'),
58
+ 'rupaka.spec._drop': ('spec.html#_drop', 'rupaka/spec.py'),
59
+ 'rupaka.spec._expr': ('spec.html#_expr', 'rupaka/spec.py'),
60
+ 'rupaka.spec._gates': ('spec.html#_gates', 'rupaka/spec.py'),
61
+ 'rupaka.spec._int32': ('spec.html#_int32', 'rupaka/spec.py'),
62
+ 'rupaka.spec._jsround': ('spec.html#_jsround', 'rupaka/spec.py'),
63
+ 'rupaka.spec._lex': ('spec.html#_lex', 'rupaka/spec.py'),
64
+ 'rupaka.spec._lookup': ('spec.html#_lookup', 'rupaka/spec.py'),
65
+ 'rupaka.spec._num': ('spec.html#_num', 'rupaka/spec.py'),
66
+ 'rupaka.spec._props': ('spec.html#_props', 'rupaka/spec.py'),
67
+ 'rupaka.spec._reads': ('spec.html#_reads', 'rupaka/spec.py'),
68
+ 'rupaka.spec._root': ('spec.html#_root', 'rupaka/spec.py'),
69
+ 'rupaka.spec._rrf': ('spec.html#_rrf', 'rupaka/spec.py'),
70
+ 'rupaka.spec._rules': ('spec.html#_rules', 'rupaka/spec.py'),
71
+ 'rupaka.spec._safe': ('spec.html#_safe', 'rupaka/spec.py'),
72
+ 'rupaka.spec._softmax': ('spec.html#_softmax', 'rupaka/spec.py'),
73
+ 'rupaka.spec._sortby': ('spec.html#_sortby', 'rupaka/spec.py'),
74
+ 'rupaka.spec._taint': ('spec.html#_taint', 'rupaka/spec.py'),
75
+ 'rupaka.spec._type_err': ('spec.html#_type_err', 'rupaka/spec.py'),
76
+ 'rupaka.spec._utf16': ('spec.html#_utf16', 'rupaka/spec.py'),
77
+ 'rupaka.spec._waits': ('spec.html#_waits', 'rupaka/spec.py'),
78
+ 'rupaka.spec._walk': ('spec.html#_walk', 'rupaka/spec.py'),
79
+ 'rupaka.spec.axis': ('spec.html#axis', 'rupaka/spec.py'),
80
+ 'rupaka.spec.bars': ('spec.html#bars', 'rupaka/spec.py'),
81
+ 'rupaka.spec.caption': ('spec.html#caption', 'rupaka/spec.py'),
82
+ 'rupaka.spec.choice': ('spec.html#choice', 'rupaka/spec.py'),
83
+ 'rupaka.spec.demo': ('spec.html#demo', 'rupaka/spec.py'),
84
+ 'rupaka.spec.examples': ('spec.html#examples', 'rupaka/spec.py'),
85
+ 'rupaka.spec.expr': ('spec.html#expr', 'rupaka/spec.py'),
86
+ 'rupaka.spec.fmt': ('spec.html#fmt', 'rupaka/spec.py'),
87
+ 'rupaka.spec.fmt_text': ('spec.html#fmt_text', 'rupaka/spec.py'),
88
+ 'rupaka.spec.fn_manifests': ('spec.html#fn_manifests', 'rupaka/spec.py'),
89
+ 'rupaka.spec.from_': ('spec.html#from_', 'rupaka/spec.py'),
90
+ 'rupaka.spec.gallery': ('spec.html#gallery', 'rupaka/spec.py'),
91
+ 'rupaka.spec.gate': ('spec.html#gate', 'rupaka/spec.py'),
92
+ 'rupaka.spec.get': ('spec.html#get', 'rupaka/spec.py'),
93
+ 'rupaka.spec.grid': ('spec.html#grid', 'rupaka/spec.py'),
94
+ 'rupaka.spec.is_': ('spec.html#is_', 'rupaka/spec.py'),
95
+ 'rupaka.spec.js_str': ('spec.html#js_str', 'rupaka/spec.py'),
96
+ 'rupaka.spec.long_sentences': ('spec.html#long_sentences', 'rupaka/spec.py'),
97
+ 'rupaka.spec.lookup': ('spec.html#lookup', 'rupaka/spec.py'),
98
+ 'rupaka.spec.manifests': ('spec.html#manifests', 'rupaka/spec.py'),
99
+ 'rupaka.spec.parse_expr': ('spec.html#parse_expr', 'rupaka/spec.py'),
100
+ 'rupaka.spec.plot': ('spec.html#plot', 'rupaka/spec.py'),
101
+ 'rupaka.spec.point': ('spec.html#point', 'rupaka/spec.py'),
102
+ 'rupaka.spec.ranklist': ('spec.html#ranklist', 'rupaka/spec.py'),
103
+ 'rupaka.spec.readout': ('spec.html#readout', 'rupaka/spec.py'),
104
+ 'rupaka.spec.row': ('spec.html#row', 'rupaka/spec.py'),
105
+ 'rupaka.spec.rrf': ('spec.html#rrf', 'rupaka/spec.py'),
106
+ 'rupaka.spec.rules': ('spec.html#rules', 'rupaka/spec.py'),
107
+ 'rupaka.spec.run': ('spec.html#run', 'rupaka/spec.py'),
108
+ 'rupaka.spec.show_value': ('spec.html#show_value', 'rupaka/spec.py'),
109
+ 'rupaka.spec.slider': ('spec.html#slider', 'rupaka/spec.py'),
110
+ 'rupaka.spec.softmax': ('spec.html#softmax', 'rupaka/spec.py'),
111
+ 'rupaka.spec.stack': ('spec.html#stack', 'rupaka/spec.py'),
112
+ 'rupaka.spec.toggle': ('spec.html#toggle', 'rupaka/spec.py'),
113
+ 'rupaka.spec.upgrade': ('spec.html#upgrade', 'rupaka/spec.py'),
114
+ 'rupaka.spec.validate': ('spec.html#validate', 'rupaka/spec.py'),
115
+ 'rupaka.spec.wheel': ('spec.html#wheel', 'rupaka/spec.py')}}}
rupaka/agent.py ADDED
@@ -0,0 +1,124 @@
1
+ """The demo agent: a model writes a spec from the block manifests, and code checks it before anyone sees it.
2
+
3
+ Docs: https://vedicreader.github.io/rupaka/agent.html.md"""
4
+
5
+ # AUTOGENERATED! DO NOT EDIT! File to edit: ../nbs/03_agent.ipynb.
6
+
7
+ # %% auto #0
8
+ __all__ = ['SHAPE', 'catalog', 'demo_sp', 'near', 'demo_prompt', 'repair_prompt', 'parse_json', 'author']
9
+
10
+ # %% ../nbs/03_agent.ipynb #c004
11
+ import json, re
12
+ from fastcore.utils import *
13
+ from .spec import manifests, fn_manifests, validate, gallery, SCHEMA
14
+
15
+ # %% ../nbs/03_agent.ipynb #c006
16
+ def _prop(k, p):
17
+ bits = [p['type'], 'required' if p.get('req') else None, f'default {json.dumps(p["default"], ensure_ascii=False)}' if 'default' in p else None]
18
+ return f'{k} ({", ".join(b for b in bits if b)})' + (f': {p["doc"]}' if p.get('doc') else '')
19
+
20
+ def catalog() -> str:
21
+ "The blocks and fns, with their purpose and props, as text for the prompt."
22
+ bl = [f'- {m.name}: {m.purpose}' + (f' Keys: {m.keys}.' if m.get('keys', 'none') != 'none' else '')
23
+ + (' Can have "lock".' if m.get('lockable') else '') + (f' Only inside {m.parent}.' if m.get('parent') else '')
24
+ + ''.join(f'\n {_prop(k, p)}' for k, p in m.props.items()) for m in manifests().values()]
25
+ fs = [f'- {m.name}: {m.purpose} Returns {m.returns}. Example: {json.dumps(m.example, ensure_ascii=False)}'
26
+ + ''.join(f'\n {_prop(k, p)}' for k, p in m.props.items()) for m in fn_manifests().values()]
27
+ return 'BLOCKS (every block can have "after": a gate id)\n' + '\n'.join(bl) + '\n\nFNS (each derive entry has "fn" and "out")\n' + '\n'.join(fs)
28
+
29
+ # %% ../nbs/03_agent.ipynb #c010
30
+ SHAPE = '''{"rupaka": 1, "id": "short-slug", "title": str, "ask": str,
31
+ "state": {key: value}, # the inputs
32
+ "derive": [{"fn": name, ..., "out": key}], # computed in order after each change
33
+ "blocks": [{"type": name, ...}],
34
+ "tables": {name: [rows]}} # data for rules and lookup'''
35
+
36
+ def demo_sp() -> str:
37
+ "The system prompt of the demo agent."
38
+ return f"""You design one interactive demo that makes the learner think. You write a JSON spec. You never write code.
39
+ Rules:
40
+ - One concept. At least one gate. The learner predicts before the demo shows any result.
41
+ - Every block that shows a value the gate's answer depends on has "after": the gate id. Inputs can show before the gate; results cannot.
42
+ - Controls that the learner should not move before the prediction have "lock": the gate id.
43
+ - A gate answer is a state path (for example "fused.0.id") that is one of its options, or {{"is": value}} for a fixed answer with "hides": the keys that would give it away.
44
+ - When a control can change the answer after the reveal, the gate has "context" that names the inputs, for example "For {{a}} + {{b}}:".
45
+ - After the reveal, the controls are live. The last caption asks one new question that the learner can answer only by using the controls.
46
+ - Wrong options in a gate each match one named misconception. Use "rules" with "alts" when the concept is a rule system.
47
+ - Text uses {{path}} for state values, and **bold** in captions. No HTML.
48
+ - Fit in 640 x 720 pixels. At most 8 blocks. Write text in ASD-STE100 Simplified Technical English, US grade 9: short sentences of at most 20 words, active voice, simple words.
49
+ - Facts must be correct. Use only facts that you are sure of.
50
+ The shape of a spec:
51
+ {SHAPE}
52
+ Use only these blocks and fns:
53
+ {catalog()}
54
+ Return JSON only."""
55
+
56
+ # %% ../nbs/03_agent.ipynb #c013
57
+ def _words(s): return set(re.findall(r'[a-zāīūṛṃḥṅñṭḍṇśṣ]{3,}', str(s).lower()))
58
+
59
+ def near(concept, k:int=2) -> list:
60
+ "The `k` gallery demos that share the most words with `concept`."
61
+ w = _words(' '.join(str(concept.get(f, '')) for f in ('name', 'statement', 'subject')))
62
+ gs = sorted(gallery().values(), key=lambda s: -len(w & _words(f"{s.get('title')} {s.get('ask')} {s.get('concept')}")))
63
+ return gs[:k]
64
+
65
+ def _brief(spec):
66
+ "A copy of `spec` with each table cut to three rows."
67
+ return {**spec, 'tables': {k: v[:3] for k, v in spec['tables'].items()}} if spec.get('tables') else spec
68
+
69
+ # %% ../nbs/03_agent.ipynb #c014
70
+ def demo_prompt(concept, first_error:str='', examples=()) -> str:
71
+ "The prompt that asks for a demo on `concept`, a dict with name, statement, subject and misconceptions."
72
+ mis = '; '.join(concept.get('misconceptions') or []) or 'none known'
73
+ ex = '\n\n'.join(json.dumps(_brief(e), ensure_ascii=False) for e in examples)
74
+ return (f"Concept: {concept['name']} (subject: {concept.get('subject') or 'general'})\n"
75
+ f"Statement: {concept.get('statement') or '(none given)'}\nMisconceptions seen: {mis}\n"
76
+ + (f"The learner's first error: {first_error}\n" if first_error else '')
77
+ + "\nDesign one demo that makes the learner predict, then see, then change the inputs."
78
+ + (" Aim the gate at the first error and the misconceptions." if first_error or concept.get('misconceptions') else '')
79
+ + (f"\n\nExamples of good demos (tables cut short):\n{ex}" if ex else '')
80
+ + '\n\nReturn the JSON spec only.')
81
+
82
+ # %% ../nbs/03_agent.ipynb #c017
83
+ def repair_prompt(spec, errors) -> str:
84
+ "The prompt that asks the model to fix only `errors` in `spec`."
85
+ s = spec if isinstance(spec, str) else json.dumps(spec, ensure_ascii=False)
86
+ return (f"This demo spec has errors.\n\nSpec:\n{s}\n\nErrors:\n" + '\n'.join(f'- {e}' for e in errors)
87
+ + '\n\nFix only these errors. Keep the rest. Return the whole spec as JSON only.')
88
+
89
+ # %% ../nbs/03_agent.ipynb #c019
90
+ def parse_json(s:str) -> dict:
91
+ "The first JSON object in `s`. Raise `ValueError` when there is none."
92
+ s = s.strip()
93
+ if (m := re.search(r'```(?:json)?\s*(.*?)```', s, re.S)): s = m.group(1).strip()
94
+ try: return json.loads(s)
95
+ except json.JSONDecodeError: pass
96
+ i, j = s.find('{'), s.rfind('}')
97
+ if 0 <= i < j:
98
+ try: return json.loads(s[i:j + 1])
99
+ except json.JSONDecodeError as e: raise ValueError(f'the reply is not valid JSON: {e}') from None
100
+ raise ValueError('the reply has no JSON object')
101
+
102
+ # %% ../nbs/03_agent.ipynb #c022
103
+ def _slug(s): return re.sub(r'[^a-z0-9]+', '-', str(s).lower()).strip('-') or 'demo'
104
+
105
+ def author(concept, ask, first_error:str='', examples=None, check=True, repairs:int=2, log:list=None) -> dict|None:
106
+ "A checked demo spec on `concept` from the model `ask`, or None after `repairs` failed repairs."
107
+ c = concept if isinstance(concept, dict) else dict(name=str(concept))
108
+ slug = c.get('slug') or _slug(c['name'])
109
+ if check is True: from rupaka.check import check
110
+ prompt, sp = demo_prompt(c, first_error, near(c) if examples is None else examples), demo_sp()
111
+ log = [] if log is None else log
112
+ for i in range(repairs + 1):
113
+ try: reply = ask(prompt, sp)
114
+ except Exception as e: log.append(dict(tries=i + 1, errors=[f'model: {e}'])); return None
115
+ try: spec = parse_json(reply)
116
+ except ValueError as e: spec, errs = None, [f'reply: {e}']
117
+ else:
118
+ spec = {**spec, 'rupaka': SCHEMA, 'id': spec.get('id') or slug, 'concept': slug}
119
+ errs = list(validate(spec))
120
+ if not errs and check: errs = list(check(spec).errors)
121
+ if not errs: return spec
122
+ log.append(dict(tries=i + 1, errors=errs))
123
+ prompt = repair_prompt(spec or reply, errs)
124
+ return None
@@ -0,0 +1,4 @@
1
+ .rk-axis .grid .hair { stroke: color-mix(in srgb, var(--rk-line) 70%, transparent); stroke-width: 1; }
2
+ .rk-axis .grid .zero, .rk-axis .grid .main { stroke: var(--rk-mute); stroke-width: 1; }
3
+ .rk-axis .grid text.num { fill: var(--rk-mute); font-size: 12px; font-variant-numeric: tabular-nums; }
4
+ .rk-axis .grid text.axl { fill: var(--rk-mute); font-size: 13px; font-style: italic; }
@@ -0,0 +1,31 @@
1
+ /* axis: a number line (no y) or x/y axes with ticks. Child blocks (point, plot) draw on its shared scale. */
2
+ (() => {
3
+ let seq = 0;
4
+ const nice = (lo, hi, want) => { const raw = (hi - lo) / want, p = 10 ** Math.floor(Math.log10(raw)), m = raw / p;
5
+ return (m < 1.5 ? 1 : m < 3 ? 2 : m < 7 ? 5 : 10) * p; };
6
+ const ticks = (lo, hi, st) => { const out = []; for (let v = Math.ceil(lo / st - 1e-9) * st; v <= hi + 1e-9; v += st) out.push(+v.toFixed(10)); return out; };
7
+ Rupaka.block('axis', (b, ctx) => {
8
+ const {el, sv, show} = Rupaka, [x0, x1] = b.x || [-5, 5], line = !b.y, [y0, y1] = b.y || [0, 1];
9
+ const W = 600, H = line ? 80 : (b.height || 360), L = line ? 16 : 40, Rm = 16, T = line ? 30 : 12, B = line ? 30 : 28;
10
+ const X = v => L + (v - x0) / (x1 - x0) * (W - L - Rm), Y = v => line ? T : T + (y1 - v) / (y1 - y0) * (H - T - B);
11
+ const plane = {X, Y, line, W, H, x: [x0, x1], y: [y0, y1], invX: px => x0 + (px - L) / (W - L - Rm) * (x1 - x0), invY: py => y1 - (py - T) / (H - T - B) * (y1 - y0)};
12
+ const id = `rk-clip-${++seq}`;
13
+ const svg = sv('svg', {viewBox: `0 0 ${W} ${H}`, role: 'group', 'aria-label': b.label || 'axes'});
14
+ plane.svg = svg; plane.clip = `url(#${id})`;
15
+ const g = sv('g', {class: 'grid'});
16
+ const xs = ticks(x0, x1, b.xstep || nice(x0, x1, 10)), ys = line ? [] : ticks(y0, y1, b.ystep || nice(y0, y1, 8));
17
+ const ax = line ? T : Y(Math.min(y1, Math.max(y0, 0))), ay = X(Math.min(x1, Math.max(x0, 0)));
18
+ xs.forEach(v => { g.append(sv('line', {class: v === 0 && !line ? 'zero' : 'hair', x1: X(v), x2: X(v), y1: line ? T - 5 : T, y2: line ? T + 5 : H - B}),
19
+ sv('text', {class: 'num', x: X(v), y: line ? T + 22 : H - B + 18, 'text-anchor': 'middle', text: show(v, 3)})); });
20
+ ys.forEach(v => { g.append(sv('line', {class: v === 0 ? 'zero' : 'hair', x1: L, x2: W - Rm, y1: Y(v), y2: Y(v)}),
21
+ sv('text', {class: 'num', x: L - 6, y: Y(v) + 4, 'text-anchor': 'end', text: show(v, 3)})); });
22
+ g.append(sv('line', {class: 'main', x1: L, x2: W - Rm, y1: ax, y2: ax}));
23
+ if (!line) g.append(sv('line', {class: 'main', x1: ay, x2: ay, y1: T, y2: H - B}));
24
+ if (b.xlabel) g.append(sv('text', {class: 'axl', x: W - Rm, y: line ? T - 10 : ax - 8, 'text-anchor': 'end', text: b.xlabel}));
25
+ if (b.ylabel && !line) g.append(sv('text', {class: 'axl', x: ay + 8, y: T + 12, text: b.ylabel}));
26
+ svg.append(sv('defs', {}, sv('clipPath', {id}, sv('rect', {x: L, y: line ? 0 : T, width: W - L - Rm, height: line ? H : H - T - B}))), g);
27
+ const c2 = Object.assign(Object.create(ctx), {plane});
28
+ svg.append(...(b.blocks || []).map(k => Rupaka.build(k, c2)));
29
+ return el('div', {}, Rupaka.label(b), svg);
30
+ });
31
+ })();
@@ -0,0 +1,18 @@
1
+ {
2
+ "name": "axis",
3
+ "since": "0.0.1",
4
+ "purpose": "A number line (give only x) or x/y axes with ticks. Put point and plot blocks inside it; they share its scale.",
5
+ "binds": "none",
6
+ "keys": "none",
7
+ "props": {
8
+ "x": {"type": "range", "req": true, "doc": "[min, max] of x"},
9
+ "y": {"type": "range", "doc": "[min, max] of y; leave it out for a number line"},
10
+ "xstep": {"type": "num", "doc": "the tick step on x"},
11
+ "ystep": {"type": "num", "doc": "the tick step on y"},
12
+ "xlabel": {"type": "str"},
13
+ "ylabel": {"type": "str"},
14
+ "height": {"type": "num", "default": 360},
15
+ "label": {"type": "str"},
16
+ "blocks": {"type": "blocks", "req": true, "doc": "point and plot blocks only", "only": ["point", "plot"]}
17
+ }
18
+ }
@@ -0,0 +1,12 @@
1
+ {
2
+ "rupaka": 1, "id": "axis-example", "title": "Axis",
3
+ "ask": "Place the point where you think −7/3 is. Then lock in.",
4
+ "state": {"x": 0},
5
+ "derive": [{"fn": "expr", "expr": "-7/3", "out": "target"}, {"fn": "expr", "expr": "round(target)", "out": "near"}],
6
+ "blocks": [
7
+ {"type": "axis", "x": [-4, 4], "xstep": 1, "blocks": [{"type": "point", "bind": "x", "step": 0.25, "label": "you"}]},
8
+ {"type": "gate", "id": "near", "prompt": "Which whole number is closest to −7/3?", "options": [-3, -2, 2, 3], "answer": "near"},
9
+ {"type": "axis", "after": "near", "x": [-4, 4], "xstep": 1, "blocks": [{"type": "point", "x": "target", "label": "−7/3"}]},
10
+ {"type": "caption", "after": "near", "text": "−7/3 is **{target:3}**. Where is −8/3?"}
11
+ ]
12
+ }
@@ -0,0 +1,8 @@
1
+ .rk-bars .bar { transition: transform .25s ease; }
2
+ .rk-bars rect { transition: width .25s ease, x .25s ease; }
3
+ .rk-bars rect.p0, .rk-bars .key i.p0 { fill: var(--rk-acc); background: var(--rk-acc); }
4
+ .rk-bars rect.p1, .rk-bars .key i.p1 { fill: color-mix(in srgb, var(--rk-acc) 50%, var(--rk-bg)); background: color-mix(in srgb, var(--rk-acc) 50%, var(--rk-bg)); }
5
+ .rk-bars text.v { fill: var(--rk-mute); font-size: 13px; font-variant-numeric: tabular-nums; }
6
+ .rk-bars text.t { font-size: 15px; }
7
+ .rk-bars .key { font-size: 13px; color: var(--rk-mute); display: flex; gap: 16px; margin-top: 4px; }
8
+ .rk-bars .key i { display: inline-block; width: 10px; height: 10px; margin-right: 6px; vertical-align: -1px; }
@@ -0,0 +1,24 @@
1
+ /* bars: horizontal bars from a derived list of {id, value, parts} or of numbers. Rows keep their element, so a new order slides. */
2
+ Rupaka.block('bars', (b, ctx) => {
3
+ const {el, sv, show} = Rupaka, H = 30, W = 600, L = b.gutter ?? 48, names = b.items || {};
4
+ const svg = sv('svg', {role: 'img', 'aria-label': b.label || b.bind}), keyed = {};
5
+ const key = b.parts ? el('div', {class: 'key'}, b.parts.map((p, i) => el('span', {}, el('i', {class: `p${i ? 1 : 0}`}), p))) : null;
6
+ const rows = () => (ctx.s.get(b.bind) || []).map((x, i) => typeof x === 'number' ? {id: String(i), value: x} : x);
7
+ ctx.s.on(() => {
8
+ const xs = rows(), mx = b.max ?? Math.max(...xs.map(x => x.value), 1e-9), sc = (W - L - 70) / mx;
9
+ svg.setAttribute('viewBox', `0 0 ${W} ${H * xs.length || H}`);
10
+ xs.forEach((x, i) => {
11
+ let g = keyed[x.id];
12
+ if (!g) { g = keyed[x.id] = sv('g', {class: 'bar'}, sv('text', {class: 't', x: 0, y: H / 2 + 5, text: names[x.id] || x.id}),
13
+ sv('rect', {class: 'p0', x: L, y: 7, height: H - 14}), sv('rect', {class: 'p1', y: 7, height: H - 14}), sv('text', {class: 'v', y: H / 2 + 5})); svg.append(g); }
14
+ g.style.display = '';
15
+ g.style.transform = `translate(0px, ${i * H}px)`;
16
+ const [a, c] = x.parts || [x.value, 0], [r0, r1] = g.querySelectorAll('rect'), v = g.querySelector('text.v');
17
+ r0.style.width = `${Math.max(0, a * sc)}px`; r1.style.x = `${L + a * sc}px`; r1.style.width = `${Math.max(0, c * sc)}px`;
18
+ v.setAttribute('x', L + Math.max(0, (a + c) * sc) + 6); v.textContent = show(x.value, b.digits ?? 4);
19
+ });
20
+ Object.keys(keyed).forEach(id => { if (!xs.some(x => x.id === id)) keyed[id].style.display = 'none'; });
21
+ svg.setAttribute('aria-label', `${b.label || b.bind}: ${xs.map(x => `${names[x.id] || x.id} ${show(x.value, b.digits ?? 4)}`).join(', ')}`);
22
+ });
23
+ return el('div', {}, Rupaka.label(b), svg, key);
24
+ });
@@ -0,0 +1,16 @@
1
+ {
2
+ "name": "bars",
3
+ "since": "0.0.1",
4
+ "purpose": "Horizontal bars for a derived list of {id, value, parts} (from rrf or softmax) or of numbers. Rows slide to their new place.",
5
+ "binds": "derived list",
6
+ "keys": "none",
7
+ "props": {
8
+ "bind": {"type": "key", "req": true, "doc": "the state key of the list"},
9
+ "label": {"type": "str"},
10
+ "items": {"type": "dict", "doc": "a name to show for each id"},
11
+ "parts": {"type": "list", "doc": "names of the two stacked parts, for a key under the bars"},
12
+ "max": {"type": "num", "doc": "a fixed top of the scale, for example 1 for shares"},
13
+ "digits": {"type": "int", "default": 4},
14
+ "gutter": {"type": "num", "default": 48, "doc": "the width for the row names"}
15
+ }
16
+ }
@@ -0,0 +1,9 @@
1
+ {
2
+ "rupaka": 1, "id": "bars-example", "title": "Bars",
3
+ "state": {"logits": [2, 1, 0], "t": 1},
4
+ "derive": [{"fn": "softmax", "in": "logits", "t": "t", "labels": ["cat", "dog", "fish"], "sort": true, "out": "p"}],
5
+ "blocks": [
6
+ {"type": "gate", "id": "top", "prompt": "Which label gets the largest share?", "options": ["cat", "dog", "fish"], "answer": "p.0.id"},
7
+ {"type": "bars", "after": "top", "bind": "p", "label": "Share", "max": 1, "digits": 3}
8
+ ]
9
+ }
@@ -0,0 +1,11 @@
1
+ {
2
+ "name": "caption",
3
+ "since": "0.0.1",
4
+ "purpose": "The explain text after a reveal. End it with one new question that the learner can answer only by using the controls.",
5
+ "binds": "none",
6
+ "keys": "none",
7
+ "props": {
8
+ "text": {"type": "text", "req": true, "doc": "short text; **bold** and {path} are allowed, nothing else"},
9
+ "label": {"type": "str", "doc": "a small label above the text"}
10
+ }
11
+ }
@@ -0,0 +1,2 @@
1
+ .rk-caption p { color: var(--rk-ink2); font-size: 16px; margin: 0; }
2
+ .rk-caption b { color: var(--rk-fg); font-weight: 600; }
@@ -0,0 +1,6 @@
1
+ /* caption: the explain text. Only **bold** and {path} are special; all text goes in with textContent, so no HTML runs. */
2
+ Rupaka.block('caption', (b, ctx) => {
3
+ const {el, fmt} = Rupaka, p = el('p');
4
+ ctx.s.on(() => p.replaceChildren(...String(b.text ?? '').split('**').map((t, i) => i % 2 ? el('b', {text: fmt(t, ctx.s.st)}) : fmt(t, ctx.s.st))));
5
+ return el('div', {}, Rupaka.label(b), p);
6
+ });
@@ -0,0 +1,9 @@
1
+ {
2
+ "rupaka": 1, "id": "caption-example", "title": "Caption",
3
+ "state": {"n": 4},
4
+ "derive": [{"fn": "expr", "expr": "n * 2", "out": "twice"}],
5
+ "blocks": [
6
+ {"type": "gate", "id": "twice", "prompt": "What is twice {n}?", "options": [6, 8, 16], "answer": "twice"},
7
+ {"type": "caption", "after": "twice", "text": "Twice {n} is **{twice}**. What is twice that?"}
8
+ ]
9
+ }
@@ -0,0 +1,13 @@
1
+ {
2
+ "name": "choice",
3
+ "since": "0.0.1",
4
+ "purpose": "Pick one of a few cases, for example which input to try.",
5
+ "binds": "string",
6
+ "keys": "←/→ or A/D move, 1-9 pick",
7
+ "lockable": true,
8
+ "props": {
9
+ "bind": {"type": "key", "req": true, "doc": "the state key of the picked option"},
10
+ "options": {"type": "list", "req": true, "doc": "the options, two to nine"},
11
+ "label": {"type": "str"}
12
+ }
13
+ }
@@ -0,0 +1,2 @@
1
+ .rk-choice .opts { display: flex; gap: 8px; flex-wrap: wrap; }
2
+ .rk-choice .opts button[aria-checked=true] { border-color: var(--rk-acc); box-shadow: inset 0 -2px 0 var(--rk-acc); }
@@ -0,0 +1,20 @@
1
+ /* choice: a row of options with one picked. ←/→ or A/D move, 1-9 pick. */
2
+ Rupaka.block('choice', (b, ctx) => {
3
+ const {el, show} = Rupaka, s = ctx.s, os = b.options || [];
4
+ const box = el('div', {class: 'opts', role: 'radiogroup', 'aria-label': b.label || b.bind});
5
+ const btns = os.map((o, i) => el('button', {type: 'button', role: 'radio', onclick: () => ctx.put(b, b.bind, o)}, show(o), i < 9 ? el('kbd', {text: i + 1}) : null));
6
+ box.append(...btns);
7
+ box.addEventListener('keydown', e => {
8
+ if (e.metaKey || e.ctrlKey || e.altKey) return;
9
+ const k = e.key.toLowerCase(), i = os.indexOf(s.get(b.bind));
10
+ const j = ['arrowright', 'd'].includes(k) ? i + 1 : ['arrowleft', 'a'].includes(k) ? i - 1 : /^[1-9]$/.test(k) ? +k - 1 : null;
11
+ if (j == null || j < 0 || j >= os.length) return;
12
+ e.preventDefault();
13
+ if (ctx.put(b, b.bind, os[j])) btns[j].focus();
14
+ });
15
+ s.on(() => {
16
+ const v = s.get(b.bind), at = Math.max(0, os.indexOf(v));
17
+ btns.forEach((x, i) => { x.setAttribute('aria-checked', String(os[i] === v)); x.tabIndex = i === at ? 0 : -1; x.disabled = !ctx.live(b.lock); });
18
+ });
19
+ return el('div', {}, Rupaka.label(b), box);
20
+ });
@@ -0,0 +1,11 @@
1
+ {
2
+ "rupaka": 1, "id": "choice-example", "title": "Choice",
3
+ "state": {"shape": "square"},
4
+ "derive": [{"fn": "lookup", "table": "sides", "key": "shape", "out": "n"}],
5
+ "blocks": [
6
+ {"type": "choice", "bind": "shape", "label": "Shape", "options": ["triangle", "square", "hexagon"]},
7
+ {"type": "gate", "id": "n", "prompt": "How many sides has a {shape}?", "options": [3, 4, 6], "answer": "n.sides", "context": "For a {shape}:", "again": true},
8
+ {"type": "readout", "after": "n", "text": "A {shape} has {n.sides} sides."}
9
+ ],
10
+ "tables": {"sides": [{"id": "triangle", "sides": 3}, {"id": "square", "sides": 4}, {"id": "hexagon", "sides": 6}]}
11
+ }
@@ -0,0 +1,17 @@
1
+ {
2
+ "name": "gate",
3
+ "since": "0.0.1",
4
+ "purpose": "Ask one question and hide the result until the learner locks in a prediction. Blocks with \"after\": this id wait for it.",
5
+ "binds": "none",
6
+ "keys": "1-9 pick, ←/→ or A/D move, ⌘↩ lock in",
7
+ "props": {
8
+ "id": {"type": "str", "req": true, "doc": "the gate id; other blocks name it in after or lock"},
9
+ "prompt": {"type": "text", "req": true, "doc": "the question; {path} takes a state value"},
10
+ "options": {"type": "options", "req": true, "doc": "a list of answers, or {\"from\": path} for a list in the state (for example rules alts)"},
11
+ "answer": {"type": "answer", "req": true, "doc": "a state path that holds the right answer, or {\"is\": value} for a fixed answer"},
12
+ "why": {"type": "str", "doc": "a placeholder that asks the learner to say why in one line"},
13
+ "context": {"type": "text", "doc": "names the inputs the verdict is for, for example \"For {a} + {b}:\"; needed when a live control feeds the answer"},
14
+ "again": {"type": "bool", "default": false, "doc": "let the learner predict again for new inputs"},
15
+ "hides": {"type": "keys", "doc": "with a fixed answer: the keys that would give the answer away; blocks that show them must wait"}
16
+ }
17
+ }
@@ -0,0 +1,10 @@
1
+ {
2
+ "rupaka": 1, "id": "gate-example", "title": "Gate",
3
+ "ask": "Two plus three. Predict the sum before you see it.",
4
+ "state": {"a": 2, "b": 3},
5
+ "derive": [{"fn": "expr", "expr": "a + b", "out": "sum"}],
6
+ "blocks": [
7
+ {"type": "gate", "id": "sum", "prompt": "What is {a} + {b}?", "options": [4, 5, 6, 23], "answer": "sum", "why": "Say why, in one line."},
8
+ {"type": "readout", "after": "sum", "label": "The sum", "text": "{a} + {b} = {sum}"}
9
+ ]
10
+ }
@@ -0,0 +1,14 @@
1
+ .rk-gate { padding: 14px 0; border-top: 1px solid var(--rk-line); border-bottom: 1px solid var(--rk-line); }
2
+ .rk-gate .q { margin: 0 0 10px; }
3
+ .rk-gate .opts { display: flex; gap: 8px; flex-wrap: wrap; margin-bottom: 12px; }
4
+ .rk-gate .opts button[aria-checked=true] { border-color: var(--rk-acc); box-shadow: inset 0 -2px 0 var(--rk-acc); }
5
+ .rk-gate .opts button:disabled { opacity: 1; color: var(--rk-ink2); }
6
+ .rk-gate .opts button:disabled[aria-checked=true] { color: var(--rk-fg); }
7
+ .rk-gate textarea { box-sizing: border-box; width: 100%; font: 16px/1.5 var(--rk-serif); color: var(--rk-fg); background: transparent; border: 0;
8
+ border-bottom: 1px solid color-mix(in srgb, var(--rk-fg) 22%, transparent); padding: 4px 2px; resize: vertical; margin-bottom: 12px; }
9
+ .rk-gate textarea:focus { outline: none; border-bottom-color: var(--rk-fg); }
10
+ .rk-gate textarea::placeholder { color: var(--rk-mute); font-style: italic; opacity: 1; }
11
+ .rk-gate .verdict { margin: 0 0 10px; font-size: 18px; }
12
+ .rk-gate .verdict .ok { color: var(--rk-ok); } .rk-gate .verdict .no { color: var(--rk-no); }
13
+ .rk-gate .verdict .stale { color: var(--rk-mute); font-size: 15px; font-style: italic; }
14
+ .rk-gate .acts { display: flex; gap: 10px; align-items: center; }
@@ -0,0 +1,61 @@
1
+ /* gate: predict, then reveal. Keys 1-9 pick, ←/→ or A/D move, ⌘↩ locks in. Blocks with `after: id` wait for it.
2
+ The verdict names the inputs it was for (`context`), because the inputs can change after the reveal. */
3
+ Rupaka.block('gate', (b, ctx) => {
4
+ const {el, fmt, show} = Rupaka, s = ctx.s, id = b.id;
5
+ s.st.gate[id] = 'open';
6
+ let picked = null, said = '', seen = '';
7
+ const opts = () => Array.isArray(b.options) ? b.options : (s.get(b.options.from) || []);
8
+ const ans = () => b.answer && typeof b.answer === 'object' ? b.answer.is : s.get(b.answer);
9
+ const done = () => s.get(`gate.${id}`) === 'done';
10
+ const q = el('p', {class: 'q', id: `rk-${ctx.spec.id || 'd'}-${id}-q`});
11
+ const box = el('div', {class: 'opts', role: 'radiogroup', 'aria-labelledby': q.id});
12
+ const why = b.why ? el('textarea', {rows: 1, placeholder: b.why, 'aria-label': b.why}) : null;
13
+ const go = el('button', {class: 'go', type: 'button', disabled: true}, 'Lock in', el('kbd', {text: '⌘↩'}));
14
+ const again = el('button', {type: 'button', hidden: true}, 'Try another');
15
+ const verdict = el('p', {class: 'verdict', 'aria-live': 'polite', hidden: true});
16
+ const stale = el('span', {class: 'stale', hidden: true, text: ' The demo now shows other inputs.'});
17
+ const pick = o => { if (done()) return; picked = o; paint(); };
18
+ function paint() {
19
+ const os = opts(), k = JSON.stringify(os);
20
+ if (k !== seen) {
21
+ seen = k;
22
+ box.replaceChildren(...os.map((o, i) => el('button', {type: 'button', role: 'radio', onclick: () => pick(o)}, show(o), i < 9 ? el('kbd', {text: i + 1}) : null)));
23
+ }
24
+ [...box.children].forEach((x, i) => { x.setAttribute('aria-checked', String(os[i] === picked)); x.disabled = done(); x.tabIndex = os[i] === picked || (picked == null && i === 0) ? 0 : -1; });
25
+ go.disabled = picked == null || done(); go.hidden = done(); again.hidden = !(done() && b.again); verdict.hidden = !done();
26
+ stale.hidden = !done() || !b.context || fmt(b.context, s.st) === said;
27
+ }
28
+ function commit() {
29
+ if (picked == null || done()) return false;
30
+ const a = ans(), ok = String(picked) === String(a);
31
+ said = b.context ? fmt(b.context, s.st) : '';
32
+ verdict.replaceChildren(said ? `${said} ` : '', 'You said ', el('b', {class: ok ? 'ok' : 'no', text: show(picked)}), '. ',
33
+ ok ? el('span', {class: 'ok', text: 'That is right.'}) : el('span', {}, 'It is ', el('b', {class: 'ok', text: show(a)}), '.'), stale);
34
+ s.st.gate[id] = 'done';
35
+ const detail = {gate: id, picked, ans: a, ok, why: why ? why.value : '', context: said};
36
+ s.st.log = [...(s.st.log || []), detail];
37
+ s.set('_', (s.st._ || 0) + 1);
38
+ Rupaka.predict(ctx, detail);
39
+ return true;
40
+ }
41
+ go.onclick = commit;
42
+ again.onclick = () => { picked = null; said = ''; if (why) why.value = ''; s.st.gate[id] = 'open'; s.set('_', (s.st._ || 0) + 1); box.querySelector('button')?.focus(); };
43
+ (ctx.gates ||= []).push({id, commit, done});
44
+ ctx.commit ||= () => { const g = ctx.gates.find(g => !g.done()); return g ? g.commit() : false; };
45
+ const root = el('div', {}, q, box, why, verdict, el('div', {class: 'acts'}, go, again));
46
+ root.addEventListener('keydown', e => {
47
+ if (e.target.tagName === 'TEXTAREA' || done() || e.metaKey || e.ctrlKey || e.altKey) return;
48
+ const os = opts(), k = e.key.toLowerCase(), i = os.indexOf(picked);
49
+ let j = /^[1-9]$/.test(k) ? +k - 1 : null;
50
+ if (j == null && e.target.closest('.opts')) j = ['arrowright', 'arrowdown', 'd'].includes(k) ? i + 1 : ['arrowleft', 'arrowup', 'a'].includes(k) ? Math.max(0, i - 1) : null;
51
+ if (j == null || j < 0 || j >= os.length) return;
52
+ e.preventDefault(); pick(os[j]);
53
+ if (e.target.closest('.opts')) box.children[j].focus();
54
+ });
55
+ s.on(() => {
56
+ q.textContent = fmt(b.prompt, s.st);
57
+ if (!Array.isArray(b.options) && !done() && !opts().some(o => o === picked)) picked = null;
58
+ paint();
59
+ });
60
+ return root;
61
+ });
@@ -0,0 +1,23 @@
1
+ {
2
+ "name": "grid",
3
+ "since": "0.0.1",
4
+ "purpose": "A heatmap of a list of numbers or a list of rows (logits, weights). The learner can raise or lower a cell.",
5
+ "binds": "list of numbers or list of rows",
6
+ "keys": "arrows or WASD move, + or E raise, − or Q lower, ⇧ for ten steps",
7
+ "lockable": true,
8
+ "props": {
9
+ "bind": {"type": "key", "req": true, "doc": "the state key of the numbers"},
10
+ "rows": {"type": "list", "doc": "row names"},
11
+ "cols": {"type": "list", "doc": "column names"},
12
+ "step": {"type": "num", "default": 1},
13
+ "min": {"type": "num"},
14
+ "max": {"type": "num"},
15
+ "lo": {"type": "num", "doc": "the value at the pale end of the ramp; the smallest value if left out"},
16
+ "hi": {"type": "num", "doc": "the value at the dark end of the ramp; the largest value if left out"},
17
+ "digits": {"type": "int", "default": 2},
18
+ "cell": {"type": "num", "default": 64, "doc": "the cell width"},
19
+ "gutter": {"type": "num", "default": 70, "doc": "the width for the row names"},
20
+ "note": {"type": "str"},
21
+ "label": {"type": "str"}
22
+ }
23
+ }