import React from "react"; import { html } from "htm"; import * as Store from "./store.js"; import * as Tone from "./tone.js"; /** @typedef {({children, classes}:{children?:preact.ComponentChildren, classes?:string})=>preact.VNode} BasicElement */ /** @type {({children, icon, light, disabled, inactive, onClick, classes, classesActive}:{children:preact.VNode, icon?:preact.VNode, light:boolean, disabled:boolean, inactive:boolean, onClick:()=>void, classes?:string, classesActive?:string})=>preact.VNode} */ export function Button({children, icon, light, disabled, inactive, onClick, classes, classesActive}) { const [FlashGet, FlashSet] = React.useState(0); const handleClick =()=> { if(inactive||disabled){ return; } FlashSet(FlashGet+1); onClick(); }; return html` `; } const staticPath = await import.meta.resolve("$/"); /** @type {BasicElement} */ export const Header =()=> { const [State, Dispatch] = Store.Consumer(); const grade = State.Live.Test?.Done || {Marks:0, Total:0, Score:0}; /** @type {(e:Event)=>void} */ const handleChange =(e)=> Dispatch({Name:"Test", Data:parseInt(/** @type {HTMLSelectElement}*/(e.target).value)}); return html`

Patient Reliability:

<${Button} inactive=${State.Errs == 0} light=${State.Errs == 0} classes="flex-[1.5] text-xs" classesActive="" onClick=${()=>Dispatch({Name:"Errs", Data:0})}>Perfect(Training Mode) <${Button} inactive=${State.Errs == 1} light=${State.Errs == 1} classes="flex-1 text-xs" classesActive="bg-yellow-600" onClick=${()=>Dispatch({Name:"Errs", Data:1})}>Good <${Button} inactive=${State.Errs == 2} light=${State.Errs == 2} classes="flex-1 text-xs" classesActive="bg-orange-600" onClick=${()=>Dispatch({Name:"Errs", Data:2})}>Reduced <${Button} inactive=${State.Errs == 3} light=${State.Errs == 3} classes="flex-1 text-xs" classesActive="bg-red-600" onClick=${()=>Dispatch({Name:"Errs", Data:3})}>Poor
Complete: ${grade.Marks} of ${grade.Total}
Accuracy: ${grade.Score}%
<${Button} disabled=${grade.Marks == 0} classes="flex-1 text-xs" onClick=${()=>Dispatch({Name:"Kill", Data:0})}>Start Over
`; } /** @type {BasicElement} */ export const Display =()=> { const [State, Dispatch] = Store.Consumer(); return html`

Toggle Overlay

<${Button} light=${State.Show.Cursor} classes="flex-1 text-xs" onClick=${()=>Dispatch({Name:"ShowCursor", Data:!State.Show.Cursor})}>Cursor <${Button} light=${State.Show.Answer} classes="flex-1 text-xs" onClick=${()=>Dispatch({Name:"ShowAnswer", Data:!State.Show.Answer})}>Answer
`; }; /** @type {BasicElement} */ export const Controls =()=> { const [State, Dispatch] = Store.Consumer(); const [pulsedGet, pulsedSet] = React.useState(true); const [playGet, playSet] = React.useState(0); React.useEffect(()=> { /** @type {number|undefined} */ let timer; if(playGet == 1) { const volNorm = (State.Stim.Value-State.Stim.Min)/(State.Stim.Max-State.Stim.Min); Tone.Play(!pulsedGet, State.Chan.Value, Store.ColumnMapping[State.Freq.Value][0], (volNorm*0.8) + 0.1); if(State.Live.Freq) { const audible = State.Stim.Value >= (State.Live.Mark.Test?.Stim??0); const errorScaled = State.Live.Mark.Errs; const errorSampled = Math.random() < errorScaled; const percieved = errorSampled ? !audible : audible; const handler = percieved ? ()=>playSet(2) : ()=>playSet(0); console.log("Audible:", audible, "Error Scaled:", errorScaled, "Error Sampled:", errorSampled, "Percieved", percieved); timer = setTimeout(handler, 800 + Math.random()*1300); } } return () => clearTimeout(timer); }, [playGet]); const classTitle = "flex-1 text-sm" return html`
<${Button} inactive=${State.Chan.Value == 0} light=${State.Chan.Value == 0} classes="flex-1" onClick=${()=>Dispatch({Name:"Chan", Data:-1})}>Left <${Button} inactive=${State.Chan.Value == 1} light=${State.Chan.Value == 1} classes="flex-1" onClick=${()=>Dispatch({Name:"Chan", Data:1})}>Right

Frequency

${Store.ColumnMapping[State.Freq.Value][0]} Hz
<${Button} disabled=${State.Freq.Value == State.Freq.Min} onClick=${()=>Dispatch({Name:"Freq", Data:-1})}> <${Glyph.Minus}/> <${Button} disabled=${State.Freq.Value == State.Freq.Max} onClick=${()=>Dispatch({Name:"Freq", Data:1})}> <${Glyph.Plus}/>

Level

${State.Stim.Value} dbHL
<${Button} disabled=${State.Stim.Value == State.Stim.Min} onClick=${()=>Dispatch({Name:"Stim", Data:-1})}> <${Glyph.Minus}/> <${Button} disabled=${State.Stim.Value == State.Stim.Max} onClick=${()=>Dispatch({Name:"Stim", Data:1})}> <${Glyph.Plus}/>
<${Button} onClick=${()=>{pulsedSet(true )}} light=${pulsedGet } inactive${pulsedGet } classes="flex-1 text(center xs)">Pulsed <${Button} onClick=${()=>{pulsedSet(false)}} light=${!pulsedGet} inactive${!pulsedGet} classes="flex-1 text(center xs)">Continuous
<${Button} classes="w-full flex-1 self-center" onClick=${()=>playSet(1)} disabled=${playGet==1} icon=${html` `} > Present Tone

Response ${State.Live.Mark.Errs > 0 && ` (${State.Live.Mark.Errs*100}% Error Chance)` }:

${playGet == 2 && html``}
Threshold <${Button} onClick=${()=>Dispatch({Name:"Mark", Data:true })} classes="text-md w-full" icon=${html` <${State.Chan.Value ? Glyph.O : Glyph.X}/> `} > Accept <${Button} onClick=${()=>Dispatch({Name:"Mark", Data:false})} classes="text-sm w-full" icon=${html` <${State.Chan.Value ? Glyph.O : Glyph.X}> <${Glyph.Arrow}/> `} > No Response <${Button} icon=${html` <${Glyph.Null}/> `} onClick=${()=>Dispatch({Name:"Mark", Data:null })} classes="text-sm w-full" disabled=${State.Live.Mark.User == undefined} > Clear
`; }; /** @type {BasicElement} */ export const Audiogram =()=> { const [State] = Store.Consumer(); const testMarksL = State.Draw.TestL.Points.map(p=>html`<${Mark} x=${p.X} y=${p.Y} response=${p.Mark?.Resp} right=${false}/>`); const userMarksL = State.Draw.UserL.Points.map(p=>html`<${Mark} x=${p.X} y=${p.Y} response=${p.Mark?.Resp} right=${false} classes=${State.Live.Mark.User == p.Mark ? "stroke-bold":""}/>`); const testMarksR = State.Draw.TestR.Points.map(p=>html`<${Mark} x=${p.X} y=${p.Y} response=${p.Mark?.Resp} right=${true} />`); const userMarksR = State.Draw.UserR.Points.map(p=>html`<${Mark} x=${p.X} y=${p.Y} response=${p.Mark?.Resp} right=${true} classes=${State.Live.Mark.User == p.Mark ? "stroke-bold":""}/>`); const testLinesL = State.Draw.TestL.Paths.map( p=>html``); const userLinesL = State.Draw.UserL.Paths.map( p=>html``); const testLinesR = State.Draw.TestR.Paths.map( p=>html``); const userLinesR = State.Draw.UserR.Paths.map( p=>html``); return html` ${ State.Show.Answer && html` ${testMarksL}${testLinesL} ${testMarksR}${testLinesR} ` } ${userMarksL}${userLinesL} ${userMarksR}${userLinesR} ${ State.Show.Cursor && html` ` }`; }; /** @type {BasicElement} */ export function Chart({children}) { const [State] = Store.Consumer(); const inset = 20; /** @type {Array} */ const rules = []; Store.ColumnMapping.forEach(([label, position, normal])=> { rules.push(html` ${label} ` ); }); for(let db = State.Stim.Min; db <= State.Stim.Max; db+=10) { rules.push(html` ` ); } return html`
Frequency (Hz) Hearing Level (dbHL)
${ rules }
${ children }
`; } /** @type {Record} */ export const Glyph = { Arrow:()=> html` `, X: ({children})=> html` ${children}`, O: ({children})=> html` ${children}`, Minus:()=>html` `, Plus:()=>html` `, Null:()=>html` ` }; /** @type {({right, response, x, y, classes}:{right:boolean, response?:boolean, x:number|string, y:number|string, classes:string})=>preact.VNode} */ export const Mark =({right, response, x, y, classes})=> { return html` <${ right ? Glyph.O : Glyph.X }> ${ !response && html`<${Glyph.Arrow}/>` } `; };