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, classes}:{children:preact.VNode, icon?:preact.VNode, light:boolean, disabled:boolean, inactive:boolean, onClick:()=>void, classes?:string})=>preact.VNode} */ export function Button({children, icon, light, disabled, inactive, onClick, classes}) { 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(); const grade = Store.Grade(State.Live.Test); /** @type {(e:Event)=>void} */ const handleChange =(e)=> Dispatch({Name:"Test", Data:parseInt(/** @type {HTMLSelectElement}*/(e.target).value)}); return html`
Select Test
Progress
Complete: ${grade.Done} of ${grade.Total}
Accuracy: ${grade.Score}%
Display
<${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 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, 800 + Math.random()*1300); } } return () => clearTimeout(timer); }, [playGet]); const classTitle = "flex-1 text-sm" return html`
Channel
<${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}/>
Stimulus
${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}/>
${playGet == 2 && html``}
<${Button} classes="w-full flex-1 self-center" onClick=${()=>playSet(1)} disabled=${playGet==1} icon=${html` `} > Present Tone
<${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
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 == 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` ${ 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 in 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}/>` } `; };