3v324v23 commited on
Commit
884eb6b
ยท
1 Parent(s): cb7f2ec

feat: add hint functionality to prompter for user feedback on actions

Browse files
docs/project.prd CHANGED
@@ -75,7 +75,7 @@ Teacher Screen:
75
  - assignments
76
  - give an assignment. load it from:
77
  - the computer (file upload)
78
- - the board
79
  - a picture (camera)
80
  - check assignments: review each student's submitted answers and give feedback/grade
81
  - tests
@@ -84,4 +84,4 @@ Teacher Screen:
84
  - class heat map
85
  - rows: students, columns: skills
86
  - each cell is colored by the skill state (not started / learning / mastered / needs review)
87
- - period filter: sep-dec, dec-jun
 
75
  - assignments
76
  - give an assignment. load it from:
77
  - the computer (file upload)
78
+ - the classroom smart board
79
  - a picture (camera)
80
  - check assignments: review each student's submitted answers and give feedback/grade
81
  - tests
 
84
  - class heat map
85
  - rows: students, columns: skills
86
  - each cell is colored by the skill state (not started / learning / mastered / needs review)
87
+ - period filter: sep-dec, jan-jun
static/prompter.js CHANGED
@@ -26,9 +26,13 @@ const CSS=`
26
  .mxp-fade{position:absolute;inset-inline:0;height:22%;pointer-events:none}
27
  .mxp-fade.top{top:0;background:linear-gradient(#000,transparent)}.mxp-fade.bot{bottom:0;background:linear-gradient(transparent,#000)}
28
  .mxp-progress{height:3px;background:#222}.mxp-progress i{display:block;height:100%;width:0;background:#f5c518}
 
 
 
 
29
  .mxp-count{position:absolute;inset:0;display:grid;place-items:center;font-size:30vh;font-weight:700;color:#f5c518;background:rgba(0,0,0,.6);pointer-events:none}
30
  `;
31
- let wheel=0,keepOpen=false,root,scroller,textEl,guide,bar,raf=0,last=0,playing=false,speed=40,size=64,mirror=false,acc=0,countTimer=0,back=null;
32
 
33
  function save(){try{localStorage.setItem("mathly.prompter",JSON.stringify({speed,size,mirror}))}catch{}}
34
  function load(){try{const s=JSON.parse(localStorage.getItem("mathly.prompter")||"{}");if(s.speed)speed=s.speed;if(s.size)size=s.size;mirror=!!s.mirror}catch{}}
@@ -40,6 +44,7 @@ function style(){
40
 
41
  function sync(){
42
  textEl.style.fontSize=size+"px"; guide.style.height=(size*1.45)+"px";
 
43
  root.classList.toggle("mirror",mirror); root.classList.toggle("playing",playing);
44
  bar.querySelector(".mxp-play").textContent=playing?"โธ ืขืฆื™ืจื”":"โ–ถ ื”ืคืขืœื”";
45
  bar.querySelector("[data-p=mirror]").setAttribute("aria-pressed",String(mirror));
@@ -75,7 +80,26 @@ function countdown(){
75
  countTimer=setInterval(()=>{n--; if(n>0) el.textContent=n; else play(true);},700);
76
  }
77
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
78
  function act(p){
 
 
 
 
 
79
  switch(p){
80
  case "play": return countdown();
81
  case "slower": speed=Math.max(5,speed-5); break;
@@ -124,14 +148,15 @@ function open(text){
124
  <button data-p="close" title="ืกื’ื™ืจื” (Esc)" aria-label="ืกื’ื™ืจื”">โœ•</button></div>
125
  <div class="mxp-progress"><i></i></div>
126
  <div class="mxp-view"><div class="mxp-scroll"><div class="mxp-text" dir="auto"></div></div>
127
- <div class="mxp-fade top"></div><div class="mxp-fade bot"></div><div class="mxp-guide"></div></div>`;
 
128
  scroller=root.querySelector(".mxp-scroll"); textEl=root.querySelector(".mxp-text"); guide=root.querySelector(".mxp-guide"); bar=root.querySelector(".mxp-bar");
129
  textEl.textContent=text;
130
  bar.addEventListener("click",e=>{const b=e.target.closest("[data-p]"); if(b) act(b.dataset.p);});
131
  bar.querySelector(".mxp-speed").addEventListener("input",e=>{speed=+e.target.value; save(); sync();});
132
  scroller.addEventListener("scroll",progress,{passive:true});
133
  const view=root.querySelector(".mxp-view");
134
- view.addEventListener("click",()=>countdown());
135
  view.addEventListener("wheel",e=>{ // while playing the wheel is a speed knob: down faster, up slower
136
  if(!playing){acc=0; return;}
137
  e.preventDefault(); wheel+=e.deltaMode?e.deltaY*40:e.deltaY;
@@ -154,6 +179,7 @@ function open(text){
154
 
155
  function close(){
156
  if(!root) return;
 
157
  play(false); document.removeEventListener("keydown",onKey,true); document.removeEventListener("fullscreenchange",onFull);
158
  if(document.fullscreenElement) document.exitFullscreen?.();
159
  root.remove(); root=null; back?.focus?.(); back=null;
 
26
  .mxp-fade{position:absolute;inset-inline:0;height:22%;pointer-events:none}
27
  .mxp-fade.top{top:0;background:linear-gradient(#000,transparent)}.mxp-fade.bot{bottom:0;background:linear-gradient(transparent,#000)}
28
  .mxp-progress{height:3px;background:#222}.mxp-progress i{display:block;height:100%;width:0;background:#f5c518}
29
+ .mxp-hint{position:absolute;left:50%;bottom:8%;translate:-50% 0;z-index:2;display:flex;align-items:center;gap:10px;padding:12px 22px;border-radius:999px;background:rgba(20,20,20,.95);border:1px solid rgba(245,197,24,.6);color:#fff;font-size:22px;font-weight:600;white-space:nowrap;pointer-events:none;opacity:0;scale:.92;transition:opacity .25s ease,scale .25s ease}
30
+ .mxp-hint.on{opacity:1;scale:1}
31
+ .mxp-hint b{color:#f5c518;font-size:28px;line-height:1}
32
+ @media (prefers-reduced-motion:reduce){.mxp-hint{transition:opacity .15s linear;scale:1}}
33
  .mxp-count{position:absolute;inset:0;display:grid;place-items:center;font-size:30vh;font-weight:700;color:#f5c518;background:rgba(0,0,0,.6);pointer-events:none}
34
  `;
35
+ let hintTimer=0,wheel=0,keepOpen=false,root,scroller,textEl,guide,bar,raf=0,last=0,playing=false,speed=40,size=64,mirror=false,acc=0,countTimer=0,back=null;
36
 
37
  function save(){try{localStorage.setItem("mathly.prompter",JSON.stringify({speed,size,mirror}))}catch{}}
38
  function load(){try{const s=JSON.parse(localStorage.getItem("mathly.prompter")||"{}");if(s.speed)speed=s.speed;if(s.size)size=s.size;mirror=!!s.mirror}catch{}}
 
44
 
45
  function sync(){
46
  textEl.style.fontSize=size+"px"; guide.style.height=(size*1.45)+"px";
47
+ textEl.style.paddingTop=guide.offsetTop+"px"; // the first line starts on the reading line
48
  root.classList.toggle("mirror",mirror); root.classList.toggle("playing",playing);
49
  bar.querySelector(".mxp-play").textContent=playing?"โธ ืขืฆื™ืจื”":"โ–ถ ื”ืคืขืœื”";
50
  bar.querySelector("[data-p=mirror]").setAttribute("aria-pressed",String(mirror));
 
80
  countTimer=setInterval(()=>{n--; if(n>0) el.textContent=n; else play(true);},700);
81
  }
82
 
83
+ // a short note at the bottom centre that fades in, stays a moment and fades out, so a mouse or clicker action is seen
84
+ function hint(icon,text){
85
+ const el=root?.querySelector(".mxp-hint"); if(!el) return;
86
+ el.innerHTML=""; const b=document.createElement("b"); b.textContent=icon; const span=document.createElement("span"); span.textContent=text;
87
+ el.append(b,span); el.classList.add("on");
88
+ clearTimeout(hintTimer); hintTimer=setTimeout(()=>el.classList.remove("on"),1100);
89
+ }
90
+ const HINTS={
91
+ play:()=>playing||root.querySelector(".mxp-count")?["โ–ถ","ื”ืคืขืœื”"]:["โธ","ืขืฆื™ืจื”"],
92
+ faster:()=>["โฉ",`ืžื”ื™ืจื•ืช ${speed}`+(speed>=300?" (ืžืงืกื™ืžื•ื)":"")], slower:()=>["โช",`ืžื”ื™ืจื•ืช ${speed}`+(speed<=5?" (ืžื™ื ื™ืžื•ื)":"")],
93
+ bigger:()=>["A+",`ื’ื•ื“ืœ ${size}`], smaller:()=>["Aโˆ’",`ื’ื•ื“ืœ ${size}`],
94
+ mirror:()=>["โ‡‹",mirror?"ืžืจืื”: ืžื•ืคืขืœืช":"ืžืจืื”: ื›ื‘ื•ื™ื”"], top:()=>["โฎ","ืœื”ืชื—ืœื”"], back:()=>["โ†ถ","ื›ืžื” ืฉื•ืจื•ืช ืื—ื•ืจื”"],
95
+ };
96
+
97
  function act(p){
98
+ actOnly(p);
99
+ if(root&&HINTS[p]) hint(...HINTS[p]());
100
+ }
101
+
102
+ function actOnly(p){
103
  switch(p){
104
  case "play": return countdown();
105
  case "slower": speed=Math.max(5,speed-5); break;
 
148
  <button data-p="close" title="ืกื’ื™ืจื” (Esc)" aria-label="ืกื’ื™ืจื”">โœ•</button></div>
149
  <div class="mxp-progress"><i></i></div>
150
  <div class="mxp-view"><div class="mxp-scroll"><div class="mxp-text" dir="auto"></div></div>
151
+ <div class="mxp-fade top"></div><div class="mxp-fade bot"></div><div class="mxp-guide"></div>
152
+ <div class="mxp-hint" role="status" aria-live="polite"></div></div>`;
153
  scroller=root.querySelector(".mxp-scroll"); textEl=root.querySelector(".mxp-text"); guide=root.querySelector(".mxp-guide"); bar=root.querySelector(".mxp-bar");
154
  textEl.textContent=text;
155
  bar.addEventListener("click",e=>{const b=e.target.closest("[data-p]"); if(b) act(b.dataset.p);});
156
  bar.querySelector(".mxp-speed").addEventListener("input",e=>{speed=+e.target.value; save(); sync();});
157
  scroller.addEventListener("scroll",progress,{passive:true});
158
  const view=root.querySelector(".mxp-view");
159
+ view.addEventListener("click",()=>act("play"));
160
  view.addEventListener("wheel",e=>{ // while playing the wheel is a speed knob: down faster, up slower
161
  if(!playing){acc=0; return;}
162
  e.preventDefault(); wheel+=e.deltaMode?e.deltaY*40:e.deltaY;
 
179
 
180
  function close(){
181
  if(!root) return;
182
+ clearTimeout(hintTimer);
183
  play(false); document.removeEventListener("keydown",onKey,true); document.removeEventListener("fullscreenchange",onFull);
184
  if(document.fullscreenElement) document.exitFullscreen?.();
185
  root.remove(); root=null; back?.focus?.(); back=null;
tests/test_phone_browser.py CHANGED
@@ -1228,6 +1228,10 @@ def test_prompter_from_management(phone_server):
1228
  page.locator(".mxp-view").hover()
1229
  page.mouse.wheel(0, 120) # while playing, the wheel is the speed
1230
  playwright.expect(page.locator(".mxp-speed-v")).not_to_have_text(speed)
 
 
 
 
1231
  page.locator(".mxp-view").click() # a click pauses
1232
  playwright.expect(prompter).to_have_class("mxp mirror")
1233
  page.keyboard.press("PageDown") # a presentation clicker plays
 
1228
  page.locator(".mxp-view").hover()
1229
  page.mouse.wheel(0, 120) # while playing, the wheel is the speed
1230
  playwright.expect(page.locator(".mxp-speed-v")).not_to_have_text(speed)
1231
+ hint = prompter.locator(".mxp-hint") # the effect is shown at the bottom, then fades
1232
+ playwright.expect(hint).to_have_class("mxp-hint on")
1233
+ playwright.expect(hint).to_contain_text("ืžื”ื™ืจื•ืช 45")
1234
+ playwright.expect(hint).to_have_class("mxp-hint", timeout=3000)
1235
  page.locator(".mxp-view").click() # a click pauses
1236
  playwright.expect(prompter).to_have_class("mxp mirror")
1237
  page.keyboard.press("PageDown") # a presentation clicker plays