lysine commited on
Commit
22c6232
·
1 Parent(s): d0af1c9

Polish compare page, remove identify page

Browse files
src/app/App.tsx CHANGED
@@ -1,16 +1,16 @@
1
  import React, { useEffect, useState } from 'react';
2
- import { redirect } from 'react-router-dom';
3
  import { createBrowserRouter, RouterProvider } from 'react-router-dom';
4
  import Compare from './marrow-cells/Compare';
5
- // import Identify from './marrow-cells/Identify';
6
  import { Helmet } from 'react-helmet';
7
  import { CellTypes } from '../marrow-cell-types';
8
  import { getTypes } from './marrow-cells/api';
9
  import { DataProvider } from './DataContext';
10
 
11
  function Redirect({ to }: { to: string }) {
 
12
  useEffect(() => {
13
- redirect(to);
14
  }, []);
15
  return null;
16
  }
@@ -39,7 +39,7 @@ export default function App() {
39
 
40
  return (
41
  <>
42
- <div className="p-8 flex flex-col gap-2">
43
  <Helmet>
44
  <title>Bone Marrow Cell Database</title>
45
  </Helmet>
@@ -64,6 +64,7 @@ export default function App() {
64
  classified into a specific cell type.
65
  </li>
66
  </ul>
 
67
  {cellTypes ? (
68
  <DataProvider data={{ cellTypes }}>
69
  <RouterProvider router={router} />
 
1
  import React, { useEffect, useState } from 'react';
2
+ import { useNavigate } from 'react-router-dom';
3
  import { createBrowserRouter, RouterProvider } from 'react-router-dom';
4
  import Compare from './marrow-cells/Compare';
 
5
  import { Helmet } from 'react-helmet';
6
  import { CellTypes } from '../marrow-cell-types';
7
  import { getTypes } from './marrow-cells/api';
8
  import { DataProvider } from './DataContext';
9
 
10
  function Redirect({ to }: { to: string }) {
11
+ const navigate = useNavigate();
12
  useEffect(() => {
13
+ navigate(to, { replace: true });
14
  }, []);
15
  return null;
16
  }
 
39
 
40
  return (
41
  <>
42
+ <div className="p-8 lg:p-16 flex flex-col gap-2">
43
  <Helmet>
44
  <title>Bone Marrow Cell Database</title>
45
  </Helmet>
 
64
  classified into a specific cell type.
65
  </li>
66
  </ul>
67
+ <div className="divider" />
68
  {cellTypes ? (
69
  <DataProvider data={{ cellTypes }}>
70
  <RouterProvider router={router} />
src/app/marrow-cells/Compare.tsx CHANGED
@@ -1,7 +1,7 @@
1
  import React, { useEffect, useState } from 'react';
2
  import { useSearchParams } from 'react-router-dom';
3
  import { RandomResult } from '../../marrow-cell-types';
4
- import { getImages } from './api';
5
  import { useData } from '../DataContext';
6
 
7
  interface CompareParams {
@@ -69,52 +69,32 @@ export default function Compare(): JSX.Element {
69
 
70
  useEffect(() => {
71
  loadImages(1);
 
72
  }, [cellType1]);
73
 
74
  useEffect(() => {
75
  loadImages(2);
 
76
  }, [cellType2]);
77
 
78
  useEffect(() => {
79
- const newFilter = paramsToCompare(searchParams);
80
- setCellType1(newFilter.type1);
81
- setCellType2(newFilter.type2);
82
- }, [searchParams]);
83
-
84
- const updateParams = (
85
- type1: string | undefined,
86
- type2: string | undefined
87
- ) => {
88
- setSearchParams(compareToParams({ type1, type2 }));
89
- };
90
 
91
  return (
92
  <>
93
- <div className="tabs">
94
- <a
95
- className={`tab tab-bordered ${
96
- location.pathname.startsWith('/compare') ? 'tab-active' : ''
97
- }`}
98
- >
99
- Compare
100
- </a>
101
- <a
102
- className={`tab tab-bordered ${
103
- location.pathname.startsWith('/compare') ? 'tab-active' : ''
104
- }`}
105
- >
106
- Identify
107
- </a>
108
- </div>
109
- <div className="flex w-full items-center gap-4">
110
- <div className="flex flex-col gap-4">
111
  <div className="form-control w-full max-w-xs">
112
  <select
113
  className={`select select-bordered ${
114
  loading1 ? 'animate-pulse' : ''
115
  }`}
116
  disabled={loading1}
117
- onChange={e => updateParams(e.target.value, cellType2)}
 
118
  >
119
  <option disabled selected>
120
  Select a cell type
@@ -129,25 +109,31 @@ export default function Compare(): JSX.Element {
129
  <span
130
  className={`label-text-alt ${!imageSet1 ? 'invisible' : ''}`}
131
  >
132
- {imageSet1?.count ?? 0}
133
  </span>
134
  </label>
135
  </div>
136
- <div className="flex flex-wrap gap-4">
137
  {imageSet1?.images.map(image => (
138
- <img key={image} src={image} className="shadow-lg" alt="Cell" />
 
 
 
 
 
139
  ))}
140
  </div>
141
  </div>
142
- <div className="divider divider-horizontal"></div>
143
- <div className="flex flex-col gap-4">
144
  <div className="form-control w-full max-w-xs">
145
  <select
146
  className={`select select-bordered ${
147
  loading2 ? 'animate-pulse' : ''
148
  }`}
149
  disabled={loading2}
150
- onChange={e => updateParams(e.target.value, cellType2)}
 
151
  >
152
  <option disabled selected>
153
  Select a cell type
@@ -162,13 +148,18 @@ export default function Compare(): JSX.Element {
162
  <span
163
  className={`label-text-alt ${!imageSet2 ? 'invisible' : ''}`}
164
  >
165
- {imageSet2?.count ?? 0}
166
  </span>
167
  </label>
168
  </div>
169
- <div className="flex flex-wrap gap-4">
170
  {imageSet2?.images.map(image => (
171
- <img key={image} src={image} className="shadow-lg" alt="Cell" />
 
 
 
 
 
172
  ))}
173
  </div>
174
  </div>
 
1
  import React, { useEffect, useState } from 'react';
2
  import { useSearchParams } from 'react-router-dom';
3
  import { RandomResult } from '../../marrow-cell-types';
4
+ import { getDataUrl, getImages } from './api';
5
  import { useData } from '../DataContext';
6
 
7
  interface CompareParams {
 
69
 
70
  useEffect(() => {
71
  loadImages(1);
72
+ setSearchParams(compareToParams({ type1: cellType1, type2: cellType2 }));
73
  }, [cellType1]);
74
 
75
  useEffect(() => {
76
  loadImages(2);
77
+ setSearchParams(compareToParams({ type1: cellType1, type2: cellType2 }));
78
  }, [cellType2]);
79
 
80
  useEffect(() => {
81
+ const params = paramsToCompare(searchParams);
82
+ setCellType1(params.type1);
83
+ setCellType2(params.type2);
84
+ }, []);
 
 
 
 
 
 
 
85
 
86
  return (
87
  <>
88
+ <div className="flex w-full items-start gap-4 flex-col md:flex-row">
89
+ <div className="flex flex-1 flex-col gap-4 items-center">
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
90
  <div className="form-control w-full max-w-xs">
91
  <select
92
  className={`select select-bordered ${
93
  loading1 ? 'animate-pulse' : ''
94
  }`}
95
  disabled={loading1}
96
+ value={cellType1}
97
+ onChange={e => setCellType1(e.target.value)}
98
  >
99
  <option disabled selected>
100
  Select a cell type
 
109
  <span
110
  className={`label-text-alt ${!imageSet1 ? 'invisible' : ''}`}
111
  >
112
+ {imageSet1?.count ?? 0} cells of this type found
113
  </span>
114
  </label>
115
  </div>
116
+ <div className="flex flex-wrap gap-4 justify-center">
117
  {imageSet1?.images.map(image => (
118
+ <img
119
+ key={image}
120
+ src={getDataUrl(image)}
121
+ className="shadow-lg"
122
+ alt="Cell"
123
+ />
124
  ))}
125
  </div>
126
  </div>
127
+ <div className="divider md:divider-horizontal"></div>
128
+ <div className="flex flex-1 flex-col gap-4 items-center">
129
  <div className="form-control w-full max-w-xs">
130
  <select
131
  className={`select select-bordered ${
132
  loading2 ? 'animate-pulse' : ''
133
  }`}
134
  disabled={loading2}
135
+ value={cellType2}
136
+ onChange={e => setCellType2(e.target.value)}
137
  >
138
  <option disabled selected>
139
  Select a cell type
 
148
  <span
149
  className={`label-text-alt ${!imageSet2 ? 'invisible' : ''}`}
150
  >
151
+ {imageSet2?.count ?? 0} cells of this type found
152
  </span>
153
  </label>
154
  </div>
155
+ <div className="flex flex-wrap gap-4 justify-center">
156
  {imageSet2?.images.map(image => (
157
+ <img
158
+ key={image}
159
+ src={getDataUrl(image)}
160
+ className="shadow-lg"
161
+ alt="Cell"
162
+ />
163
  ))}
164
  </div>
165
  </div>
src/app/marrow-cells/Identify.tsx DELETED
@@ -1,518 +0,0 @@
1
- import React, { useEffect, useState } from 'react';
2
- import { Link, useSearchParams } from 'react-router-dom';
3
- import { Helmet } from 'react-helmet';
4
- import { FilterParams } from '../../marrow-cell-types';
5
- import { getDataUrl, getImages, getTypes } from './api';
6
-
7
- function filterToParams(filter: FilterParams): URLSearchParams {
8
- const params = new URLSearchParams();
9
- if (filter.type) {
10
- filter.type.forEach(type => params.append('type', type));
11
- }
12
- return params;
13
- }
14
-
15
- function paramsToFilter(params: URLSearchParams): FilterParams {
16
- const newFilter: FilterParams = {};
17
- if (params.has('type')) {
18
- newFilter.type = params.getAll('type') as string[];
19
- }
20
- return newFilter;
21
- }
22
-
23
- export default function Identify(): JSX.Element {
24
- const [searchParams, setSearchParams] = useSearchParams();
25
-
26
- const [patientId, setPatientId] = useState<number | null>(null);
27
- const [resultCount, setResultCount] = useState<number>(-1);
28
-
29
- const [filterParams, setFilterParams] = useState<FilterParams>({});
30
-
31
- const [patient, setPatient] = useState<FullPatient | null>(null);
32
- const [error, setError] = useState<string | null>(null);
33
- const [loading, setLoading] = useState<boolean>(false);
34
-
35
- const [audioZoom, setAudioZoom] = useState(100);
36
- const [regionsLevel, setRegionsLevel] = useState<RegionsLevel>(
37
- RegionsLevel.None
38
- );
39
- const [showAnswer, setShowAnswer] = useState(false);
40
- const [spectrogram, setSpectrogram] = useState(false);
41
-
42
- const getRandom = () => {
43
- setLoading(true);
44
- getRandomPatient(filterParams)
45
- .then(result => {
46
- setPatientId(result.patientId);
47
- setResultCount(result.count);
48
- setSearchParams(p => {
49
- p.set('id', result.patientId.toString());
50
- return p;
51
- });
52
- })
53
- .catch(err => {
54
- if (err.response.status === 404) {
55
- setResultCount(0);
56
- }
57
- })
58
- .finally(() => {
59
- setLoading(false);
60
- });
61
- };
62
-
63
- const loadPatient = () => {
64
- setLoading(true);
65
- getPatient(patientId!)
66
- .then(patient => {
67
- setPatient(patient);
68
- setError(null);
69
- })
70
- .catch(err => {
71
- setPatient(null);
72
- setError(err.response.data.message);
73
- })
74
- .finally(() => {
75
- setLoading(false);
76
- });
77
- };
78
-
79
- useEffect(() => {
80
- setShowAnswer(false);
81
- setFilterParams(paramsToFilter(searchParams));
82
- if (patientId === null) {
83
- if (searchParams.has('id')) {
84
- setPatientId(Number(searchParams.get('id')));
85
- } else {
86
- getRandom();
87
- }
88
- } else {
89
- loadPatient();
90
- }
91
- }, [searchParams, patientId]);
92
-
93
- const randomClicked = () => {
94
- setSearchParams(filterToParams(filterParams));
95
- setPatientId(null);
96
- };
97
-
98
- const arrayToggle = <T extends string>(
99
- array: Exclude<keyof FilterParams, 'murmur'>,
100
- value: T,
101
- checked: boolean
102
- ): void => {
103
- setFilterParams(f => {
104
- if (checked) {
105
- return {
106
- ...f,
107
- [array]: [...(f[array] ?? []), value],
108
- };
109
- } else {
110
- return {
111
- ...f,
112
- [array]: (f[array] as string[] | null)?.filter(v => v !== value),
113
- };
114
- }
115
- });
116
- };
117
-
118
- return (
119
- <div className="p-8 flex flex-col gap-2">
120
- <Helmet>
121
- <title>Heart Sounds Database - auscultate</title>
122
- </Helmet>
123
- <div className="text-sm breadcrumbs flex justify-center w-full">
124
- <ul>
125
- <li>
126
- <a href="https://lysine-med.hf.space/">Med</a>
127
- </li>
128
- <li>
129
- <Link to="/">Auscultation</Link>
130
- </li>
131
- <li>Heart Sounds</li>
132
- </ul>
133
- </div>
134
- <p className="text-3xl text-center">Heart Sounds Database</p>
135
- <p className="text-center">
136
- Filter and access heart sounds from the CirCor DigiScope Phonocardiogram
137
- Dataset.
138
- </p>
139
- <p className="font-bold">Points to note</p>
140
- <ul className="list-disc">
141
- <li>
142
- The provided analysis may not be 100% accurate and may not be the only
143
- abnormalities found.
144
- </li>
145
- <li>
146
- This dataset only records murmur. Other heart sound abnormalities are
147
- not considered.
148
- </li>
149
- <li>
150
- A patient with no detected murmur may still have other undocumented
151
- abnormalities.
152
- </li>
153
- </ul>
154
- <div className="collapse collapse-arrow bg-base-200 my-4">
155
- <input type="checkbox" />
156
- <div className="collapse-title text-xl font-medium">Select Filters</div>
157
- <div className="collapse-content">
158
- <form className="bg-base-200 p-4 pt-0 flex flex-col items-center w-full">
159
- <fieldset className="w-full" disabled={loading}>
160
- <div className="divider">Auscultation location</div>
161
- <div className="flex flex-wrap gap-4 justify-center">
162
- {Object.values(Location).map(loc => (
163
- <div key={loc} className="form-control">
164
- <label className="label cursor-pointer gap-2">
165
- <input
166
- type="checkbox"
167
- checked={filterParams.location?.includes(loc) ?? false}
168
- onChange={e =>
169
- arrayToggle('location', loc, e.target.checked)
170
- }
171
- className="checkbox"
172
- />
173
- <span className="label-text">{nameLocation(loc)}</span>
174
- </label>
175
- </div>
176
- ))}
177
- </div>
178
-
179
- <div className="divider">Murmur Type</div>
180
- <div className="flex flex-wrap gap-4 justify-center">
181
- <div className="form-control">
182
- <label className="label cursor-pointer gap-2">
183
- <input
184
- type="radio"
185
- className="radio"
186
- checked={!filterParams.murmur}
187
- onChange={e => {
188
- if (e.target.checked) {
189
- setFilterParams(f => ({ ...f, murmur: undefined }));
190
- }
191
- }}
192
- />
193
- <span className="label-text">No filter</span>
194
- </label>
195
- </div>
196
- {Object.values(MurmurFilter).map(filter => (
197
- <div key={filter} className="form-control">
198
- <label className="label cursor-pointer gap-2">
199
- <input
200
- type="radio"
201
- className="radio"
202
- checked={filterParams.murmur === filter}
203
- onChange={e => {
204
- if (e.target.checked) {
205
- setFilterParams(f => ({ ...f, murmur: filter }));
206
- }
207
- }}
208
- />
209
- <span className="label-text">{nameMurmur(filter)}</span>
210
- </label>
211
- </div>
212
- ))}
213
- </div>
214
-
215
- <div className="divider">Murmur location</div>
216
- <div className="flex flex-wrap gap-4 justify-center">
217
- {Object.values(Location).map(loc => (
218
- <div key={loc} className="form-control">
219
- <label className="label cursor-pointer gap-2">
220
- <input
221
- type="checkbox"
222
- checked={
223
- filterParams.murmurLocation?.includes(loc) ?? false
224
- }
225
- onChange={e =>
226
- arrayToggle('murmurLocation', loc, e.target.checked)
227
- }
228
- className="checkbox"
229
- />
230
- <span className="label-text">{nameLocation(loc)}</span>
231
- </label>
232
- </div>
233
- ))}
234
- </div>
235
-
236
- <div className="divider">Murmur Timing</div>
237
- <div className="flex flex-wrap gap-4 justify-center">
238
- {Object.values(MurmurTiming).map(loc => (
239
- <div key={loc} className="form-control">
240
- <label className="label cursor-pointer gap-2">
241
- <input
242
- type="checkbox"
243
- checked={filterParams.timing?.includes(loc) ?? false}
244
- onChange={e =>
245
- arrayToggle('timing', loc, e.target.checked)
246
- }
247
- className="checkbox"
248
- />
249
- <span className="label-text">
250
- {nameTiming(
251
- loc,
252
- ['systolic', 'diastolic'].includes(
253
- filterParams.murmur ?? ''
254
- )
255
- ? (filterParams.murmur as 'systolic' | 'diastolic')
256
- : 'general'
257
- )}
258
- </span>
259
- </label>
260
- </div>
261
- ))}
262
- </div>
263
-
264
- <div className="divider">Murmur Shape</div>
265
- <div className="flex flex-wrap gap-4 justify-center">
266
- {Object.values(MurmurShape).map(loc => (
267
- <div key={loc} className="form-control">
268
- <label className="label cursor-pointer gap-2">
269
- <input
270
- type="checkbox"
271
- checked={filterParams.shape?.includes(loc) ?? false}
272
- onChange={e =>
273
- arrayToggle('shape', loc, e.target.checked)
274
- }
275
- className="checkbox"
276
- />
277
- <span className="label-text">{loc}</span>
278
- </label>
279
- </div>
280
- ))}
281
- </div>
282
-
283
- <div className="divider">Murmur Grading</div>
284
- <div className="flex flex-wrap gap-4 justify-center">
285
- {Object.values(MurmurGrading).map(loc => (
286
- <div key={loc} className="form-control">
287
- <label className="label cursor-pointer gap-2">
288
- <input
289
- type="checkbox"
290
- checked={filterParams.grading?.includes(loc) ?? false}
291
- onChange={e =>
292
- arrayToggle('grading', loc, e.target.checked)
293
- }
294
- className="checkbox"
295
- />
296
- <span className="label-text">
297
- {nameGrading(
298
- loc,
299
- ['systolic', 'diastolic'].includes(
300
- filterParams.murmur ?? ''
301
- )
302
- ? (filterParams.murmur as 'systolic' | 'diastolic')
303
- : 'general'
304
- )}
305
- </span>
306
- </label>
307
- </div>
308
- ))}
309
- </div>
310
-
311
- <div className="divider">Murmur Pitch</div>
312
- <div className="flex flex-wrap gap-4 justify-center">
313
- {Object.values(MurmurPitch).map(loc => (
314
- <div key={loc} className="form-control">
315
- <label className="label cursor-pointer gap-2">
316
- <input
317
- type="checkbox"
318
- checked={filterParams.pitch?.includes(loc) ?? false}
319
- onChange={e =>
320
- arrayToggle('pitch', loc, e.target.checked)
321
- }
322
- className="checkbox"
323
- />
324
- <span className="label-text">{loc}</span>
325
- </label>
326
- </div>
327
- ))}
328
- </div>
329
-
330
- <div className="divider">Murmur Quality</div>
331
- <div className="flex flex-wrap gap-4 justify-center">
332
- {Object.values(MurmurQuality).map(loc => (
333
- <div key={loc} className="form-control">
334
- <label className="label cursor-pointer gap-2">
335
- <input
336
- type="checkbox"
337
- checked={filterParams.quality?.includes(loc) ?? false}
338
- onChange={e =>
339
- arrayToggle('quality', loc, e.target.checked)
340
- }
341
- className="checkbox"
342
- />
343
- <span className="label-text">{loc}</span>
344
- </label>
345
- </div>
346
- ))}
347
- </div>
348
-
349
- <div className="divider">Heart Outcome</div>
350
- <div className="flex flex-wrap gap-4 justify-center">
351
- <div className="form-control">
352
- <label className="label cursor-pointer gap-2">
353
- <input
354
- type="radio"
355
- className="radio"
356
- checked={!filterParams.outcome}
357
- onChange={e => {
358
- if (e.target.checked) {
359
- setFilterParams(f => ({ ...f, outcome: undefined }));
360
- }
361
- }}
362
- />
363
- <span className="label-text">No filter</span>
364
- </label>
365
- </div>
366
- {Object.values(Outcome).map(filter => (
367
- <div key={filter} className="form-control">
368
- <label className="label cursor-pointer gap-2">
369
- <input
370
- type="radio"
371
- className="radio"
372
- checked={filterParams.outcome === filter}
373
- onChange={e => {
374
- if (e.target.checked) {
375
- setFilterParams(f => ({ ...f, outcome: filter }));
376
- }
377
- }}
378
- />
379
- <span className="label-text">{filter}</span>
380
- </label>
381
- </div>
382
- ))}
383
- </div>
384
- </fieldset>
385
- </form>
386
- </div>
387
-
388
- <button
389
- className="btn btn-primary"
390
- onClick={randomClicked}
391
- disabled={loading}
392
- >
393
- {loading ? (
394
- <span className="loading loading-spinner loading-sm"></span>
395
- ) : (
396
- 'Random Patient'
397
- )}
398
- </button>
399
- </div>
400
- {resultCount < 0 ? null : (
401
- <p>{resultCount} patients with the selected filters.</p>
402
- )}
403
- {error === null ? null : (
404
- <div className="alert alert-error">
405
- <svg
406
- xmlns="http://www.w3.org/2000/svg"
407
- className="stroke-current shrink-0 h-6 w-6"
408
- fill="none"
409
- viewBox="0 0 24 24"
410
- >
411
- <path
412
- strokeLinecap="round"
413
- strokeLinejoin="round"
414
- strokeWidth="2"
415
- d="M10 14l2-2m0 0l2-2m-2 2l-2-2m2 2l2 2m7-2a9 9 0 11-18 0 9 9 0 0118 0z"
416
- />
417
- </svg>
418
- <div>
419
- <h3 className="font-bold">
420
- An error occurred while loading the patient
421
- </h3>
422
- <div className="text-xs">{error}</div>
423
- </div>
424
- </div>
425
- )}
426
- <div className="divider"></div>
427
- {patient === null ? null : (
428
- <div>
429
- <Demographics patient={patient} />
430
- <div className="flex gap-4 my-4 justify-end flex-wrap">
431
- <div className="flex items-center gap-2">
432
- <span>Zoom: </span>
433
- <input
434
- type="range"
435
- min="20"
436
- max="1000"
437
- value={audioZoom}
438
- onChange={e => setAudioZoom(Number(e.target.value))}
439
- className="range range-sm min-w-[250px] w-1/4"
440
- />
441
- </div>
442
- </div>
443
- {patient.tracks.map(track => (
444
- <AuscultationTrack
445
- key={track.audioFile}
446
- patient={patient}
447
- track={track}
448
- zoom={audioZoom}
449
- showAnswer={showAnswer}
450
- spectrogram={showAnswer && spectrogram}
451
- regionsLevel={showAnswer ? regionsLevel : RegionsLevel.None}
452
- />
453
- ))}
454
- <div className="collapse collapse-arrow bg-base-200">
455
- <input
456
- type="checkbox"
457
- checked={showAnswer}
458
- onChange={e => setShowAnswer(e.target.checked)}
459
- />
460
- <div className="collapse-title text-xl font-medium">
461
- Heart Sound Analysis
462
- </div>
463
- <div className="collapse-content">
464
- <div className="p-4 pt-0 flex flex-col items-center w-full gap-4">
465
- <p className="text-lg">{getMurmurDescription(patient)}</p>
466
- {patient.murmur !== MurmurStatus.Present ? null : (
467
- <p>
468
- All audible locations:{' '}
469
- {patient.murmurLocations.map(loc => (
470
- <kbd className="kbd" key={loc}>
471
- {nameLocation(loc)}
472
- </kbd>
473
- ))}
474
- </p>
475
- )}
476
- <p className="text-lg">Heart outcome: {patient.outcome}</p>
477
- <div className="flex gap-8 my-4 justify-end flex-wrap">
478
- <div className="flex items-center gap-4">
479
- <span className="label-text">Annotations:</span>
480
- <div>
481
- <input
482
- type="range"
483
- min="0"
484
- max="3"
485
- value={regionsLevel}
486
- onChange={e => setRegionsLevel(Number(e.target.value))}
487
- className="range"
488
- step="1"
489
- />
490
- <div className="w-full flex justify-between text-[5px] px-2">
491
- <span>|</span>
492
- <span>|</span>
493
- <span>|</span>
494
- <span>|</span>
495
- </div>
496
- </div>
497
- </div>
498
- <div className="divider-vertical" />
499
- <div className="form-control">
500
- <label className="label cursor-pointer flex gap-4">
501
- <span className="label-text">Show spectrogram:</span>
502
- <input
503
- type="checkbox"
504
- className="toggle"
505
- checked={spectrogram}
506
- onChange={e => setSpectrogram(e.target.checked)}
507
- />
508
- </label>
509
- </div>
510
- </div>
511
- </div>
512
- </div>
513
- </div>
514
- </div>
515
- )}
516
- </div>
517
- );
518
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
src/app/marrow-cells/api.ts CHANGED
@@ -19,5 +19,5 @@ export async function getTypes(): Promise<CellTypes> {
19
  }
20
 
21
  export function getDataUrl(filename: string): string {
22
- return `${SERVER_BASE_PATH}marrow-cell-data/bone_marrow_cecll_dataset/${filename}`;
23
  }
 
19
  }
20
 
21
  export function getDataUrl(filename: string): string {
22
+ return `${SERVER_BASE_PATH}marrow-cell-data/bone_marrow_cell_dataset/${filename}`;
23
  }
src/lib/marrow-cells/api.ts CHANGED
@@ -52,12 +52,12 @@ router.get(
52
  }
53
  const type = typeOptions[Math.floor(Math.random() * typeOptions.length)];
54
  const images = cellImages[type].slice();
55
- // randomly sample at least 10 images
56
- const sampleSize = Math.min(10, images.length);
57
  const sample: string[] = [];
58
  for (let i = 0; i < sampleSize; i++) {
59
  const index = Math.floor(Math.random() * images.length);
60
- sample.push(images[index]);
61
  images.splice(index, 1);
62
  }
63
  res.status(200).json({
 
52
  }
53
  const type = typeOptions[Math.floor(Math.random() * typeOptions.length)];
54
  const images = cellImages[type].slice();
55
+ // randomly sample at most 16 images
56
+ const sampleSize = Math.min(16, images.length);
57
  const sample: string[] = [];
58
  for (let i = 0; i < sampleSize; i++) {
59
  const index = Math.floor(Math.random() * images.length);
60
+ sample.push(images[index].replace('\\', '/'));
61
  images.splice(index, 1);
62
  }
63
  res.status(200).json({