import React from "react"; import { html } from "htm"; import * as Store from "./store.js"; import * as Tone from "./tone.js"; /** @typedef {({children}:{children?:preact.ComponentChildren})=>preact.VNode} BasicElement */ /** @type {({children, icon, light, disabled, inactive, onClick}:{children:preact.VNode, icon?:preact.VNode, light:boolean, disabled:boolean, inactive:boolean, onClick:()=>void})=>preact.VNode} */ export function Button({children, icon, light, disabled, inactive, onClick}) { const [FlashGet, FlashSet] = React.useState(0); const handleClick =()=> { if(inactive||disabled){ return; } FlashSet(FlashGet+1); onClick(); }; return html` `; } /** @type {BasicElement} */ export const Select =()=> { const [State, Dispatch] = Store.Consumer(); /** @type {(e:Event)=>void} */ const handleChange =(e)=> Dispatch({Name:"Test", Data:parseInt(/** @type {HTMLSelectElement}*/(e.target).value)}); return html`
`; } /** @type {BasicElement} */ export const Grade =()=> { const [State] = Store.Consumer(); const grade = Store.Grade(State.Live.Test); return html`
Complete: ${grade.Done} of ${grade.Total}
Accuracy: ${grade.Score}%
` }; /** @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 testMark = State.Live.Freq[/** @type {"TestL"|"TestR"}*/(`Test${State.Chan.Value ? "R":"L"}`)]; const handler = testMark.Stim <= State.Stim.Value ? ()=>{playSet(2)} : ()=>{playSet(0)} timer = setTimeout(handler, 500 + Math.random()*1000); } } return () => clearTimeout(timer); }, [playGet]); return html` <${Grade}/>
Channel
${State.Chan.Value}
<${Button} light=${State.Chan.Value == 0} inactive=${State.Chan.Value == 0} onClick=${()=>Dispatch({Name:"Chan", Data:-1})}>Left <${Button} light=${State.Chan.Value == 1} inactive=${State.Chan.Value == 1} onClick=${()=>Dispatch({Name:"Chan", Data:1})}>Right
Frequency
${Store.ColumnMapping[State.Freq.Value][0]}
<${Button} disabled=${State.Freq.Value == State.Freq.Min} onClick=${()=>Dispatch({Name:"Freq", Data:-1})}>- <${Button} disabled=${State.Freq.Value == State.Freq.Max} onClick=${()=>Dispatch({Name:"Freq", Data:1})}>+
Stimulus
${State.Stim.Value}
<${Button} disabled=${State.Stim.Value == State.Stim.Min} onClick=${()=>Dispatch({Name:"Stim", Data:-1})}>- <${Button} disabled=${State.Stim.Value == State.Stim.Max} onClick=${()=>Dispatch({Name:"Stim", Data:1})}>+
Tone
<${Button} onClick=${()=>{pulsedSet(true)}} light=${pulsedGet} inactive${pulsedGet}>Pulsed <${Button} onClick=${()=>{pulsedSet(false)}} light=${!pulsedGet} inactive${!pulsedGet}>Continuous
Present
${playGet == 2 && html``} <${Button} onClick=${()=>playSet(1)} disabled=${playGet==1} icon=${html``}>Play
Mark
<${Button} onClick=${()=>Dispatch({Name:"Mark", Data:true })} icon=${html`<${Mark} right=${State.Chan.Value} response=${true} x="0" y="0" classes="stroke(white 2 draw) w-2 h-2 translate-x-1/2 translate-y-1/2"/>`}>Response <${Button} onClick=${()=>Dispatch({Name:"Mark", Data:false})} icon=${html`<${Mark} right=${State.Chan.Value} response=${false} x="0" y="0" classes="stroke(white 2 draw) w-2 h-2 translate-x-1/2 translate-y-1/2"/>`}>No Response <${Button} icon=${html` `} onClick=${()=>Dispatch({Name:"Mark", Data:null })} disabled=${State.Live.Mark == 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 == 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 == 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` ${testMarksL}${testLinesL} ${testMarksR}${testLinesR} ${userMarksL}${userLinesL} ${userMarksR}${userLinesR} `; }; /** @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 in Hz Hearing Level (dbHL)
${ rules }
${ children }
`; } /** @type {Record} */ const Glyph = { Arrow:()=> html` `, X: ({children})=> html` ${children}`, O: ({children})=> html` ${children}` }; /** @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}/>` } `; };