code wiki / _hdl_build / nx_meal_skills_page.nx
nx_meal_skills_page.nx source
↩ module page · 299 lines · 16138 B
1// nx_meal_skills_page.nx -- the SKILLS surface: what a dish actually asks of you, what it would cost to skip,
2// and who you can learn each technique from.
3//
4// It is a separate emitter from nx_meal_page on purpose (rule 9). The recipe page answers "what does this cost
5// and where did it come from"; this one answers "can I do it, how long will it take ME, and should I even try
6// tonight". Bolting both into one render would have meant editing a surface that is already 9/9 gate-proven.
7//
8// It composes four existing engines and adds none: nx_meal_effort (skill tree, per-cook time, make-vs-buy),
9// nx_coach_curriculum_lib (the technique checklists -- the SAME engine that coaches NVC), nx_skill_video (who
10// you learn from), and nx_nishi_editorial (the design system).
11//
12// WHAT IT REFUSES TO SAY, and each refusal is inherited rather than re-invented:
13// - a step whose skill we have never assessed shows UNMEASURED, and is excluded from the total
14// - if an hour of this household's time is unpriced, the ledger says so instead of ranking on invented money
15// - an option missing money or time is listed and NOT ranked
16// - a video source claims a preserved copy only when its bytes come back out of the WARC
17// - the request button renders only when the intake is actually listening
18// license_tier: ORIGINAL No hw writes (Rule 26).
19import "nx_nishi_editorial.nx"
20import "nx_meal_effort.nx"
21import "nx_skill_video.nx"
22import "nx_coach_curriculum_lib.nx"
23import "nx_meal_plan.nx"
24import "nx_syscalls.nx"
25
26const MSP_FLD: i64 = 1024
27const MSP_BIG: i64 = 4096
28const MSP_MAX: i64 = 64
29
30// minutes, rendered from seconds
31func msp_mins(out: *u8, off: i64, seconds: i64) -> i64 {
32 var o: i64 = ed_num(out, off, (seconds + 30) / 60)
33 return ed_raw(out, o, " min" as *u8)
34}
35
36func msp_level_name(level: i64) -> *u8 {
37 if level == 0 { return "Never done it" as *u8 }
38 if level == 1 { return "Getting there" as *u8 }
39 if level == 2 { return "Competent" as *u8 }
40 if level == 3 { return "Proficient" as *u8 }
41 if level == 4 { return "Expert" as *u8 }
42 return "Not assessed" as *u8
43}
44
45// one step: what it asks, which skill, where that skill sits, and what it costs THIS cook
46func msp_step(out: *u8, off: i64, prefix: *u8, unit: *u8, rid: *u8, idx: i64) -> i64 {
47 let label: *u8 = sys_mmap(MSP_FLD)
48 let sid: *u8 = sys_mmap(MSP_FLD)
49 ef_step_field(prefix, rid, idx, EF_P_LABEL, label)
50 ef_step_field(prefix, rid, idx, EF_P_SKILL, sid)
51 let lv: i64 = ef_level(prefix, unit, sid)
52 let secs: i64 = ef_step_time(prefix, unit, rid, idx)
53
54 var o: i64 = ed_raw(out, off, "<div class='cell' data-state='" as *u8)
55 if secs < 0 { o = ed_raw(out, o, "alert" as *u8) } else {
56 if lv >= 3 { o = ed_raw(out, o, "have" as *u8) } else { o = ed_raw(out, o, "buy" as *u8) }
57 }
58 o = ed_raw(out, o, "'><span class='cell-tier'>Step " as *u8)
59 o = ed_num(out, o, idx + 1)
60 o = ed_raw(out, o, "</span><span class='cell-name'>" as *u8)
61 o = ed_esc(out, o, label)
62 o = ed_raw(out, o, "</span><span class='cell-state'>" as *u8)
63 o = ed_esc(out, o, msp_level_name(lv))
64 o = ed_raw(out, o, "</span><p class='cell-note'>" as *u8)
65
66 let path: *u8 = sys_mmap(MSP_BIG)
67 if ef_skill_path(prefix, sid, path) > 0 {
68 o = ed_esc(out, o, path)
69 } else {
70 o = ed_esc(out, o, sid)
71 }
72 if secs < 0 {
73 o = ed_raw(out, o, " · we have not measured you on this, so it is held out of the time total rather than guessed" as *u8)
74 } else {
75 o = ed_raw(out, o, " · about " as *u8)
76 o = msp_mins(out, o, secs)
77 o = ed_raw(out, o, " at your pace" as *u8)
78 }
79 return ed_raw(out, o, "</p></div>" as *u8)
80}
81
82// the technique checklist for one skill, straight out of the shared curriculum store
83func msp_checklist(out: *u8, off: i64, h: *i64, sid: *u8, label: *u8) -> i64 {
84 let n: i64 = cc_element_count(h, sid)
85 if n <= 0 { return off }
86 var o: i64 = ed_raw(out, off, "<div class='track'><h3>" as *u8)
87 o = ed_esc(out, o, label)
88 o = ed_raw(out, o, "</h3>" as *u8)
89 let ol: *i64 = sys_mmap(16) as *i64
90 let pr: *u8 = cc_prompt(h, sid, ol)
91 if ol[0] > 0 {
92 o = ed_raw(out, o, "<p class='form-hint'>" as *u8)
93 var k: i64 = 0
94 while k < ol[0] { out[o] = pr[k]; o = o + 1; k = k + 1 }
95 o = ed_raw(out, o, "</p>" as *u8)
96 }
97 o = ed_raw(out, o, "<ol class='steps'>" as *u8)
98 var i: i64 = 0
99 while i < n {
100 let em: *u8 = cc_element(h, sid, i, ol)
101 o = ed_raw(out, o, "<li>" as *u8)
102 var k2: i64 = 0
103 while k2 < ol[0] { out[o] = em[k2]; o = o + 1; k2 = k2 + 1 }
104 let dr: *u8 = cc_drill(h, sid, i, ol)
105 if ol[0] > 0 {
106 o = ed_raw(out, o, "<br><span class='cell-note'>Drill: " as *u8)
107 var k3: i64 = 0
108 while k3 < ol[0] { out[o] = dr[k3]; o = o + 1; k3 = k3 + 1 }
109 o = ed_raw(out, o, "</span>" as *u8)
110 }
111 o = ed_raw(out, o, "</li>" as *u8)
112 i = i + 1
113 }
114 return ed_raw(out, o, "</ol></div>" as *u8)
115}
116
117// the ways to watch this technique done, and whether we still hold a copy of each
118func msp_sources(out: *u8, off: i64, vp: *u8, sid: *u8, warc: *u8, warclen: i64) -> i64 {
119 let vids: *i64 = sys_mmap(8 * MSP_MAX) as *i64
120 let n: i64 = sv_by_skill(vp, sid, vids)
121 if n <= 0 { return off }
122 var o: i64 = ed_raw(out, off, "<div class='scroller'><table><thead><tr><th>Source</th><th>Who</th><th>Kind</th><th>Length</th><th>Copy held</th></tr></thead><tbody>" as *u8)
123 let f: *u8 = sys_mmap(MSP_FLD)
124 var i: i64 = 0
125 while i < n {
126 let vid: *u8 = vids[i] as *u8
127 o = ed_raw(out, o, "<tr><td>" as *u8)
128 sv_field(vp, vid, SV_F_TITLE, f)
129 o = ed_esc(out, o, f)
130 o = ed_raw(out, o, "</td><td>" as *u8)
131 sv_field(vp, vid, SV_F_CREATOR, f)
132 o = ed_esc(out, o, f)
133 o = ed_raw(out, o, "</td><td>" as *u8)
134 sv_field(vp, vid, SV_F_KIND, f)
135 if fd_streq(f, "pro" as *u8) == 1 { o = ed_tag(out, o, "info" as *u8, "professional" as *u8) }
136 if fd_streq(f, "amateur" as *u8) == 1 { o = ed_tag(out, o, "mute" as *u8, "amateur" as *u8) }
137 if fd_streq(f, "chef" as *u8) == 1 { o = ed_tag(out, o, "have" as *u8, "a chef you follow" as *u8) }
138 o = ed_raw(out, o, "</td><td class='num'>" as *u8)
139 let secs: i64 = sv_seconds(vp, vid)
140 if secs > 0 { o = msp_mins(out, o, secs) } else { o = ed_raw(out, o, "—" as *u8) }
141 o = ed_raw(out, o, "</td><td>" as *u8)
142 if sv_is_preserved(vp, vid, warc, warclen) == 1 {
143 o = ed_raw(out, o, "<span class='credit-note kept'>" as *u8)
144 o = ed_num(out, o, sv_preserved_bytes(vp, vid, warc, warclen))
145 o = ed_raw(out, o, " bytes preserved</span>" as *u8)
146 } else {
147 o = ed_raw(out, o, "<span class='credit-note risk'>not preserved · can rot</span>" as *u8)
148 }
149 o = ed_raw(out, o, "</td></tr>" as *u8)
150 i = i + 1
151 }
152 return ed_raw(out, o, "</tbody><caption>Whose demonstration you watch is your choice. An amateur kitchen often looks more like yours than a studio does; neither is ranked above the other here.</caption></table></div>" as *u8)
153}
154
155// ---- the page ----
156func msp_render(out: *u8, prefix: *u8, vp: *u8, currprefix: *u8, unit: *u8, rid: *u8, warc: *u8, warclen: i64, scratch_money: i64, scratch_complete: i64, intake_live: i64, back: *u8) -> i64 {
157 let title: *u8 = sys_mmap(MSP_FLD)
158 mp_field(prefix, rid, MP_F_TITLE, title)
159 let ulabel: *u8 = sys_mmap(MSP_FLD)
160 mp_unit_field(prefix, unit, MP_U_LABEL, ulabel)
161
162 let head: *u8 = sys_mmap(MSP_BIG)
163 var ho: i64 = ed_raw(head, 0, "Skills ยท " as *u8)
164 ho = ed_raw(head, ho, "" as *u8)
165 head[0] = 0 as u8
166 var o: i64 = ed_doc_open(out, 0, title)
167
168 // ---------- masthead ----------
169 o = ed_raw(out, o, "<header class='mast'><p class='mast-eyebrow'>Skills · what this dish asks of you</p><h1>" as *u8)
170 o = ed_esc(out, o, title)
171 o = ed_raw(out, o, "</h1><p class='thesis'>Every step below names the one technique it exercises, where that technique sits in the wider craft, and how long it takes at YOUR measured pace — not a stranger's.</p><div class='mast-meta'>" as *u8)
172 o = ed_raw(out, o, "<span><b>Cooking for</b> " as *u8)
173 o = ed_esc(out, o, ulabel)
174 o = ed_raw(out, o, "</span><span><b>Steps</b> " as *u8)
175 let nsteps: i64 = ef_step_count(prefix, rid)
176 o = ed_num(out, o, nsteps)
177 o = ed_raw(out, o, "</span></div></header>" as *u8)
178
179 o = ed_raw(out, o, "<ul class='filters'><li><a href='" as *u8)
180 o = ed_esc(out, o, back)
181 o = ed_raw(out, o, "'>← the recipe</a></li><li><a aria-current='page' href='#'>Skills</a></li></ul>" as *u8)
182
183 // ---------- 01 the steps ----------
184 o = ed_sec_open(out, o, "01" as *u8, "What it asks of you" as *u8)
185 o = ed_raw(out, o, "<div class='grid'>" as *u8)
186 var i: i64 = 0
187 while i < nsteps { o = msp_step(out, o, prefix, unit, rid, i); i = i + 1 }
188 o = ed_raw(out, o, "</div>" as *u8)
189 let tm: *i64 = sys_mmap(8 * 4) as *i64
190 ef_cook_time(prefix, unit, rid, tm)
191 o = ed_raw(out, o, "<p class='key'>Active time at your pace: " as *u8)
192 o = msp_mins(out, o, tm[0])
193 if tm[1] > 0 {
194 o = ed_raw(out, o, " across " as *u8)
195 o = ed_num(out, o, tm[2] - tm[1])
196 o = ed_raw(out, o, " of " as *u8)
197 o = ed_num(out, o, tm[2])
198 o = ed_raw(out, o, " steps — the rest are unmeasured" as *u8)
199 }
200 o = ed_raw(out, o, "</p>" as *u8)
201 o = ed_sec_close(out, o)
202
203 // ---------- 02 cook it or buy it ----------
204 o = ed_sec_open(out, o, "02" as *u8, "Cook it, or buy it" as *u8)
205 let opt: *i64 = sys_mmap(8 * EF_O_SLOTS * 8) as *i64
206 let nopt: i64 = ef_compare(prefix, unit, rid, scratch_money, scratch_complete, opt, 8)
207 let best: i64 = ef_best(opt, nopt)
208 o = ed_raw(out, o, "<div class='scroller'><table><thead><tr><th>Option</th><th>Money</th><th>Your time</th><th>Time cost</th><th>Total</th></tr></thead><tbody>" as *u8)
209 var k: i64 = 0
210 while k < nopt {
211 let b: i64 = k * EF_O_SLOTS
212 o = ed_raw(out, o, "<tr" as *u8)
213 if k == best { o = ed_raw(out, o, " data-hi" as *u8) }
214 o = ed_raw(out, o, "><td>" as *u8)
215 o = ed_esc(out, o, opt[b + EF_O_KIND] as *u8)
216 o = ed_raw(out, o, "</td><td class='num'>" as *u8)
217 if opt[b + EF_O_MONEY] < 0 { o = ed_raw(out, o, "unknown" as *u8) } else { o = ed_money(out, o, opt[b + EF_O_MONEY]) }
218 o = ed_raw(out, o, "</td><td class='num'>" as *u8)
219 if opt[b + EF_O_SECONDS] < 0 { o = ed_raw(out, o, "unmeasured" as *u8) } else { o = msp_mins(out, o, opt[b + EF_O_SECONDS]) }
220 o = ed_raw(out, o, "</td><td class='num'>" as *u8)
221 if opt[b + EF_O_KNOWN] == 0 { o = ed_raw(out, o, "—" as *u8) } else { o = ed_money(out, o, opt[b + EF_O_TIMECOST]) }
222 o = ed_raw(out, o, "</td><td class='num'>" as *u8)
223 if opt[b + EF_O_KNOWN] == 0 { o = ed_raw(out, o, "not ranked" as *u8) } else { o = ed_money(out, o, opt[b + EF_O_TOTAL]) }
224 o = ed_raw(out, o, "</td></tr>" as *u8)
225 k = k + 1
226 }
227 o = ed_raw(out, o, "</tbody><caption>Time is valued at this household's own stated rate. Options missing either a price or a time are shown but never ranked.</caption></table></div>" as *u8)
228
229 if ef_time_is_valued(prefix, unit) == 0 {
230 o = ed_note(out, o, "flag" as *u8, "Your time is not priced, so this ranks on money alone" as *u8, "Nobody has said what an hour of this household's time is worth, so every option above counts your evening as free -- which will always make cooking from scratch look like the winner. Set a value and the ranking may change." as *u8)
231 }
232 let unranked: i64 = ef_unranked(opt, nopt)
233 if unranked > 0 {
234 let ub: *u8 = sys_mmap(MSP_BIG)
235 var uo: i64 = ed_num(ub, 0, unranked)
236 uo = ed_raw(ub, uo, " of these options is missing either a price or a measured time, so it is listed but left out of the ranking. Being cheapest on the half we measured is not being cheapest." as *u8)
237 ub[uo] = 0 as u8
238 o = ed_note(out, o, "open" as *u8, "Not everything here can be compared" as *u8, ub)
239 }
240 if best >= 0 {
241 o = ed_raw(out, o, "<p class='kicker'>On tonight's numbers the cheapest way to eat this is <b>" as *u8)
242 o = ed_esc(out, o, opt[best * EF_O_SLOTS + EF_O_KIND] as *u8)
243 o = ed_raw(out, o, "</b> at " as *u8)
244 o = ed_money(out, o, opt[best * EF_O_SLOTS + EF_O_TOTAL])
245 o = ed_raw(out, o, " all in. Get measured on the missing techniques and that answer can change — skill is a term in this arithmetic, not a badge.</p>" as *u8)
246 }
247 o = ed_sec_close(out, o)
248
249 // ---------- 03 learn the technique ----------
250 o = ed_sec_open(out, o, "03" as *u8, "Learn the technique" as *u8)
251 let h: *i64 = ss_open(currprefix)
252 let seen: *i64 = sys_mmap(8 * MSP_MAX) as *i64
253 var nseen: i64 = 0
254 var s: i64 = 0
255 while s < nsteps {
256 let sid: *u8 = sys_mmap(MSP_FLD)
257 ef_step_field(prefix, rid, s, EF_P_SKILL, sid)
258 var dup: i64 = 0
259 var q: i64 = 0
260 while q < nseen { if fd_streq(seen[q] as *u8, sid) == 1 { dup = 1 } q = q + 1 }
261 if dup == 0 {
262 seen[nseen] = sid as i64
263 nseen = nseen + 1
264 let nm: *u8 = sys_mmap(MSP_FLD)
265 if ef_skill_field(prefix, sid, EF_S_NAME, nm) == 0 { nm[0] = 0 as u8 }
266 o = ed_raw(out, o, "<div class='tracks'>" as *u8)
267 if (h as i64) != 0 { o = msp_checklist(out, o, h, sid, nm) }
268 o = ed_raw(out, o, "</div>" as *u8)
269 o = msp_sources(out, o, vp, sid, warc, warclen)
270 }
271 s = s + 1
272 }
273 o = ed_sec_close(out, o)
274
275 // ---------- 04 ask for what is missing ----------
276 o = ed_sec_open(out, o, "04" as *u8, "Ask for what is missing" as *u8)
277 if intake_live == 1 {
278 o = ed_raw(out, o, "<p class='lede'>If the cook you follow has not covered one of these techniques, ask them for it. Requests are counted by PERSON, not by click, and a chef is only ever asked for something they have not already published.</p>" as *u8)
279 o = ed_raw(out, o, "<form class='form' method='post' action='/intake/kitchen-request'>" as *u8)
280 o = ed_raw(out, o, "<label>Which technique<input name='name' maxlength='120' placeholder='dicing an onion'></label>" as *u8)
281 o = ed_raw(out, o, "<label>Whose take you want<input name='email' maxlength='160' placeholder='the cook you follow'></label>" as *u8)
282 o = ed_raw(out, o, "<label>Anything else<textarea name='message' maxlength='1000'></textarea></label>" as *u8)
283 o = ed_raw(out, o, "<input class='hp' type='text' name='website' tabindex='-1' aria-hidden='true' autocomplete='off'>" as *u8)
284 o = ed_raw(out, o, "<input type='hidden' name='back' value='" as *u8)
285 o = ed_esc(out, o, back)
286 o = ed_raw(out, o, "'><button type='submit'>Request it</button></form>" as *u8)
287 } else {
288 o = ed_note(out, o, "flag" as *u8, "Requests are not open yet" as *u8, "The intake service that would receive a request is built and gate-proven, but nothing is answering on this host right now, so there is no form here. A request box that quietly swallows what you ask for is worse than none." as *u8)
289 }
290 o = ed_sec_close(out, o)
291
292 o = ed_raw(out, o, "<footer class='colophon'>" as *u8)
293 o = ed_raw(out, o, "<p>Your pace comes from assessed skill levels, not averages. A step we have never measured you on is reported as unmeasured and held out of the total rather than guessed at.</p>" as *u8)
294 o = ed_raw(out, o, "<p>Coaching here is a self-report against a technique checklist -- the same engine that coaches conversation frameworks. Filming yourself and getting automatic feedback needs video-to-keypoints, which does not exist yet; the pose comparison behind it does.</p>" as *u8)
295 o = ed_raw(out, o, "</footer>" as *u8)
296 o = ed_doc_close(out, o)
297 out[o] = 0 as u8
298 return o
299}