From 8b217c8ce48a3a83065d689f2ce81713a216ef20 Mon Sep 17 00:00:00 2001 From: Seth Trowbridge Date: Wed, 31 May 2023 18:28:49 -0400 Subject: [PATCH 1/8] misc --- src/store.js | 3 ++- src/twind.js | 2 +- src/ui.js | 3 ++- 3 files changed, 5 insertions(+), 3 deletions(-) diff --git a/src/store.js b/src/store.js index 66e8e85..2269af5 100644 --- a/src/store.js +++ b/src/store.js @@ -44,7 +44,8 @@ const Reselect =(inState, inTest)=> if(plot.Hz == hz) { output.Freq = plot; - output.Mark = plot[`User${inState.Chan.Value ? "R" : "L"}`]; + output.Mark = inState.Chan.Value ? plot.UserR : plot.UserL; + break; } } } diff --git a/src/twind.js b/src/twind.js index 6cbbdf2..84be560 100644 --- a/src/twind.js +++ b/src/twind.js @@ -71,7 +71,7 @@ export const Configure = { } ], [ - 'text-shadow-lcd', {"text-shadow": "0px 2px 2px #00000055"} + 'text-shadow-lcd', {"text-shadow": "0px 1px 1px #00000055"} ], [ 'box-notch', "border-t(1 [#ffffff]) border-r(1 [#ffffff]) border-b(1 [#00000033]) border-l(1 [#00000033]) flex items-center justify-end gap-1 p-2" ], [ "box-buttons", "flex gap-1 items-center p-2 rounded-lg bg-gradient-to-b from-[#00000022] border-b(1 [#ffffff]) border-t(1 [#00000033])"] diff --git a/src/ui.js b/src/ui.js index 24d43f2..0273993 100644 --- a/src/ui.js +++ b/src/ui.js @@ -274,6 +274,7 @@ export const Audiogram =()=> }`; }; + /** @type {BasicElement} */ export function Chart({children}) { @@ -308,7 +309,7 @@ export function Chart({children})
- + ${ rules }
${ children } From de986f131b612e7e3306faf94ab376baff71c05d Mon Sep 17 00:00:00 2001 From: Seth Trowbridge Date: Sat, 3 Jun 2023 11:38:52 -0400 Subject: [PATCH 2/8] reorganization --- deno.json | 20 +++++++------------- deno.map.json | 8 -------- index.html | 19 ++++++++++++------- {src => js}/app.js | 24 +++++++++--------------- {src => js}/store.js | 4 ++-- {src => js}/tone.js | 0 {src => js}/twind.js | 2 +- {src => js}/ui.js | 20 +++++++++++++++----- store.d.ts => ts/store.d.ts | 0 store.test.ts => ts/store.test.ts | 0 10 files changed, 46 insertions(+), 51 deletions(-) delete mode 100644 deno.map.json rename {src => js}/app.js (50%) rename {src => js}/store.js (98%) rename {src => js}/tone.js (100%) rename {src => js}/twind.js (98%) rename {src => js}/ui.js (95%) rename store.d.ts => ts/store.d.ts (100%) rename store.test.ts => ts/store.test.ts (100%) diff --git a/deno.json b/deno.json index 6997ddd..538cf72 100644 --- a/deno.json +++ b/deno.json @@ -1,15 +1,9 @@ { - "compilerOptions": { "types": ["./store"], "checkJs": true }, - "importMap": "./deno.map.json", - "fmt": { - "options": { - "lineWidth": 256 - } - }, - "tasks": { - "dev": "deno task serve & deno task test", - "fmt": "deno fmt --watch", - "serve": "deno run -A --unstable --no-check https://deno.land/std@0.167.0/http/file_server.ts", - "test": "deno test store.test.ts --watch --no-lock --no-check" + "compilerOptions": { "types": ["*.d.ts"], "checkJs": true }, + "imports": { + "@twind/": "https://esm.sh/@twind/", + "react": "https://esm.sh/preact@10.11.3/compat", + "htm": "https://esm.sh/htm@3.1.1/preact", + "app": "./js/app.js" } -} +} \ No newline at end of file diff --git a/deno.map.json b/deno.map.json deleted file mode 100644 index 65001b1..0000000 --- a/deno.map.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "imports": { - "@twind/": "https://esm.sh/@twind/", - "react": "https://esm.sh/preact@10.11.3/compat", - "htm": "https://esm.sh/htm@3.1.1/preact", - "app": "./src/app.js" - } -} diff --git a/index.html b/index.html index 11a419f..c9e0a2a 100644 --- a/index.html +++ b/index.html @@ -1,7 +1,12 @@ - - - -
- - - \ No newline at end of file + + + + + + +
+ + + + + \ No newline at end of file diff --git a/src/app.js b/js/app.js similarity index 50% rename from src/app.js rename to js/app.js index 54c6113..fcc2b29 100644 --- a/src/app.js +++ b/js/app.js @@ -15,22 +15,16 @@ TW.Init(ShadowCSS, ShadowDiv); React.render(html` <${Store.Provider}> +
+ <${UI.Select}/> +
-
-
- -
-
- <${UI.Select}/> -
-
- <${UI.Controls}/> -
-
- <${UI.Chart}> - <${UI.Audiogram}/> - -
+ <${UI.Controls}/> + +
+ <${UI.Chart}> + <${UI.Audiogram}/> +
diff --git a/src/store.js b/js/store.js similarity index 98% rename from src/store.js rename to js/store.js index 2269af5..abe8f9a 100644 --- a/src/store.js +++ b/js/store.js @@ -240,8 +240,8 @@ export const Initial = Reducer( ] }, {Name:"Test", Data:0}); -/** @type {preact.Context} */ -export const Context = React.createContext([Initial, (_a)=>{}]); + +export const Context = React.createContext(/** @type {Store.Binding} */([Initial, (_a)=>{}])); /** @type {(props:{children:preact.ComponentChildren})=>preact.VNode} */ export const Provider =(props)=> diff --git a/src/tone.js b/js/tone.js similarity index 100% rename from src/tone.js rename to js/tone.js diff --git a/src/twind.js b/js/twind.js similarity index 98% rename from src/twind.js rename to js/twind.js index 84be560..adca149 100644 --- a/src/twind.js +++ b/js/twind.js @@ -2,7 +2,7 @@ import * as TW from "@twind/core@1.0.1"; import TWPreTail from "@twind/preset-tailwind@1.0.1"; import TWPreAuto from "@twind/preset-autoprefix@1.0.1"; -/** @type {TW.TwindConfig} */ +/** @type {TW.TwindUserConfig} */ export const Configure = { theme: { diff --git a/src/ui.js b/js/ui.js similarity index 95% rename from src/ui.js rename to js/ui.js index 0273993..17b955e 100644 --- a/src/ui.js +++ b/js/ui.js @@ -46,6 +46,11 @@ export const Select =()=> return html`
+
+
+ +
+
Select Test
@@ -104,12 +109,17 @@ export const Controls =()=> 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 +
+
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 +
+
+
+
diff --git a/store.d.ts b/ts/store.d.ts similarity index 100% rename from store.d.ts rename to ts/store.d.ts diff --git a/store.test.ts b/ts/store.test.ts similarity index 100% rename from store.test.ts rename to ts/store.test.ts From 54d8928d1116e49c3c1db006e34fa1a53fcf1e52 Mon Sep 17 00:00:00 2001 From: Seth Trowbridge Date: Sun, 4 Jun 2023 13:44:28 -0400 Subject: [PATCH 3/8] layout changes --- js/app.js | 12 +- js/ui.js | 293 +++++++++++++++++++++++++---------------------- ts/store.test.ts | 2 +- 3 files changed, 161 insertions(+), 146 deletions(-) diff --git a/js/app.js b/js/app.js index fcc2b29..e232f32 100644 --- a/js/app.js +++ b/js/app.js @@ -15,17 +15,19 @@ TW.Init(ShadowCSS, ShadowDiv); React.render(html` <${Store.Provider}> -
- <${UI.Select}/> -
+
+ <${UI.Header}/> + +
<${UI.Controls}/> - -
<${UI.Chart}> <${UI.Audiogram}/>
+ <${UI.Display}/> + +
`, ShadowDiv); \ No newline at end of file diff --git a/js/ui.js b/js/ui.js index 17b955e..d28ffd7 100644 --- a/js/ui.js +++ b/js/ui.js @@ -36,7 +36,7 @@ export function Button({children, icon, light, disabled, inactive, onClick, clas } /** @type {BasicElement} */ -export const Select =()=> +export const Header =()=> { const [State, Dispatch] = Store.Consumer(); const grade = Store.Grade(State.Live.Test); @@ -45,23 +45,28 @@ export const Select =()=> const handleChange =(e)=> Dispatch({Name:"Test", Data:parseInt(/** @type {HTMLSelectElement}*/(e.target).value)}); return html` -
-
-
- -
+
+ +
+
-
-
Select Test
-
- ${State.Test.map((t, i)=>html``)}
+
+ <${Button} inactive=${State.Chan.Value == 0} light=${State.Chan.Value == 0} classes="flex-1 text-xs" onClick=${()=>Dispatch({Name:"Chan", Data:-1})}>None + <${Button} inactive=${State.Chan.Value == 1} light=${State.Chan.Value == 1} classes="flex-1 text-xs" onClick=${()=>Dispatch({Name:"Chan", Data:1})}>Slight + <${Button} inactive=${State.Chan.Value == 1} light=${State.Chan.Value == 1} classes="flex-1 text-xs" onClick=${()=>Dispatch({Name:"Chan", Data:1})}>Moderate + <${Button} inactive=${State.Chan.Value == 1} light=${State.Chan.Value == 1} classes="flex-1 text-xs" onClick=${()=>Dispatch({Name:"Chan", Data:1})}>Severe +
-
-
Progress
-
+ +
+
Complete: ${grade.Done} of ${grade.Total}
@@ -69,15 +74,24 @@ export const Select =()=>
Accuracy: ${grade.Score}%
-
-
Display
-
+
`; +} + +/** @type {BasicElement} */ +export const Display =()=> +{ + const [State, Dispatch] = Store.Consumer(); + return html` +
+
+
<${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 =()=> @@ -109,135 +123,134 @@ export const Controls =()=> 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 + <${Button} inactive=${State.Chan.Value == 0} light=${State.Chan.Value == 0} classes="flex-1" onClick=${()=>Dispatch({Name:"Chan", Data:-1})}>Left Ear + <${Button} inactive=${State.Chan.Value == 1} light=${State.Chan.Value == 1} classes="flex-1" onClick=${()=>Dispatch({Name:"Chan", Data:1})}>Right Ear
-
- -
-
-
-
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 +
+
+
${Store.ColumnMapping[State.Freq.Value][0]} Hz
+ <${Button} disabled=${State.Freq.Value == State.Freq.Min} onClick=${()=>Dispatch({Name:"Freq", Data:-1})}> + + <${Glyph.Minus}/> + -
- <${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} disabled=${State.Freq.Value == State.Freq.Max} onClick=${()=>Dispatch({Name:"Freq", Data:1})}> + + <${Glyph.Plus}/> + + +
+
+
+
+
${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} + 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 +
+ + + + + + + ${playGet == 2 && html``} + + + + + + + + + + + + + + + + + + + +
-
-
Threshold
-
- <${Button} - onClick=${()=>Dispatch({Name:"Mark", Data:true })} - classes="text-md w-full" - icon=${html` +
+
+
+ <${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` - <${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 - + <${Glyph.Null}/> + + `} + onClick=${()=>Dispatch({Name:"Mark", Data:null })} + classes="text-sm w-full" + disabled=${State.Live.Mark == undefined} + > + Clear + +
@@ -310,8 +323,8 @@ export function Chart({children}) ); } return html` -
-
+
+
Frequency in Hz diff --git a/ts/store.test.ts b/ts/store.test.ts index 39c1ccd..7c29953 100644 --- a/ts/store.test.ts +++ b/ts/store.test.ts @@ -1,5 +1,5 @@ import { assertEquals } from "https://deno.land/std@0.166.0/testing/asserts.ts"; -import { Reducer, ColumnMapping, Initial } from "./src/store.js"; +import { Reducer, ColumnMapping, Initial } from "../js/store.js"; let state:Store.State = { Chan: { Min:0, Max:1, Value:0, Step:1 }, From 6c6220e5db54f2ff6333f2ea3d6d4a2d91d6364b Mon Sep 17 00:00:00 2001 From: Seth Trowbridge Date: Sun, 4 Jun 2023 14:54:46 -0400 Subject: [PATCH 4/8] localstorage started --- js/store.js | 137 +++++++++++++++++++++++++++++++--------------------- 1 file changed, 82 insertions(+), 55 deletions(-) diff --git a/js/store.js b/js/store.js index abe8f9a..20099d7 100644 --- a/js/store.js +++ b/js/store.js @@ -132,6 +132,7 @@ export function Reducer(inState, inAction) clone.Live.Mark = Data !== null ? {Stim:clone.Stim.Value, Resp:Data} : undefined; clone.Live.Freq[key] = clone.Live.Mark; clone.Draw[key] = Redraw(clone.Live.Test, clone.Chan.Value, clone.Stim, true); + SaveTests(clone); } } else if( Name=="Stim" || Name=="Chan" || Name=="Freq") @@ -159,7 +160,86 @@ export function Reducer(inState, inAction) return clone; } -/** @type {Store.State} */ + +/** @type {Store.Test[]} */ +const TestDefault = [ + { + Name: "Patient A Asymmetric Notch", + Plot: + [ + { Hz: 500, TestL: { Stim: 15, Resp: true }, TestR: { Stim: 10, Resp: true } }, + { Hz: 1000, TestL: { Stim: 10, Resp: true }, TestR: { Stim: 10, Resp: true } }, + { Hz: 2000, TestL: { Stim: 15, Resp: true }, TestR: { Stim: 20, Resp: true } }, + { Hz: 3000, TestL: { Stim: 30, Resp: true }, TestR: { Stim: 40, Resp: true } }, + { Hz: 4000, TestL: { Stim: 40, Resp: true }, TestR: { Stim: 55, Resp: true } }, + { Hz: 6000, TestL: { Stim: 35, Resp: true }, TestR: { Stim: 40, Resp: true } }, + { Hz: 8000, TestL: { Stim: 20, Resp: true }, TestR: { Stim: 15, Resp: true } } + ] + }, + { + Name: "Patient B High Freq Hearing Loss", + Plot: + [ + { Hz: 500, TestL: { Stim: 10, Resp: true }, TestR: { Stim: 10, Resp: true } }, + { Hz: 1000, TestL: { Stim: 15, Resp: true }, TestR: { Stim: 10, Resp: true } }, + { Hz: 2000, TestL: { Stim: 10, Resp: true }, TestR: { Stim: 15, Resp: true } }, + { Hz: 3000, TestL: { Stim: 25, Resp: true }, TestR: { Stim: 20, Resp: true } }, + { Hz: 4000, TestL: { Stim: 35, Resp: true }, TestR: { Stim: 35, Resp: true } }, + { Hz: 6000, TestL: { Stim: 50, Resp: true }, TestR: { Stim: 55, Resp: true } }, + { Hz: 8000, TestL: { Stim: 80, Resp: true }, TestR: { Stim: 75, Resp: true } } + ] + }, + { + Name: "Patient C Unilateral Hearing Loss", + Plot: + [ + { Hz: 500, TestL: { Stim: 15, Resp: true }, TestR: { Stim: 40, Resp: true } }, + { Hz: 1000, TestL: { Stim: 15, Resp: true }, TestR: { Stim: 50, Resp: true } }, + { Hz: 2000, TestL: { Stim: 20, Resp: true }, TestR: { Stim: 65, Resp: true } }, + { Hz: 3000, TestL: { Stim: 15, Resp: true }, TestR: { Stim: 70, Resp: true } }, + { Hz: 4000, TestL: { Stim: 20, Resp: true }, TestR: { Stim: 65, Resp: true } }, + { Hz: 6000, TestL: { Stim: 25, Resp: true }, TestR: { Stim: 60, Resp: true } }, + { Hz: 8000, TestL: { Stim: 20, Resp: true }, TestR: { Stim: 45, Resp: true } } + ] + }, + { + Name: "Patient D Normal Hearing", + Plot: + [ + { Hz: 500, TestL: { Stim: 5, Resp: true }, TestR: { Stim: 10, Resp: true } }, + { Hz: 1000, TestL: { Stim: 0, Resp: true }, TestR: { Stim: 5, Resp: true } }, + { Hz: 2000, TestL: { Stim: 5, Resp: true }, TestR: { Stim: 5, Resp: true } }, + { Hz: 3000, TestL: { Stim: 15, Resp: true }, TestR: { Stim: 10, Resp: true } }, + { Hz: 4000, TestL: { Stim: 15, Resp: true }, TestR: { Stim: 15, Resp: true } }, + { Hz: 6000, TestL: { Stim: 5, Resp: true }, TestR: { Stim: 10, Resp: true } }, + { Hz: 8000, TestL: { Stim: 0, Resp: true }, TestR: { Stim: 5, Resp: true } } + ] + } +]; + +/** @type {Store.Test[] | string | null } */ +let TestSaved = localStorage.getItem("app-tests"); +if(TestSaved) +{ + TestSaved = JSON.parse(TestSaved); +} + +/**@type {(inState:Store.State)=>void} */ +const SaveTests =(inState)=> localStorage.setItem("app-tests", JSON.stringify(inState.Test)); + +/**@type {(inState:Store.State)=>void} */ +const SaveSettings =(inState)=> localStorage.setItem("app-settings", JSON.stringify({ + Chan:inState.Chan, + Freq:inState.Freq, + Stim:inState.Stim, + Show:inState.Show, + TestIndex:inState.TestIndex, +})); + + +/** @type {Store.Test[]} */ +const TestActual = Array.isArray(TestSaved) ? TestSaved : TestDefault + export const Initial = Reducer( { Chan: { Min:0, Max:1, Value:0, Step:1 }, @@ -184,60 +264,7 @@ export const Initial = Reducer( Answer:false }, TestIndex: 0, - Test: [ - { - Name: "Patient A Asymmetric Notch", - Plot: - [ - { Hz: 500, TestL: { Stim: 15, Resp: true }, TestR: { Stim: 10, Resp: true } }, - { Hz: 1000, TestL: { Stim: 10, Resp: true }, TestR: { Stim: 10, Resp: true } }, - { Hz: 2000, TestL: { Stim: 15, Resp: true }, TestR: { Stim: 20, Resp: true } }, - { Hz: 3000, TestL: { Stim: 30, Resp: true }, TestR: { Stim: 40, Resp: true } }, - { Hz: 4000, TestL: { Stim: 40, Resp: true }, TestR: { Stim: 55, Resp: true } }, - { Hz: 6000, TestL: { Stim: 35, Resp: true }, TestR: { Stim: 40, Resp: true } }, - { Hz: 8000, TestL: { Stim: 20, Resp: true }, TestR: { Stim: 15, Resp: true } } - ] - }, - { - Name: "Patient B High Freq Hearing Loss", - Plot: - [ - { Hz: 500, TestL: { Stim: 10, Resp: true }, TestR: { Stim: 10, Resp: true } }, - { Hz: 1000, TestL: { Stim: 15, Resp: true }, TestR: { Stim: 10, Resp: true } }, - { Hz: 2000, TestL: { Stim: 10, Resp: true }, TestR: { Stim: 15, Resp: true } }, - { Hz: 3000, TestL: { Stim: 25, Resp: true }, TestR: { Stim: 20, Resp: true } }, - { Hz: 4000, TestL: { Stim: 35, Resp: true }, TestR: { Stim: 35, Resp: true } }, - { Hz: 6000, TestL: { Stim: 50, Resp: true }, TestR: { Stim: 55, Resp: true } }, - { Hz: 8000, TestL: { Stim: 80, Resp: true }, TestR: { Stim: 75, Resp: true } } - ] - }, - { - Name: "Patient C Unilateral Hearing Loss", - Plot: - [ - { Hz: 500, TestL: { Stim: 15, Resp: true }, TestR: { Stim: 40, Resp: true } }, - { Hz: 1000, TestL: { Stim: 15, Resp: true }, TestR: { Stim: 50, Resp: true } }, - { Hz: 2000, TestL: { Stim: 20, Resp: true }, TestR: { Stim: 65, Resp: true } }, - { Hz: 3000, TestL: { Stim: 15, Resp: true }, TestR: { Stim: 70, Resp: true } }, - { Hz: 4000, TestL: { Stim: 20, Resp: true }, TestR: { Stim: 65, Resp: true } }, - { Hz: 6000, TestL: { Stim: 25, Resp: true }, TestR: { Stim: 60, Resp: true } }, - { Hz: 8000, TestL: { Stim: 20, Resp: true }, TestR: { Stim: 45, Resp: true } } - ] - }, - { - Name: "Patient D Normal Hearing", - Plot: - [ - { Hz: 500, TestL: { Stim: 5, Resp: true }, TestR: { Stim: 10, Resp: true } }, - { Hz: 1000, TestL: { Stim: 0, Resp: true }, TestR: { Stim: 5, Resp: true } }, - { Hz: 2000, TestL: { Stim: 5, Resp: true }, TestR: { Stim: 5, Resp: true } }, - { Hz: 3000, TestL: { Stim: 15, Resp: true }, TestR: { Stim: 10, Resp: true } }, - { Hz: 4000, TestL: { Stim: 15, Resp: true }, TestR: { Stim: 15, Resp: true } }, - { Hz: 6000, TestL: { Stim: 5, Resp: true }, TestR: { Stim: 10, Resp: true } }, - { Hz: 8000, TestL: { Stim: 0, Resp: true }, TestR: { Stim: 5, Resp: true } } - ] - } - ] + Test: TestActual }, {Name:"Test", Data:0}); From 3af4718cdb26f43daf6b446e24cd3f8114564a02 Mon Sep 17 00:00:00 2001 From: Seth Trowbridge Date: Wed, 7 Jun 2023 19:30:56 -0400 Subject: [PATCH 5/8] start error ui --- js/app.js | 5 +++-- js/store.js | 17 +++++++++++++++++ js/ui.js | 16 ++++++++++------ ts/store.d.ts | 5 ++++- 4 files changed, 34 insertions(+), 9 deletions(-) diff --git a/js/app.js b/js/app.js index e232f32..1c84348 100644 --- a/js/app.js +++ b/js/app.js @@ -23,10 +23,11 @@ React.render(html` <${UI.Controls}/> <${UI.Chart}> <${UI.Audiogram}/> +
<${UI.Display}/>
- - <${UI.Display}/> + +
diff --git a/js/store.js b/js/store.js index 20099d7..d1f2206 100644 --- a/js/store.js +++ b/js/store.js @@ -148,6 +148,22 @@ export function Reducer(inState, inAction) clone.Live = Reselect(clone); } } + else if (Name == "Errs") + { + clone.Errs = Data; + } + else if (Name == "Kill") + { + console.log(clone.Live.Test); + clone.Live.Test && clone.Live.Test.Plot.forEach(freq=> + { + freq.UserL = undefined; + freq.UserR = undefined; + }); + clone.Draw["UserL"] = Redraw(clone.Live.Test, clone.Chan.Value, clone.Stim, true); + clone.Draw["UserR"] = Redraw(clone.Live.Test, clone.Chan.Value, clone.Stim, true); + SaveTests(clone); + } else if (Name == "ShowCursor") { clone.Show.Cursor = Data; @@ -245,6 +261,7 @@ export const Initial = Reducer( Chan: { Min:0, Max:1, Value:0, Step:1 }, Freq: { Min:2, Max:8, Value:3, Step:1 }, Stim: { Min:-10, Max:120, Value:30, Step:5 }, + Errs: 0, Live: { Test: undefined, diff --git a/js/ui.js b/js/ui.js index d28ffd7..43fcd91 100644 --- a/js/ui.js +++ b/js/ui.js @@ -3,7 +3,7 @@ import { html } from "htm"; import * as Store from "./store.js"; import * as Tone from "./tone.js"; -/** @typedef {({children}:{children?:preact.ComponentChildren})=>preact.VNode} BasicElement */ +/** @typedef {({children, classes}:{children?:preact.ComponentChildren, classes?:string})=>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}) @@ -58,10 +58,11 @@ export const Header =()=>
- <${Button} inactive=${State.Chan.Value == 0} light=${State.Chan.Value == 0} classes="flex-1 text-xs" onClick=${()=>Dispatch({Name:"Chan", Data:-1})}>None - <${Button} inactive=${State.Chan.Value == 1} light=${State.Chan.Value == 1} classes="flex-1 text-xs" onClick=${()=>Dispatch({Name:"Chan", Data:1})}>Slight - <${Button} inactive=${State.Chan.Value == 1} light=${State.Chan.Value == 1} classes="flex-1 text-xs" onClick=${()=>Dispatch({Name:"Chan", Data:1})}>Moderate - <${Button} inactive=${State.Chan.Value == 1} light=${State.Chan.Value == 1} classes="flex-1 text-xs" onClick=${()=>Dispatch({Name:"Chan", Data:1})}>Severe +

Patient Error:

+ <${Button} inactive=${State.Errs == 0.0} light=${State.Errs == 0.0} classes="flex-1 text-xs" onClick=${()=>Dispatch({Name:"Errs", Data:0.0})}>None + <${Button} inactive=${State.Errs == 0.1} light=${State.Errs == 0.1} classes="flex-1 text-xs" onClick=${()=>Dispatch({Name:"Errs", Data:0.1})}>Slight + <${Button} inactive=${State.Errs == 0.3} light=${State.Errs == 0.3} classes="flex-1 text-xs" onClick=${()=>Dispatch({Name:"Errs", Data:0.3})}>Moderate + <${Button} inactive=${State.Errs == 0.6} light=${State.Errs == 0.6} classes="flex-1 text-xs" onClick=${()=>Dispatch({Name:"Errs", Data:0.6})}>Severe
@@ -72,8 +73,11 @@ export const Header =()=>
Accuracy: ${grade.Score}%
+ <${Button} disabled=${grade.Done == 0} classes="flex-1 text-xs" onClick=${()=>Dispatch({Name:"Kill", Data:0})}>Start Over
+ +
`; } @@ -323,7 +327,7 @@ export function Chart({children}) ); } return html` -
+
Frequency in Hz diff --git a/ts/store.d.ts b/ts/store.d.ts index 95a60b5..93e7d75 100644 --- a/ts/store.d.ts +++ b/ts/store.d.ts @@ -24,6 +24,7 @@ declare namespace Store { Chan: Range; Freq: Range; Stim: Range; + Errs: number; Live: Context; Draw: DrawChart; Show: {Cursor:boolean, Answer:boolean} @@ -36,9 +37,11 @@ declare namespace Store { type ActionChan = { Name: "Chan"; Data: number }; type ActionFreq = { Name: "Freq"; Data: number }; type ActionStim = { Name: "Stim"; Data: number }; + type ActionErrs = { Name: "Errs"; Data: number }; + type ActionKill = { Name: "Kill"; Data: number }; type ActionShowCursor = {Name: "ShowCursor", Data:boolean}; type ActionShowAnswer = {Name: "ShowAnswer", Data:boolean}; - type Action = ActionMark | ActionTest | ActionChan | ActionFreq | ActionStim | ActionShowCursor | ActionShowAnswer; + type Action = ActionMark | ActionTest | ActionChan | ActionFreq | ActionStim | ActionErrs | ActionKill | ActionShowCursor | ActionShowAnswer; type Reducer = (inState: State, inAction: Action) => State; type ContextUpdater = (inState: State) => boolean; From 576df329463238d5b61242ac7f969b3b48d504b0 Mon Sep 17 00:00:00 2001 From: Seth Trowbridge Date: Wed, 7 Jun 2023 19:31:10 -0400 Subject: [PATCH 6/8] make grade part of state --- js/store.js | 135 +++++++++++++++++++++++++------------------------- js/ui.js | 10 ++-- ts/store.d.ts | 8 ++- 3 files changed, 78 insertions(+), 75 deletions(-) diff --git a/js/store.js b/js/store.js index d1f2206..8b41f12 100644 --- a/js/store.js +++ b/js/store.js @@ -28,6 +28,55 @@ export const ColumnLookup =(inFrequency)=> /** @type {(inDecimal:number)=>string} */ const Perc =(inDecimal)=> `${inDecimal*100}%`; +/** @type {(inTest:Store.Test)=>Store.Grade} */ +export const Grade =(inTest)=> +{ + /** @type {Store.Grade} */ + const output = { Total:0, Marks:0, Score:0 }; + + /** @type {(inGoal:number, inResult:number)=>number} */ + const Mapper =(inGoal, inResult)=> + { + const err = Math.abs(inGoal-inResult); + if(err == 0){ return 1; } + else if(err > 0 && err <= 5){ return 0.9; } + else if(err > 5 && err <= 10){ return 0.7; } + else if(err > 10 && err <= 15){ return 0.2; } + else{ return 0; } + } + + + for(let i=0; i 0) + { + output.Score = Math.floor((output.Score/output.Marks) * 10000)/100; + } + + return output; + +} + /** Creates a new Store.Context object that contain the current selections * @type {(inState:Store.State, inTest?:Store.Test)=>Store.Context} */ const Reselect =(inState, inTest)=> @@ -122,6 +171,7 @@ export function Reducer(inState, inAction) TestL: Redraw(test, 0, clone.Stim, false), TestR: Redraw(test, 1, clone.Stim, false) }; + test.Done = Grade(test); } } else if (Name == "Mark") @@ -132,6 +182,7 @@ export function Reducer(inState, inAction) clone.Live.Mark = Data !== null ? {Stim:clone.Stim.Value, Resp:Data} : undefined; clone.Live.Freq[key] = clone.Live.Mark; clone.Draw[key] = Redraw(clone.Live.Test, clone.Chan.Value, clone.Stim, true); + clone.Live.Test.Done = Grade(clone.Live.Test); SaveTests(clone); } } @@ -154,15 +205,18 @@ export function Reducer(inState, inAction) } else if (Name == "Kill") { - console.log(clone.Live.Test); - clone.Live.Test && clone.Live.Test.Plot.forEach(freq=> + if(clone.Live.Test) { - freq.UserL = undefined; - freq.UserR = undefined; - }); - clone.Draw["UserL"] = Redraw(clone.Live.Test, clone.Chan.Value, clone.Stim, true); - clone.Draw["UserR"] = Redraw(clone.Live.Test, clone.Chan.Value, clone.Stim, true); - SaveTests(clone); + clone.Live.Test.Plot.forEach(freq=> + { + freq.UserL = undefined; + freq.UserR = undefined; + }); + clone.Draw["UserL"] = Redraw(clone.Live.Test, clone.Chan.Value, clone.Stim, true); + clone.Draw["UserR"] = Redraw(clone.Live.Test, clone.Chan.Value, clone.Stim, true); + clone.Live.Test.Done = Grade(clone.Live.Test); + SaveTests(clone); + } } else if (Name == "ShowCursor") { @@ -244,13 +298,11 @@ if(TestSaved) const SaveTests =(inState)=> localStorage.setItem("app-tests", JSON.stringify(inState.Test)); /**@type {(inState:Store.State)=>void} */ -const SaveSettings =(inState)=> localStorage.setItem("app-settings", JSON.stringify({ - Chan:inState.Chan, - Freq:inState.Freq, - Stim:inState.Stim, - Show:inState.Show, - TestIndex:inState.TestIndex, -})); +const SaveSettings =(inState)=> +{ + const clone = {...inState, Test:null, Draw:null, }; + localStorage.setItem("app-settings", JSON.stringify(clone)); +} /** @type {Store.Test[]} */ @@ -296,55 +348,4 @@ export const Provider =(props)=> }; /** @type {()=>Store.Binding} */ -export const Consumer =()=> React.useContext(Context); - -/** @type {(inTest:Store.Test|undefined)=>Store.Grade} */ -export const Grade =(inTest)=> -{ - /** @type {Store.Grade} */ - const output = { Total:0, Done:0, Score:0 }; - - /** @type {(inGoal:number, inResult:number)=>number} */ - const Mapper =(inGoal, inResult)=> - { - const err = Math.abs(inGoal-inResult); - if(err == 0){ return 1; } - else if(err > 0 && err <= 5){ return 0.9; } - else if(err > 5 && err <= 10){ return 0.7; } - else if(err > 10 && err <= 15){ return 0.2; } - else{ return 0; } - } - - if(inTest) - { - for(let i=0; i 0) - { - output.Score = Math.floor((output.Score/output.Done) * 10000)/100; - } - - return output; -} \ No newline at end of file +export const Consumer =()=> React.useContext(Context); \ No newline at end of file diff --git a/js/ui.js b/js/ui.js index 43fcd91..b658267 100644 --- a/js/ui.js +++ b/js/ui.js @@ -39,7 +39,7 @@ export function Button({children, icon, light, disabled, inactive, onClick, clas export const Header =()=> { const [State, Dispatch] = Store.Consumer(); - const grade = Store.Grade(State.Live.Test); + const grade = State.Live.Test?.Done; /** @type {(e:Event)=>void} */ const handleChange =(e)=> Dispatch({Name:"Test", Data:parseInt(/** @type {HTMLSelectElement}*/(e.target).value)}); @@ -68,16 +68,14 @@ export const Header =()=>
-
Complete: ${grade.Done} of ${grade.Total}
+
Complete: ${grade.Marks} of ${grade.Total}
-
+
Accuracy: ${grade.Score}%
- <${Button} disabled=${grade.Done == 0} classes="flex-1 text-xs" onClick=${()=>Dispatch({Name:"Kill", Data:0})}>Start Over + <${Button} disabled=${grade.Marks == 0} classes="flex-1 text-xs" onClick=${()=>Dispatch({Name:"Kill", Data:0})}>Start Over
- -
`; } diff --git a/ts/store.d.ts b/ts/store.d.ts index 93e7d75..8f4aae8 100644 --- a/ts/store.d.ts +++ b/ts/store.d.ts @@ -12,7 +12,11 @@ declare namespace Store { UserR?: TestFrequencySample; }; - type Test = { Name: string; Plot: Array }; + type Test = { + Name: string; + Done?: Grade; + Plot: Array + }; type Context = { Test?: Test; @@ -58,7 +62,7 @@ declare namespace Store { type Grade = { Total:number, - Done:number, + Marks:number, Score:number }; } \ No newline at end of file From ed49a67933a41d20f4bb45debaa59f8beda06731 Mon Sep 17 00:00:00 2001 From: Seth Trowbridge Date: Wed, 7 Jun 2023 20:32:47 -0400 Subject: [PATCH 7/8] human error --- js/ui.js | 27 +++++++++++++++++++++++---- 1 file changed, 23 insertions(+), 4 deletions(-) diff --git a/js/ui.js b/js/ui.js index b658267..6d358d7 100644 --- a/js/ui.js +++ b/js/ui.js @@ -39,7 +39,7 @@ export function Button({children, icon, light, disabled, inactive, onClick, clas export const Header =()=> { const [State, Dispatch] = Store.Consumer(); - const grade = State.Live.Test?.Done; + 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)}); @@ -114,7 +114,26 @@ export const Controls =()=> 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)} + const audible = State.Stim.Value >= testMark.Stim; + + const error = State.Stim.Value - testMark.Stim; + let errorMapped = error; + if(error >= 30){ errorMapped = 0.0; } + else if(error >= 25){ errorMapped = 0.1; } + else if(error >= 20){ errorMapped = 0.2; } + else if(error >= 15){ errorMapped = 0.3; } + else if(error >= 10){ errorMapped = 0.5; } + else if(error >= 5){ errorMapped = 0.7; } + else if(error == 0){ errorMapped = 1.0; } + else if(error >= -5){ errorMapped = 0.5; } + else if(error >=-10){ errorMapped = 0.1; } + else if(error >=-15){ errorMapped = 0.0; } + + const errorScaled = State.Errs*errorMapped; + const errorSampled = Math.random() < errorScaled; + const percieved = errorSampled ? !audible : audible + const handler = percieved ? ()=>playSet(2) : ()=>playSet(0); + console.log("Error:", error, "Error Mapped:", errorMapped, "Error Scaled:", errorScaled, "Error Sampled:", errorSampled); timer = setTimeout(handler, 800 + Math.random()*1300); } } @@ -129,8 +148,8 @@ export const Controls =()=>
- <${Button} inactive=${State.Chan.Value == 0} light=${State.Chan.Value == 0} classes="flex-1" onClick=${()=>Dispatch({Name:"Chan", Data:-1})}>Left Ear - <${Button} inactive=${State.Chan.Value == 1} light=${State.Chan.Value == 1} classes="flex-1" onClick=${()=>Dispatch({Name:"Chan", Data:1})}>Right Ear + <${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
From 34b92973063a59af6fb5f84fd2cfa70a13949338 Mon Sep 17 00:00:00 2001 From: Seth Trowbridge Date: Wed, 7 Jun 2023 21:09:10 -0400 Subject: [PATCH 8/8] persist settings --- js/store.js | 63 +++++++++++++++++++++++++++------------------------ js/ui.js | 2 +- ts/store.d.ts | 20 ++++++++++++---- 3 files changed, 49 insertions(+), 36 deletions(-) diff --git a/js/store.js b/js/store.js index 8b41f12..1d4fa3b 100644 --- a/js/store.js +++ b/js/store.js @@ -161,7 +161,7 @@ export function Reducer(inState, inAction) const test = clone.Test[Data]; if(test) { - clone.TestIndex = Data; + clone.Pick = Data; clone.Live = Reselect(clone, test); clone.Draw = { @@ -227,6 +227,8 @@ export function Reducer(inState, inAction) clone.Show.Answer = Data; } + SaveSettings(clone); + return clone; } @@ -286,34 +288,42 @@ const TestDefault = [ ] } ]; - -/** @type {Store.Test[] | string | null } */ -let TestSaved = localStorage.getItem("app-tests"); -if(TestSaved) -{ - TestSaved = JSON.parse(TestSaved); -} - +/** @type {Store.Test[]} */ +const TestActual = JSON.parse(localStorage.getItem("app-tests")||"false") || TestDefault; /**@type {(inState:Store.State)=>void} */ const SaveTests =(inState)=> localStorage.setItem("app-tests", JSON.stringify(inState.Test)); -/**@type {(inState:Store.State)=>void} */ -const SaveSettings =(inState)=> -{ - const clone = {...inState, Test:null, Draw:null, }; - localStorage.setItem("app-settings", JSON.stringify(clone)); -} - - -/** @type {Store.Test[]} */ -const TestActual = Array.isArray(TestSaved) ? TestSaved : TestDefault - -export const Initial = Reducer( +/** @type {Store.StatePartSimple} */ +const SettingsDefault = { Chan: { Min:0, Max:1, Value:0, Step:1 }, Freq: { Min:2, Max:8, Value:3, Step:1 }, Stim: { Min:-10, Max:120, Value:30, Step:5 }, Errs: 0, + Pick: 0, + Show: { Cursor:true, Answer:false } +}; +/** @type {Store.StatePartSimple} */ +const SettingsActual = JSON.parse(localStorage.getItem("app-settings")||"false") || SettingsDefault; +/**@type {(inState:Store.State)=>void} */ +const SaveSettings =(inState)=> +{ + /** @type {Store.StatePartSimple} */ + const clone = { + Chan:inState.Chan, + Freq:inState.Freq, + Stim:inState.Stim, + Errs:inState.Errs, + Pick:inState.Pick, + Show:inState.Show + }; + localStorage.setItem("app-settings", JSON.stringify(clone)); +}; + +export const Initial = Reducer( +{ + ...SettingsActual, + Test: TestActual, Live: { Test: undefined, @@ -326,15 +336,8 @@ export const Initial = Reducer( UserR:{Points:[], Paths:[]}, TestL:{Points:[], Paths:[]}, TestR:{Points:[], Paths:[]} - }, - Show: - { - Cursor:true, - Answer:false - }, - TestIndex: 0, - Test: TestActual -}, {Name:"Test", Data:0}); + } +}, {Name:"Test", Data:SettingsActual.Pick}); export const Context = React.createContext(/** @type {Store.Binding} */([Initial, (_a)=>{}])); diff --git a/js/ui.js b/js/ui.js index 6d358d7..19b36c1 100644 --- a/js/ui.js +++ b/js/ui.js @@ -53,7 +53,7 @@ export const Header =()=>
- ${State.Test.map((t, i)=>html``)}
diff --git a/ts/store.d.ts b/ts/store.d.ts index 8f4aae8..cdde39f 100644 --- a/ts/store.d.ts +++ b/ts/store.d.ts @@ -13,9 +13,9 @@ declare namespace Store { }; type Test = { - Name: string; + Name : string; Done?: Grade; - Plot: Array + Plot : Array }; type Context = { @@ -24,18 +24,28 @@ declare namespace Store { Mark?: TestFrequencySample; }; - type State = { + type StatePartSimple = + { Chan: Range; Freq: Range; Stim: Range; Errs: number; + Pick: number; + Show: + { + Cursor:boolean, + Answer:boolean + } + }; + type StatePartComplex = + { Live: Context; Draw: DrawChart; - Show: {Cursor:boolean, Answer:boolean} - TestIndex: number; Test: Array; }; + type State = StatePartSimple & StatePartComplex; + type ActionMark = { Name: "Mark"; Data: boolean | null }; type ActionTest = { Name: "Test"; Data: number }; type ActionChan = { Name: "Chan"; Data: number };