From 99d21c093c75bfa771022938c0fe52c74d948fb8 Mon Sep 17 00:00:00 2001 From: "A. Malthe Henriksen" Date: Tue, 20 May 2025 21:05:35 +0200 Subject: [PATCH 01/49] Cleanup, move aplication state into another file --- src/App.svelte | 2 +- .../features/OutdatedMicrobitWarning.svelte | 2 +- .../features/ReconnectPrompt.svelte | 2 +- .../features/bottom/BottomPanel.svelte | 2 +- .../bottom/ConnectedLiveGraphButtons.svelte | 3 +- .../bottom/LiveGraphInformationSection.svelte | 2 +- .../ConnectDialogContainer.svelte | 2 +- .../bluetooth/BluetoothConnectDialog.svelte | 2 +- .../features/datacollection/Gesture.svelte | 4 +- .../features/graphs/LiveGraph.svelte | 3 +- .../graphs/knngraph/KNNModelGraphDrawer.ts | 3 +- .../features/model/ModelGestureStack.svelte | 3 +- src/components/layout/OverlayView.svelte | 2 +- src/components/layout/SideBarMenuView.svelte | 2 +- src/components/sidemenu/ModelMenu.svelte | 3 +- src/lib/domain/stores/HighlightedAxes.ts | 2 +- src/lib/domain/stores/KNNModelSettings.ts | 20 +++--- src/lib/domain/stores/Model.ts | 1 - .../InputMicrobitHandler.ts | 3 +- .../OutputMicrobitHandler.ts | 2 +- src/lib/stores/ModelTraining.ts | 32 ++++++++++ src/lib/stores/Stores.ts | 63 +++---------------- src/lib/stores/applicationState.ts | 56 +++++++++++++++++ src/lib/stores/connectDialogStore.ts | 2 +- src/lib/stores/uiStore.ts | 3 +- src/lib/utils/Recording.ts | 3 +- src/pages/Homepage.svelte | 2 +- src/pages/data/DataPageNoData.svelte | 2 +- src/pages/filter/D3Plot.svelte | 3 +- .../IntroVideoTile.svelte | 2 +- src/pages/model/ModelPage.svelte | 2 +- .../model/stackview/ModelPageStackView.svelte | 3 +- .../ModelPageStackViewContent.svelte | 3 +- .../model/tileview/ModelPageTileView.svelte | 3 +- .../tileview/ModelPageTileViewTiles.svelte | 3 +- .../training/KnnModelTrainingPageView.svelte | 3 +- src/pages/training/PredictionLegend.svelte | 3 +- src/pages/training/TrainingPage.svelte | 3 +- src/pages/training/TrainingPage.ts | 7 +-- .../training/TrainingPageModelView.svelte | 5 +- .../controlbar/TrainingPageTabs.svelte | 2 +- .../ValidationGestureSelectGestureCard.svelte | 3 +- 42 files changed, 166 insertions(+), 107 deletions(-) create mode 100644 src/lib/stores/ModelTraining.ts create mode 100644 src/lib/stores/applicationState.ts diff --git a/src/App.svelte b/src/App.svelte index 0d35998d5..2c4ccaba0 100644 --- a/src/App.svelte +++ b/src/App.svelte @@ -30,7 +30,6 @@ import Router from './router/Router.svelte'; import { Feature, getFeature } from './lib/FeatureToggles'; import { welcomeLog } from './lib/utils/Logger'; - import { DeviceRequestStates, state } from './lib/stores/Stores'; import MediaQuery from './components/layout/MediaQuery.svelte'; import BottomBarMenuView from './components/layout/BottomBarMenuView.svelte'; import CookieBanner from './components/features/cookie-bannner/CookieBanner.svelte'; @@ -39,6 +38,7 @@ import OverlayView from './components/layout/OverlayView.svelte'; import SideBarMenuView from './components/layout/SideBarMenuView.svelte'; import PageContentView from './components/layout/PageContentView.svelte'; + import { DeviceRequestStates, state } from './lib/stores/applicationState'; welcomeLog(); if (CookieManager.isReconnectFlagSet()) { diff --git a/src/components/features/OutdatedMicrobitWarning.svelte b/src/components/features/OutdatedMicrobitWarning.svelte index f4a20270d..2107eb0b5 100644 --- a/src/components/features/OutdatedMicrobitWarning.svelte +++ b/src/components/features/OutdatedMicrobitWarning.svelte @@ -14,8 +14,8 @@ import StaticConfiguration from '../../StaticConfiguration'; import Microbits from '../../lib/microbit-interfacing/Microbits'; import { HexOrigin } from '../../lib/microbit-interfacing/HexOrigin'; - import { DeviceRequestStates } from '../../lib/stores/Stores'; import StandardButton from '../ui/buttons/StandardButton.svelte'; + import { DeviceRequestStates } from '../../lib/stores/applicationState'; let hasBeenClosed = false; export let targetRole: 'INPUT' | 'OUTPUT'; let showMakeCodeUpdateMessage = diff --git a/src/components/features/ReconnectPrompt.svelte b/src/components/features/ReconnectPrompt.svelte index 8d3681d78..bbb3777d3 100644 --- a/src/components/features/ReconnectPrompt.svelte +++ b/src/components/features/ReconnectPrompt.svelte @@ -10,8 +10,8 @@ import { btPatternInput, btPatternOutput } from '../../lib/stores/connectionStore'; import Microbits from '../../lib/microbit-interfacing/Microbits'; import { MBSpecs } from 'microbyte'; - import { DeviceRequestStates, state } from '../../lib/stores/Stores'; import StandardButton from '../ui/buttons/StandardButton.svelte'; + import { DeviceRequestStates, state } from '../../lib/stores/applicationState'; let reconnectText: string; let reconnectButtonText: string; diff --git a/src/components/features/bottom/BottomPanel.svelte b/src/components/features/bottom/BottomPanel.svelte index 6d71055a1..076747de4 100644 --- a/src/components/features/bottom/BottomPanel.svelte +++ b/src/components/features/bottom/BottomPanel.svelte @@ -10,13 +10,13 @@ import LiveGraphInformationSection from './LiveGraphInformationSection.svelte'; import { tr } from '../../../i18n'; import ConnectDialogContainer from '../../features/connection-prompt/ConnectDialogContainer.svelte'; - import { state } from '../../../lib/stores/Stores'; import { startConnectionProcess } from '../../../lib/stores/connectDialogStore'; import Microbits from '../../../lib/microbit-interfacing/Microbits'; import View3DLive from '../3d-inspector/View3DLive.svelte'; import BaseDialog from '../../ui/dialogs/BaseDialog.svelte'; import MicrobitLiveGraph from '../graphs/MicrobitLiveGraph.svelte'; import StandardButton from '../../ui/buttons/StandardButton.svelte'; + import { state } from '../../../lib/stores/applicationState'; let componentWidth: number; let connectDialogReference: ConnectDialogContainer; diff --git a/src/components/features/bottom/ConnectedLiveGraphButtons.svelte b/src/components/features/bottom/ConnectedLiveGraphButtons.svelte index fdab22439..7eca9ca60 100644 --- a/src/components/features/bottom/ConnectedLiveGraphButtons.svelte +++ b/src/components/features/bottom/ConnectedLiveGraphButtons.svelte @@ -6,7 +6,8 @@ diff --git a/src/components/features/connection-prompt/ConnectDialogContainer.svelte b/src/components/features/connection-prompt/ConnectDialogContainer.svelte index ed03dc105..b285528a0 100644 --- a/src/components/features/connection-prompt/ConnectDialogContainer.svelte +++ b/src/components/features/connection-prompt/ConnectDialogContainer.svelte @@ -21,7 +21,7 @@ import { btPatternInput, btPatternOutput } from '../../../lib/stores/connectionStore'; import BrokenFirmwareDetected from './usb/BrokenFirmwareDetected.svelte'; import { MBSpecs } from 'microbyte'; - import { DeviceRequestStates } from '../../../lib/stores/Stores'; + import { DeviceRequestStates } from '../../../lib/stores/applicationState'; let flashProgress = 0; diff --git a/src/components/features/connection-prompt/bluetooth/BluetoothConnectDialog.svelte b/src/components/features/connection-prompt/bluetooth/BluetoothConnectDialog.svelte index 1df306232..1b9535045 100644 --- a/src/components/features/connection-prompt/bluetooth/BluetoothConnectDialog.svelte +++ b/src/components/features/connection-prompt/bluetooth/BluetoothConnectDialog.svelte @@ -19,8 +19,8 @@ import StaticConfiguration from '../../../../StaticConfiguration'; import Logger from '../../../../lib/utils/Logger'; import { MBSpecs } from 'microbyte'; - import { DeviceRequestStates, state } from '../../../../lib/stores/Stores'; import StandardButton from '../../../ui/buttons/StandardButton.svelte'; + import { DeviceRequestStates, state } from '../../../../lib/stores/applicationState'; // callbacks export let deviceState: DeviceRequestStates; diff --git a/src/components/features/datacollection/Gesture.svelte b/src/components/features/datacollection/Gesture.svelte index 550c11a01..cb2ec64ed 100644 --- a/src/components/features/datacollection/Gesture.svelte +++ b/src/components/features/datacollection/Gesture.svelte @@ -20,16 +20,16 @@ import GestureCard from '../../ui/Card.svelte'; import StaticConfiguration from '../../../StaticConfiguration'; import Gesture from '../../../lib/domain/stores/gesture/Gesture'; - import { state, stores } from '../../../lib/stores/Stores'; + import { stores } from '../../../lib/stores/Stores'; import type { RecordingData } from '../../../lib/domain/RecordingData'; import { startRecording } from '../../../lib/utils/Recording'; import GestureDot from '../../ui/GestureDot.svelte'; import StandardButton from '../../ui/buttons/StandardButton.svelte'; + import { state } from '../../../lib/stores/applicationState'; export let onNoMicrobitSelect: () => void; export let gesture: Gesture; const gestures = stores.getGestures(); - $: liveData = $stores.liveData; const defaultNewName = $t('content.data.classPlaceholderNewClass'); const recordingDuration = StaticConfiguration.recordingDuration; diff --git a/src/components/features/graphs/LiveGraph.svelte b/src/components/features/graphs/LiveGraph.svelte index b18b379ab..58f8aa554 100644 --- a/src/components/features/graphs/LiveGraph.svelte +++ b/src/components/features/graphs/LiveGraph.svelte @@ -9,11 +9,12 @@ import { type Unsubscriber } from 'svelte/store'; import { SmoothieChart, TimeSeries } from 'smoothie'; import DimensionLabels from './DimensionLabels.svelte'; - import { state, stores } from '../../../lib/stores/Stores'; import type { LiveData } from '../../../lib/domain/stores/LiveData'; import type { LiveDataVector } from '../../../lib/domain/stores/LiveDataVector'; import StaticConfiguration from '../../../StaticConfiguration'; import SmoothedLiveData from '../../../lib/livedata/SmoothedLiveData'; + import { state } from '../../../lib/stores/applicationState'; + import { stores } from '../../../lib/stores/Stores'; /** * TimesSeries, but with the data array added. diff --git a/src/components/features/graphs/knngraph/KNNModelGraphDrawer.ts b/src/components/features/graphs/knngraph/KNNModelGraphDrawer.ts index c1d7fd5c0..c41eaa778 100644 --- a/src/components/features/graphs/knngraph/KNNModelGraphDrawer.ts +++ b/src/components/features/graphs/knngraph/KNNModelGraphDrawer.ts @@ -9,8 +9,9 @@ import { get } from 'svelte/store'; import * as d3 from 'd3'; import { knnNeighbours } from '../../../../lib/stores/KnnModelGraph'; import type { Point3D, Point3DTransformed } from '../../../../lib/utils/graphUtils'; -import { state, stores } from '../../../../lib/stores/Stores'; +import { stores } from '../../../../lib/stores/Stores'; import StaticConfiguration from '../../../../StaticConfiguration'; +import { state } from '../../../../lib/stores/applicationState'; export type GraphDrawConfig = { xRot: number; diff --git a/src/components/features/model/ModelGestureStack.svelte b/src/components/features/model/ModelGestureStack.svelte index 9d092ef9e..624cb44e7 100644 --- a/src/components/features/model/ModelGestureStack.svelte +++ b/src/components/features/model/ModelGestureStack.svelte @@ -19,7 +19,7 @@ import type { SoundData } from '../../../lib/domain/stores/gesture/Gesture'; import type Gesture from '../../../lib/domain/stores/gesture/Gesture'; import Microbits from '../../../lib/microbit-interfacing/Microbits'; - import { state, stores } from '../../../lib/stores/Stores'; + import { stores } from '../../../lib/stores/Stores'; import StaticConfiguration from '../../../StaticConfiguration'; import Card from '../../ui/Card.svelte'; import GestureDot from '../../ui/GestureDot.svelte'; @@ -32,6 +32,7 @@ import PinSelector from './ModelPinSelector.svelte'; import { PinTurnOnState } from '../../../lib/PinTurnOnState'; import { MBSpecs } from 'microbyte'; + import { state } from '../../../lib/stores/applicationState'; const gestures = stores.getGestures(); type TriggerAction = 'turnOn' | 'turnOff' | 'none'; diff --git a/src/components/layout/OverlayView.svelte b/src/components/layout/OverlayView.svelte index cc1bfffbd..cdf92ea28 100644 --- a/src/components/layout/OverlayView.svelte +++ b/src/components/layout/OverlayView.svelte @@ -11,8 +11,8 @@ import ReconnectPrompt from '../features/ReconnectPrompt.svelte'; import OutdatedMicrobitWarning from '../features/OutdatedMicrobitWarning.svelte'; import { isInputPatternValid } from '../../lib/stores/connectionStore'; - import { state } from '../../lib/stores/Stores'; import FilterListFilterPreview from '../features/filters/FilterListFilterPreview.svelte'; + import { state } from '../../lib/stores/applicationState'; // Helps show error messages on top of page let latestMessage = ''; diff --git a/src/components/layout/SideBarMenuView.svelte b/src/components/layout/SideBarMenuView.svelte index dba2bc542..7808b8ac1 100644 --- a/src/components/layout/SideBarMenuView.svelte +++ b/src/components/layout/SideBarMenuView.svelte @@ -9,11 +9,11 @@ import { get } from 'svelte/store'; import type { MenuProperties } from '../sidemenu/Menus'; import { currentPath, navigate, Paths } from '../../router/Router'; - import { state } from '../../lib/stores/Stores'; import MediaQuery from './MediaQuery.svelte'; import { Feature, getFeature } from '../../lib/FeatureToggles'; import Menus from '../sidemenu/Menus'; import MenuButton from '../sidemenu/MenuButton.svelte'; + import { state } from '../../lib/stores/applicationState'; $: shouldBeExpanded = (menuProps: MenuProperties) => { let path = $currentPath; diff --git a/src/components/sidemenu/ModelMenu.svelte b/src/components/sidemenu/ModelMenu.svelte index 1d8ac8a64..80bdb64ad 100644 --- a/src/components/sidemenu/ModelMenu.svelte +++ b/src/components/sidemenu/ModelMenu.svelte @@ -7,7 +7,8 @@ diff --git a/src/pages/filter/D3Plot.svelte b/src/pages/filter/D3Plot.svelte index fca496a7a..800807b39 100644 --- a/src/pages/filter/D3Plot.svelte +++ b/src/pages/filter/D3Plot.svelte @@ -12,8 +12,9 @@ import FilterGraphLimits from '../../lib/utils/FilterLimits'; import { type GestureData } from '../../lib/domain/stores/gesture/Gesture'; import StaticConfiguration from '../../StaticConfiguration'; - import { state, stores } from '../../lib/stores/Stores'; import type { RecordingData } from '../../lib/domain/RecordingData'; + import { stores } from '../../lib/stores/Stores'; + import { state } from '../../lib/stores/applicationState'; export let filterType: FilterType; export let fullScreen: boolean = false; diff --git a/src/pages/home-page-content-tiles/IntroVideoTile.svelte b/src/pages/home-page-content-tiles/IntroVideoTile.svelte index b46a0aa48..b3e970627 100644 --- a/src/pages/home-page-content-tiles/IntroVideoTile.svelte +++ b/src/pages/home-page-content-tiles/IntroVideoTile.svelte @@ -6,7 +6,7 @@

diff --git a/src/pages/model/ModelPage.svelte b/src/pages/model/ModelPage.svelte index 31725c586..943315bdf 100644 --- a/src/pages/model/ModelPage.svelte +++ b/src/pages/model/ModelPage.svelte @@ -10,7 +10,7 @@ import ControlBar from '../../components/ui/control-bar/ControlBar.svelte'; import ExpandableControlBarMenu from '../../components/ui/control-bar/control-bar-items/ExpandableControlBarMenu.svelte'; import { Feature, hasFeature } from '../../lib/FeatureToggles'; - import { ModelView, state } from '../../lib/stores/Stores'; + import { ModelView, state } from '../../lib/stores/applicationState'; import ModelPageStackView from './stackview/ModelPageStackView.svelte'; import ModelPageTileView from './tileview/ModelPageTileView.svelte'; diff --git a/src/pages/model/stackview/ModelPageStackView.svelte b/src/pages/model/stackview/ModelPageStackView.svelte index dc7e0e718..03ef0bc3c 100644 --- a/src/pages/model/stackview/ModelPageStackView.svelte +++ b/src/pages/model/stackview/ModelPageStackView.svelte @@ -11,8 +11,9 @@ import TrainModelFirstTitle from '../../../components/features/model/TrainModelFirstTitle.svelte'; import ModelPageStackViewContent from './ModelPageStackViewContent.svelte'; import StaticConfiguration from '../../../StaticConfiguration'; - import { state, stores } from '../../../lib/stores/Stores'; + import { stores } from '../../../lib/stores/Stores'; import PleaseConnect from '../../../components/features/PleaseConnect.svelte'; + import { state } from '../../../lib/stores/applicationState'; const classifier = stores.getClassifier(); // In case of manual classification, variables for evaluation diff --git a/src/pages/model/stackview/ModelPageStackViewContent.svelte b/src/pages/model/stackview/ModelPageStackViewContent.svelte index 660b8467e..b73d918b0 100644 --- a/src/pages/model/stackview/ModelPageStackViewContent.svelte +++ b/src/pages/model/stackview/ModelPageStackViewContent.svelte @@ -7,8 +7,9 @@ import { fade } from 'svelte/transition'; import Information from '../../../components/ui/information/Information.svelte'; import { t } from './../../../i18n'; - import { state, stores } from '../../../lib/stores/Stores'; + import { stores } from '../../../lib/stores/Stores'; import OutputGesture from '../../../components/features/model/ModelGesture.svelte'; + import { state } from '../../../lib/stores/applicationState'; const gestures = stores.getGestures(); // Bool flags to know whether output microbit popup should be show diff --git a/src/pages/model/tileview/ModelPageTileView.svelte b/src/pages/model/tileview/ModelPageTileView.svelte index f3924bd46..e9c9400ea 100644 --- a/src/pages/model/tileview/ModelPageTileView.svelte +++ b/src/pages/model/tileview/ModelPageTileView.svelte @@ -11,7 +11,8 @@ import Microbits from '../../../lib/microbit-interfacing/Microbits'; import ModelPageTileViewTiles from './ModelPageTileViewTiles.svelte'; import StaticConfiguration from '../../../StaticConfiguration'; - import { state, stores } from '../../../lib/stores/Stores'; + import { state } from '../../../lib/stores/applicationState'; + import { stores } from '../../../lib/stores/Stores'; const classifier = stores.getClassifier(); // In case of manual classification, variables for evaluation diff --git a/src/pages/model/tileview/ModelPageTileViewTiles.svelte b/src/pages/model/tileview/ModelPageTileViewTiles.svelte index 116282c80..530eb6829 100644 --- a/src/pages/model/tileview/ModelPageTileViewTiles.svelte +++ b/src/pages/model/tileview/ModelPageTileViewTiles.svelte @@ -10,8 +10,9 @@ import Microbits from '../../../lib/microbit-interfacing/Microbits'; import MediaQuery from '../../../components/layout/MediaQuery.svelte'; import StaticConfiguration from '../../../StaticConfiguration'; - import { state, stores } from '../../../lib/stores/Stores'; + import { stores } from '../../../lib/stores/Stores'; import OutputGesture from '../../../components/features/model/ModelGesture.svelte'; + import { state } from '../../../lib/stores/applicationState'; // In case of manual classification, variables for evaluation let recordingTime = 0; diff --git a/src/pages/training/KnnModelTrainingPageView.svelte b/src/pages/training/KnnModelTrainingPageView.svelte index cfbeefaab..5cd85a5c6 100644 --- a/src/pages/training/KnnModelTrainingPageView.svelte +++ b/src/pages/training/KnnModelTrainingPageView.svelte @@ -22,7 +22,7 @@ const knnModelSettings = stores.getKNNModelSettings(); $: { - if (!$classifier.model.isTrained) { + if (!$classifier.model.isTrained && $classifier.model.hasModel) { trainModel(ModelRegistry.KNN); } } @@ -35,7 +35,6 @@ const changeK = (amount: number) => { const newVal = Math.max($knnModelSettings.k + amount, 1); knnModelSettings.setK(newVal); - trainModel(ModelRegistry.KNN); }; $: { if ($knnModelSettings.k > maxK) { diff --git a/src/pages/training/PredictionLegend.svelte b/src/pages/training/PredictionLegend.svelte index 4b9dfdc4a..d587bd3a9 100644 --- a/src/pages/training/PredictionLegend.svelte +++ b/src/pages/training/PredictionLegend.svelte @@ -5,7 +5,8 @@ --> diff --git a/src/pages/training/controlbar/TrainingPageTabs.svelte b/src/pages/training/controlbar/TrainingPageTabs.svelte index 3ffdaa0b8..4b3813b1e 100644 --- a/src/pages/training/controlbar/TrainingPageTabs.svelte +++ b/src/pages/training/controlbar/TrainingPageTabs.svelte @@ -14,7 +14,7 @@ import { navigate, Paths } from '../../../router/Router'; import StandardButton from '../../../components/ui/buttons/StandardButton.svelte'; - const selectedModel = stores.getSelectedModel(); + const selectedModel = stores.getModelTraining().getSelectedModel(); const showTabBar = hasFeature(Feature.KNN_MODEL); if (!showTabBar) { diff --git a/src/pages/validation/ValidationGestureSelectGestureCard.svelte b/src/pages/validation/ValidationGestureSelectGestureCard.svelte index 29e76ee59..c958466a2 100644 --- a/src/pages/validation/ValidationGestureSelectGestureCard.svelte +++ b/src/pages/validation/ValidationGestureSelectGestureCard.svelte @@ -14,10 +14,11 @@ MicrobitInteractions, } from '../../lib/stores/uiStore'; import { t } from '../../i18n'; - import { state, stores } from '../../lib/stores/Stores'; + import { stores } from '../../lib/stores/Stores'; import { get } from 'svelte/store'; import Logger from '../../lib/utils/Logger'; import StandardButton from '../../components/ui/buttons/StandardButton.svelte'; + import { state } from '../../lib/stores/applicationState'; export let gesture: Gesture; export let onNoMicrobitSelect: () => void; From e952a0d34b5d413d03524c994502239574532e98 Mon Sep 17 00:00:00 2001 From: "A. Malthe Henriksen" Date: Tue, 20 May 2025 21:06:20 +0200 Subject: [PATCH 02/49] Fix test --- src/lib/stores/ModelTraining.ts | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/lib/stores/ModelTraining.ts b/src/lib/stores/ModelTraining.ts index 2423b8d90..ab7b7decb 100644 --- a/src/lib/stores/ModelTraining.ts +++ b/src/lib/stores/ModelTraining.ts @@ -1,3 +1,8 @@ +/** + * (c) 2023-2025, Center for Computational Thinking and Design at Aarhus University and contributors + * + * SPDX-License-Identifier: MIT + */ import { writable, type Invalidator, From bfa7a546344865a5abdb0041d41cf4620df6df56 Mon Sep 17 00:00:00 2001 From: "A. Malthe Henriksen" Date: Tue, 20 May 2025 21:44:22 +0200 Subject: [PATCH 03/49] Revert the ModelTraining store --- src/lib/stores/ModelTraining.ts | 37 ------------------- src/lib/stores/Stores.ts | 12 +++--- src/pages/training/TrainingPage.ts | 4 +- .../training/TrainingPageModelView.svelte | 2 +- .../controlbar/TrainingPageTabs.svelte | 2 +- 5 files changed, 9 insertions(+), 48 deletions(-) delete mode 100644 src/lib/stores/ModelTraining.ts diff --git a/src/lib/stores/ModelTraining.ts b/src/lib/stores/ModelTraining.ts deleted file mode 100644 index ab7b7decb..000000000 --- a/src/lib/stores/ModelTraining.ts +++ /dev/null @@ -1,37 +0,0 @@ -/** - * (c) 2023-2025, Center for Computational Thinking and Design at Aarhus University and contributors - * - * SPDX-License-Identifier: MIT - */ -import { - writable, - type Invalidator, - type Readable, - type Subscriber, - type Unsubscriber, - type Writable, -} from 'svelte/store'; -import type SelectedModel from '../domain/SelectedModel'; - -export interface ModelTrainingStore {} - -class ModelTraining implements Readable { - private store: Writable; - - public constructor(private selectedModel: SelectedModel) { - this.store = writable({}); - } - - public getSelectedModel(): SelectedModel { - return this.selectedModel; - } - - public subscribe( - run: Subscriber, - invalidate?: Invalidator | undefined, - ): Unsubscriber { - return this.store.subscribe(run, invalidate); - } -} - -export default ModelTraining; diff --git a/src/lib/stores/Stores.ts b/src/lib/stores/Stores.ts index aeee62d72..bac373984 100644 --- a/src/lib/stores/Stores.ts +++ b/src/lib/stores/Stores.ts @@ -34,7 +34,6 @@ import { Recorder } from '../domain/stores/Recorder'; import ValidationResults from '../domain/stores/ValidationResults'; import Snackbar from './Snackbar'; import { state, type ApplicationState } from './applicationState'; -import ModelTraining from './ModelTraining'; type StoresType = { liveData: LiveData | undefined; @@ -58,7 +57,6 @@ class Stores implements Readable { private validationSets: ValidationSets; private validationResults: ValidationResults; private recorder: Recorder; - private modelTraining: ModelTraining; public constructor(private applicationState: Readable) { this.neuralNetworkSettings = new NeuralNetworkSettings(); @@ -89,7 +87,6 @@ class Stores implements Readable { this.gestures, this.highlightedAxis, ); - this.modelTraining = new ModelTraining(this.selectedModel); } public subscribe( @@ -147,6 +144,10 @@ class Stores implements Readable { return this.highlightedAxis; } + public getSelectedModel(): SelectedModel { + return this.selectedModel; + } + public getAvailableAxes(): AvailableAxes { return this.availableAxes; } @@ -174,10 +175,7 @@ class Stores implements Readable { public getRecorder(): Recorder { return this.recorder; } - - public getModelTraining(): ModelTraining { - return this.modelTraining; - } } + export const stores = new Stores(state); diff --git a/src/pages/training/TrainingPage.ts b/src/pages/training/TrainingPage.ts index 62908206f..48e46df27 100644 --- a/src/pages/training/TrainingPage.ts +++ b/src/pages/training/TrainingPage.ts @@ -63,12 +63,12 @@ const trackModelEvent = () => { appInsights.trackEvent({ name: 'ModelTrained', properties: { - modelType: get(stores.getModelTraining().getSelectedModel()).id, + modelType: get(stores.getSelectedModel()).id, }, }); } }; export const selectModel = async (model: ModelInfo) => { - stores.getModelTraining().getSelectedModel().set(model); + stores.getSelectedModel().set(model); }; diff --git a/src/pages/training/TrainingPageModelView.svelte b/src/pages/training/TrainingPageModelView.svelte index 3e10f625b..828bb2332 100644 --- a/src/pages/training/TrainingPageModelView.svelte +++ b/src/pages/training/TrainingPageModelView.svelte @@ -14,7 +14,7 @@ import FiltersList from '../../components/features/filters/FiltersList.svelte'; import { state } from '../../lib/stores/applicationState'; - const selectedModel = stores.getModelTraining().getSelectedModel(); + const selectedModel = stores.getSelectedModel(); const showFilterList = hasFeature(Feature.KNN_MODEL); diff --git a/src/pages/training/controlbar/TrainingPageTabs.svelte b/src/pages/training/controlbar/TrainingPageTabs.svelte index 4b3813b1e..3ffdaa0b8 100644 --- a/src/pages/training/controlbar/TrainingPageTabs.svelte +++ b/src/pages/training/controlbar/TrainingPageTabs.svelte @@ -14,7 +14,7 @@ import { navigate, Paths } from '../../../router/Router'; import StandardButton from '../../../components/ui/buttons/StandardButton.svelte'; - const selectedModel = stores.getModelTraining().getSelectedModel(); + const selectedModel = stores.getSelectedModel(); const showTabBar = hasFeature(Feature.KNN_MODEL); if (!showTabBar) { From 5bd3b17177b24dac16b6932a1975f905f817be08 Mon Sep 17 00:00:00 2001 From: "A. Malthe Henriksen" Date: Thu, 22 May 2025 22:33:11 +0200 Subject: [PATCH 04/49] Only retrain knn after knn has been trained at least once --- .../knngraph/AxesFilterVectorView.svelte | 2 +- .../knngraph/KNNModelGraphController.ts | 5 +- .../graphs/knngraph/KNNModelGraphDrawer.ts | 2 +- src/lib/domain/ModelTrainer.ts | 2 + src/lib/domain/SelectedModel.ts | 7 +- src/lib/domain/stores/HighlightedAxes.ts | 10 +- src/lib/domain/stores/KNNModelSettings.ts | 8 +- src/lib/domain/stores/Model.ts | 26 ++++ src/lib/mlmodels/KNNMLModel.ts | 2 +- src/lib/mlmodels/KNNModelTrainer.ts | 9 +- src/lib/mlmodels/KNNNonNormalizedMLModel.ts | 2 +- .../mlmodels/KNNNonNormalizedModelTrainer.ts | 8 +- src/lib/mlmodels/LayersModelTrainer.ts | 7 + .../stores/{KnnModelGraph.ts => KNNStores.ts} | 2 + src/lib/stores/Stores.ts | 4 +- .../training/KnnModelTrainingPageView.svelte | 125 ++++++++++-------- .../NeuralNetworkTrainingPageView.svelte | 5 +- src/pages/training/TrainingPage.ts | 33 +---- 18 files changed, 152 insertions(+), 107 deletions(-) rename src/lib/stores/{KnnModelGraph.ts => KNNStores.ts} (91%) diff --git a/src/components/features/graphs/knngraph/AxesFilterVectorView.svelte b/src/components/features/graphs/knngraph/AxesFilterVectorView.svelte index a1a78f9e9..975914728 100644 --- a/src/components/features/graphs/knngraph/AxesFilterVectorView.svelte +++ b/src/components/features/graphs/knngraph/AxesFilterVectorView.svelte @@ -9,11 +9,11 @@ import arrowCreate from 'arrows-svg'; import { onMount } from 'svelte'; import { vectorArrows } from './AxesFilterVector'; - import { knnCurrentPoint } from '../../../../lib/stores/KnnModelGraph'; import StaticConfiguration from '../../../../StaticConfiguration'; import type { Axis } from '../../../../lib/domain/Axis'; import { stores } from '../../../../lib/stores/Stores'; import StandardButton from '../../../ui/buttons/StandardButton.svelte'; + import { knnCurrentPoint } from '../../../../lib/stores/KNNStores'; const classifier = stores.getClassifier(); diff --git a/src/components/features/graphs/knngraph/KNNModelGraphController.ts b/src/components/features/graphs/knngraph/KNNModelGraphController.ts index d22373161..9c2b2fdfc 100644 --- a/src/components/features/graphs/knngraph/KNNModelGraphController.ts +++ b/src/components/features/graphs/knngraph/KNNModelGraphController.ts @@ -5,10 +5,7 @@ */ import { type Writable, derived, get, writable } from 'svelte/store'; import KNNModelGraphDrawer, { type GraphDrawConfig } from './KNNModelGraphDrawer'; -import { - knnCurrentPoint, - knnTrainingDataPoints, -} from '../../../../lib/stores/KnnModelGraph'; +import { knnCurrentPoint, knnTrainingDataPoints } from '../../../../lib/stores/KNNStores'; import type Filters from '../../../../lib/domain/Filters'; import { stores } from '../../../../lib/stores/Stores'; import type { Point3D } from '../../../../lib/utils/graphUtils'; diff --git a/src/components/features/graphs/knngraph/KNNModelGraphDrawer.ts b/src/components/features/graphs/knngraph/KNNModelGraphDrawer.ts index c41eaa778..dd4c712c5 100644 --- a/src/components/features/graphs/knngraph/KNNModelGraphDrawer.ts +++ b/src/components/features/graphs/knngraph/KNNModelGraphDrawer.ts @@ -7,7 +7,7 @@ import { gridPlanes3D, points3D, lines3D } from 'd3-3d'; import { knnHighlightedPoint } from './KnnPointToolTip'; import { get } from 'svelte/store'; import * as d3 from 'd3'; -import { knnNeighbours } from '../../../../lib/stores/KnnModelGraph'; +import { knnNeighbours } from '../../../../lib/stores/KNNStores'; import type { Point3D, Point3DTransformed } from '../../../../lib/utils/graphUtils'; import { stores } from '../../../../lib/stores/Stores'; import StaticConfiguration from '../../../../StaticConfiguration'; diff --git a/src/lib/domain/ModelTrainer.ts b/src/lib/domain/ModelTrainer.ts index c3fb427bf..895a67562 100644 --- a/src/lib/domain/ModelTrainer.ts +++ b/src/lib/domain/ModelTrainer.ts @@ -5,6 +5,7 @@ */ import type { MLModel } from './MLModel'; +import type { ModelInfo } from './ModelRegistry'; import type { TrainingDataRepository } from './TrainingDataRepository'; import type { Vector } from './Vector'; @@ -17,5 +18,6 @@ export type TrainingData = { }; export interface ModelTrainer { + getModelInfo(): ModelInfo; trainModel(trainingDataRepository: TrainingDataRepository): Promise; } diff --git a/src/lib/domain/SelectedModel.ts b/src/lib/domain/SelectedModel.ts index f4f332879..1a9181c22 100644 --- a/src/lib/domain/SelectedModel.ts +++ b/src/lib/domain/SelectedModel.ts @@ -12,10 +12,11 @@ import { } from 'svelte/store'; import ModelRegistry, { type ModelInfo } from './ModelRegistry'; import PersistantWritable from '../repository/PersistantWritable'; +import Logger from '../utils/Logger'; class SelectedModel implements Writable { private store: Writable; - public constructor() { + public constructor(private knnHasTrained: Writable) { this.store = new PersistantWritable( ModelRegistry.NeuralNetwork, 'selectedModel', @@ -23,6 +24,10 @@ class SelectedModel implements Writable { } public set(value: ModelInfo): void { + Logger.log('SelectedModel', `Setting selected model to ${value.title}`); + if (value.id === ModelRegistry.KNN.id) { + this.knnHasTrained.set(false); + } this.store.set(value); } diff --git a/src/lib/domain/stores/HighlightedAxes.ts b/src/lib/domain/stores/HighlightedAxes.ts index 50006d865..71cad63be 100644 --- a/src/lib/domain/stores/HighlightedAxes.ts +++ b/src/lib/domain/stores/HighlightedAxes.ts @@ -15,12 +15,13 @@ import { type Subscriber } from 'svelte/motion'; import SelectedModel from '../SelectedModel'; import ModelRegistry from '../ModelRegistry'; import type { Axis } from '../Axis'; -import { trainModel } from '../../../pages/training/TrainingPage'; import PersistantWritable from '../../repository/PersistantWritable'; import Logger from '../../utils/Logger'; import { t } from '../../../i18n'; import type Snackbar from '../../stores/Snackbar'; import type { ApplicationState } from '../../stores/applicationState'; +import { knnHasTrained } from '../../stores/KNNStores'; +import { trainKNNModel } from '../../../pages/training/TrainingPage'; class HighlightedAxes implements Writable { private value: PersistantWritable; // Use this.set instead of this.value.set! @@ -101,8 +102,11 @@ class HighlightedAxes implements Writable { get(this.selectedModel).id === ModelRegistry.KNN.id && get(this.applicationState).isInputConnected ) { - Logger.log('HighlightedAxes', 'Retraining KNN model due to axes changed'); - await trainModel(ModelRegistry.KNN); + if (get(knnHasTrained)) { + Logger.log('HighlightedAxes', 'Retraining KNN model due to axes changed'); + // Only train if the knn model has been trained before + await trainKNNModel(); + } } } } diff --git a/src/lib/domain/stores/KNNModelSettings.ts b/src/lib/domain/stores/KNNModelSettings.ts index 3c4b12b4c..2332a5de2 100644 --- a/src/lib/domain/stores/KNNModelSettings.ts +++ b/src/lib/domain/stores/KNNModelSettings.ts @@ -15,8 +15,9 @@ import { import StaticConfiguration from '../../../StaticConfiguration'; import type SelectedModel from '../SelectedModel'; import ModelRegistry from '../ModelRegistry'; -import { trainModel } from '../../../pages/training/TrainingPage'; +import { trainKNNModel } from '../../../pages/training/TrainingPage'; import type Classifier from './Classifier'; +import { knnHasTrained } from '../../stores/KNNStores'; interface KNNModelSettingsType { k: number; @@ -65,7 +66,10 @@ class KNNModelSettings implements Readable { private onUpdate() { if (get(this.selectedModel).id === ModelRegistry.KNN.id) { - trainModel(ModelRegistry.KNN); + if (get(knnHasTrained)) { + // Only train if the knn model has been trained before + trainKNNModel(); + } } } } diff --git a/src/lib/domain/stores/Model.ts b/src/lib/domain/stores/Model.ts index f1253a711..affa5f6bf 100644 --- a/src/lib/domain/stores/Model.ts +++ b/src/lib/domain/stores/Model.ts @@ -16,6 +16,13 @@ import { type TrainerConsumer } from '../../repository/LocalStorageClassifierRep import type { MLModel } from '../MLModel'; import type { ModelTrainer } from '../ModelTrainer'; import type { Vector } from '../Vector'; +import CookieManager from '../../CookieManager'; +import { appInsights } from '../../../appInsights'; +import { stores } from '../../stores/Stores'; +import type { ModelInfo } from '../ModelRegistry'; +import Logger from '../../utils/Logger'; +import ModelRegistry from '../ModelRegistry'; +import { knnHasTrained } from '../../stores/KNNStores'; export enum TrainingStatus { Untrained, @@ -51,16 +58,24 @@ class Model implements Readable { } public async train(modelTrainer: ModelTrainer): Promise { + Logger.log('Model', 'Training new model: ' + modelTrainer.getModelInfo().title); this.modelData.update(state => { state.trainingStatus = TrainingStatus.InProgress; return state; }); try { await this.trainerConsumer(modelTrainer); + + // TODO: Consider making a service for some of the business logic here + if (modelTrainer.getModelInfo().id === ModelRegistry.KNN.id) { + knnHasTrained.set(true); + } + this.modelData.update(state => { state.trainingStatus = TrainingStatus.Success; return state; }); + this.trackModelEvent(modelTrainer.getModelInfo()); } catch (err) { this.modelData.update(state => { state.trainingStatus = TrainingStatus.Failure; @@ -127,6 +142,17 @@ class Model implements Readable { }); return derivedStore.subscribe(run, invalidate); } + + private trackModelEvent(model: ModelInfo): void { + if (CookieManager.getComplianceChoices().analytics) { + appInsights.trackEvent({ + name: 'ModelTrained', + properties: { + modelType: model.id, + }, + }); + } + } } export default Model; diff --git a/src/lib/mlmodels/KNNMLModel.ts b/src/lib/mlmodels/KNNMLModel.ts index 8c050135a..50e83157e 100644 --- a/src/lib/mlmodels/KNNMLModel.ts +++ b/src/lib/mlmodels/KNNMLModel.ts @@ -8,7 +8,7 @@ import Logger from '../utils/Logger'; import type { LabelledPoint } from './KNNNonNormalizedMLModel'; import { distanceBetween } from '../utils/graphUtils'; import type { Vector } from '../domain/Vector'; -import { knnCurrentPoint, knnNeighbours } from '../stores/KnnModelGraph'; +import { knnCurrentPoint, knnNeighbours } from '../stores/KNNStores'; class KNNMLModel implements MLModel { constructor( diff --git a/src/lib/mlmodels/KNNModelTrainer.ts b/src/lib/mlmodels/KNNModelTrainer.ts index 4ac1c4bb9..6cc5db09e 100644 --- a/src/lib/mlmodels/KNNModelTrainer.ts +++ b/src/lib/mlmodels/KNNModelTrainer.ts @@ -7,13 +7,20 @@ import KNNMLModel from './KNNMLModel'; import type { ModelTrainer } from '../domain/ModelTrainer'; import type { TrainingDataRepository } from '../domain/TrainingDataRepository'; import type { LabelledPoint } from './KNNNonNormalizedMLModel'; -import { knnTrainingDataPoints } from '../stores/KnnModelGraph'; +import { knnTrainingDataPoints } from '../stores/KNNStores'; +import type { ModelInfo } from '../domain/ModelRegistry'; +import ModelRegistry from '../domain/ModelRegistry'; /** * Trains a K-Nearest Neighbour model */ class KNNModelTrainer implements ModelTrainer { constructor(private k: number) {} + + public getModelInfo(): ModelInfo { + return ModelRegistry.KNN; + } + public trainModel(trainingDataRepository: TrainingDataRepository): Promise { const trainingData = trainingDataRepository.getTrainingData(); const mean = trainingDataRepository.getTrainingDataMean(); diff --git a/src/lib/mlmodels/KNNNonNormalizedMLModel.ts b/src/lib/mlmodels/KNNNonNormalizedMLModel.ts index 2d0941dcc..95b1d412f 100644 --- a/src/lib/mlmodels/KNNNonNormalizedMLModel.ts +++ b/src/lib/mlmodels/KNNNonNormalizedMLModel.ts @@ -4,7 +4,7 @@ * SPDX-License-Identifier: MIT */ -import { knnCurrentPoint, knnNeighbours } from '../stores/KnnModelGraph'; +import { knnCurrentPoint, knnNeighbours } from '../stores/KNNStores'; import type { MLModel } from '../domain/MLModel'; import type { Vector } from '../domain/Vector'; import { distanceBetween } from '../utils/graphUtils'; diff --git a/src/lib/mlmodels/KNNNonNormalizedModelTrainer.ts b/src/lib/mlmodels/KNNNonNormalizedModelTrainer.ts index a9ae7de58..95fcb18dc 100644 --- a/src/lib/mlmodels/KNNNonNormalizedModelTrainer.ts +++ b/src/lib/mlmodels/KNNNonNormalizedModelTrainer.ts @@ -3,12 +3,14 @@ * * SPDX-License-Identifier: MIT */ -import { knnTrainingDataPoints } from '../stores/KnnModelGraph'; +import { knnTrainingDataPoints } from '../stores/KNNStores'; import type { ModelTrainer } from '../domain/ModelTrainer'; import type { TrainingDataRepository } from '../domain/TrainingDataRepository'; import Logger from '../utils/Logger'; import type { LabelledPoint } from './KNNNonNormalizedMLModel'; import KNNNonNormalizedMLModel from './KNNNonNormalizedMLModel'; +import type { ModelInfo } from '../domain/ModelRegistry'; +import ModelRegistry from '../domain/ModelRegistry'; /** * Trains a K-Nearest Neighbour model. Unlike the version provided by tensorflow, the points are not normalized @@ -16,6 +18,10 @@ import KNNNonNormalizedMLModel from './KNNNonNormalizedMLModel'; class KNNNonNormalizedModelTrainer implements ModelTrainer { constructor(private k: number) {} + public getModelInfo(): ModelInfo { + return ModelRegistry.KNN; + } + public trainModel( trainingDataRepository: TrainingDataRepository, ): Promise { diff --git a/src/lib/mlmodels/LayersModelTrainer.ts b/src/lib/mlmodels/LayersModelTrainer.ts index 7dc9d925d..47a8c867e 100644 --- a/src/lib/mlmodels/LayersModelTrainer.ts +++ b/src/lib/mlmodels/LayersModelTrainer.ts @@ -3,6 +3,8 @@ * * SPDX-License-Identifier: MIT */ +import type { ModelInfo } from '../domain/ModelRegistry'; +import ModelRegistry from '../domain/ModelRegistry'; import type { ModelTrainer } from '../domain/ModelTrainer'; import type { TrainingDataRepository } from '../domain/TrainingDataRepository'; import LayersMLModel from './LayersMLModel'; @@ -25,6 +27,11 @@ class LayersModelTrainer implements ModelTrainer { private settings: LayersModelTrainingSettings, private onFitIteration: (h: LossTrainingIteration) => void, ) {} + + public getModelInfo(): ModelInfo { + return ModelRegistry.NeuralNetwork; + } + public async trainModel( trainingDataRepository: TrainingDataRepository, ): Promise { diff --git a/src/lib/stores/KnnModelGraph.ts b/src/lib/stores/KNNStores.ts similarity index 91% rename from src/lib/stores/KnnModelGraph.ts rename to src/lib/stores/KNNStores.ts index cb041d100..ea1ed74e7 100644 --- a/src/lib/stores/KnnModelGraph.ts +++ b/src/lib/stores/KNNStores.ts @@ -14,3 +14,5 @@ export const knnCurrentPoint = writable(undefined); export const knnTrainingDataPoints = writable([]); export const knnNeighbours = writable([]); + +export const knnHasTrained = writable(false); diff --git a/src/lib/stores/Stores.ts b/src/lib/stores/Stores.ts index bac373984..bdba3b61a 100644 --- a/src/lib/stores/Stores.ts +++ b/src/lib/stores/Stores.ts @@ -34,6 +34,7 @@ import { Recorder } from '../domain/stores/Recorder'; import ValidationResults from '../domain/stores/ValidationResults'; import Snackbar from './Snackbar'; import { state, type ApplicationState } from './applicationState'; +import { knnHasTrained } from './KNNStores'; type StoresType = { liveData: LiveData | undefined; @@ -68,7 +69,7 @@ class Stores implements Readable { this.classifier = repositories.getClassifierRepository().getClassifier(); this.confidences = repositories.getClassifierRepository().getConfidences(); this.gestures = new Gestures(repositories.getGestureRepository()); - this.selectedModel = new SelectedModel(); + this.selectedModel = new SelectedModel(knnHasTrained); this.knnModelSettings = new KNNModelSettings(this.selectedModel, this.classifier); this.highlightedAxis = new HighlightedAxes( this.classifier, @@ -177,5 +178,4 @@ class Stores implements Readable { } } - export const stores = new Stores(state); diff --git a/src/pages/training/KnnModelTrainingPageView.svelte b/src/pages/training/KnnModelTrainingPageView.svelte index 5cd85a5c6..d1612e9be 100644 --- a/src/pages/training/KnnModelTrainingPageView.svelte +++ b/src/pages/training/KnnModelTrainingPageView.svelte @@ -5,14 +5,14 @@ --> -{#if $highlightedAxis.length === 1} -

-
-
-
-
changeK(-1)} - class="bg-secondary font-bold text-secondarytext cursor-pointer select-none hover:bg-opacity-60 border-primary border-r-1 content-center px-2 rounded-l-xl"> - - -
-
changeK(1)} - class="bg-secondary border-primary text-secondarytext cursor-pointer hover:bg-opacity-60 select-none content-center px-2 rounded-r-xl"> - + +
+ {#if !$knnHasTrained} +
+ trainKNNModel()} + >{$t('menu.trainer.trainModelButtonSimple')} +
+ {/if} + {#if $highlightedAxis.length === 1} +
+
+
+
+
changeK(-1)} + class="bg-secondary font-bold text-secondarytext cursor-pointer select-none hover:bg-opacity-60 border-primary border-r-1 content-center px-2 rounded-l-xl"> + - +
+
changeK(1)} + class="bg-secondary border-primary text-secondarytext cursor-pointer hover:bg-opacity-60 select-none content-center px-2 rounded-r-xl"> + + +
+

+ {$knnModelSettings.k} + {$t('content.trainer.knn.neighbours')} +

+
+ +
+
-

- {$knnModelSettings.k} - {$t('content.trainer.knn.neighbours')} -

-
- -
-
+ {#if $filters.length == 2 && $classifier.model.isTrained && $highlightedAxis.length === 1} + + {:else} +
+

+ {$t('menu.trainer.knn.onlyTwoFilters')} +

+
+ {/if}
- {#if $filters.length == 2 && $classifier.model.isTrained && $highlightedAxis.length === 1} - - {:else} -
-

- {$t('menu.trainer.knn.onlyTwoFilters')} -

+ {:else} +
+

{$t('content.trainer.knn.selectOneAxis')}

+
+ highlightedAxis.set([$availableAxes[0]])}> + X + + highlightedAxis.set([$availableAxes[1]])}> + Y + + highlightedAxis.set([$availableAxes[2]])}> + Z +
- {/if} -
-{:else} -
-

{$t('content.trainer.knn.selectOneAxis')}

-
- highlightedAxis.set([$availableAxes[0]])}> - X - - highlightedAxis.set([$availableAxes[1]])}> - Y - - highlightedAxis.set([$availableAxes[2]])}> - Z -
-
-{/if} + {/if} +
diff --git a/src/pages/training/NeuralNetworkTrainingPageView.svelte b/src/pages/training/NeuralNetworkTrainingPageView.svelte index 6d8618669..d18521192 100644 --- a/src/pages/training/NeuralNetworkTrainingPageView.svelte +++ b/src/pages/training/NeuralNetworkTrainingPageView.svelte @@ -5,9 +5,8 @@ --> + +
+
+
+
+ + {#if isOpen} +
+ +
+ {:else} +
+ +
+ {/if} +
diff --git a/src/pages/ValidationPage.svelte b/src/pages/ValidationPage.svelte index 36a91f19e..24fc42edb 100644 --- a/src/pages/ValidationPage.svelte +++ b/src/pages/ValidationPage.svelte @@ -8,13 +8,17 @@ import StandardDialog from '../components/ui/dialogs/StandardDialog.svelte'; import ValidationPageControlBar from './validation/ValidationPageControlBar.svelte'; import ValidationPageMainContent from './validation/ValidationPageMainContent.svelte'; - import { t } from '../i18n'; + import { t, tr } from '../i18n'; import { startConnectionProcess } from '../lib/stores/connectDialogStore'; import ValidationPageActionContent from './validation/ValidationPageActionContent.svelte'; import StandardButton from '../components/ui/buttons/StandardButton.svelte'; import ConnectDialogContainer from '../components/features/connection-prompt/ConnectDialogContainer.svelte'; + import Drawer from '../components/ui/drawer/Drawer.svelte'; + import { stores } from '../lib/stores/Stores'; + import ValidationpageActionContentMinimized from './validation/ValidationpageActionContentMinimized.svelte'; let isConnectionDialogOpen = false; + let isActionsOpen = false; @@ -25,12 +29,23 @@
+ style="height: calc(100vh - 48px - 160px - {isActionsOpen + ? '152px' + : '32px'}); transition: height 0.3s ease;"> (isConnectionDialogOpen = true)} />
-
- +
+ (isActionsOpen = false)} + onOpen={() => (isActionsOpen = true)} + heightMax="152px" + heightMin="32px"> + + +
diff --git a/src/pages/validation/ValidationpageActionContentMinimized.svelte b/src/pages/validation/ValidationpageActionContentMinimized.svelte new file mode 100644 index 000000000..f7121d434 --- /dev/null +++ b/src/pages/validation/ValidationpageActionContentMinimized.svelte @@ -0,0 +1,32 @@ + + + + +
+
+

Hello world

+ +
+ {#if !isNaN($accuracy)} +

+ {$tr('content.validation.accuracy')}: +

+

+ {($accuracy * 100).toFixed(1)} % +

+ {:else} + {$tr('content.validation.accuracy')}: - + {/if} +
+
+
From 59f5b8c262d853ed217801c23535ce663370622e Mon Sep 17 00:00:00 2001 From: "A. Malthe Henriksen" Date: Mon, 2 Jun 2025 20:55:16 +0200 Subject: [PATCH 10/49] Add drawer with buttons --- src/assets/messages/ui.en.json | 1 + .../ui/buttons/StandardButton.svelte | 11 +++- src/components/ui/drawer/Drawer.svelte | 6 +- src/lib/domain/stores/ValidationResults.ts | 61 +++++++++++++++++-- src/pages/ValidationPage.svelte | 19 +++++- src/pages/validation/ValidationPage.ts | 5 ++ .../ValidationPageActionContent.svelte | 33 ++-------- ...alidationpageActionContentMinimized.svelte | 26 +++++++- 8 files changed, 121 insertions(+), 41 deletions(-) diff --git a/src/assets/messages/ui.en.json b/src/assets/messages/ui.en.json index 7f15fe567..49e35b3fe 100644 --- a/src/assets/messages/ui.en.json +++ b/src/assets/messages/ui.en.json @@ -113,6 +113,7 @@ "content.validation.noGestures.description": "You haven't added any gestures yet. Go to the data step in the menu to the left and add some.", "content.validation.tutorial.title": "Validation sets", "content.validation.tutorial.description": "Create a separate dataset to evaluate the performance of your model. The model will not be trained on this data.", + "content.validation.tutorial.trainmodelfirst": "Train a model first", "footer.connectButtonNotConnected": "Connect your micro:bit", "footer.disconnectButton": "Disconnect", "footer.helpHeader": "Live graph", diff --git a/src/components/ui/buttons/StandardButton.svelte b/src/components/ui/buttons/StandardButton.svelte index 6a3e6936c..e6d2e95fb 100644 --- a/src/components/ui/buttons/StandardButton.svelte +++ b/src/components/ui/buttons/StandardButton.svelte @@ -13,6 +13,10 @@ padding: 1px 10px; font-size: 14px; } + .tiny { + padding: 0px 10px; + font-size: 14px; + } .outlined { color: var(--color); border-style: solid; @@ -55,6 +59,7 @@ export let onClick: (e: Event) => void = TypingUtils.emptyFunction; export let disabled = false; export let small = false; + export let tiny = false; export let outlined = false; export let fillOnHover = false; export let bold = true; @@ -87,8 +92,9 @@ class:shadow-md={shadows} class:bg-disabled={true} class:font-bold={bold} + class:tiny class:small - class:normal={!small} + class:normal={!small && !tiny} class:outlined class:cursor-default={disabled} on:click={onClick}> @@ -108,7 +114,8 @@ class:shadow-md={shadows} class:font-bold={bold} class:small - class:normal={!small} + class:tiny + class:normal={!small && !tiny} class:outlined class:filled={!outlined} class:fillOnHover diff --git a/src/components/ui/drawer/Drawer.svelte b/src/components/ui/drawer/Drawer.svelte index 7de6f8f93..b0ddba222 100644 --- a/src/components/ui/drawer/Drawer.svelte +++ b/src/components/ui/drawer/Drawer.svelte @@ -29,7 +29,7 @@ style="height: {isOpen ? heightMax : heightMin}; transition: height 0.3s ease; {style ?? ''}">
{#if isOpen} -
+
{:else} -
+
{/if} diff --git a/src/lib/domain/stores/ValidationResults.ts b/src/lib/domain/stores/ValidationResults.ts index a29472ae0..311a9ddd6 100644 --- a/src/lib/domain/stores/ValidationResults.ts +++ b/src/lib/domain/stores/ValidationResults.ts @@ -21,8 +21,10 @@ import { ClassifierInput } from '../ClassifierInput'; import { findLargestIndex } from '../../utils/Math'; import type Gestures from './gesture/Gestures'; import type Gesture from './gesture/Gesture'; -import type { GestureID } from './gesture/Gesture'; +import type { GestureData, GestureID } from './gesture/Gesture'; import type HighlightedAxes from './HighlightedAxes'; +import type { ValidationSetMatrix } from '../../../pages/validation/ValidationPage'; +import Matrix from '../Matrix'; export type ValidationResult = { prediction: number[]; @@ -31,7 +33,8 @@ export type ValidationResult = { }[][]; class ValidationResults implements Readable { private store: Writable; - private accuracy: Writable; + private accuracy: Readable; + private autoUpdate: Writable; public constructor( private validationSets: ValidationSets, @@ -40,7 +43,10 @@ class ValidationResults implements Readable { private highlightedAxes: HighlightedAxes, ) { this.store = writable([]); - this.accuracy = writable(0); + this.autoUpdate = writable(false); + this.accuracy = derived(this.getMatrix(), matrix => { + return matrix.accurateResults / this.validationSets.count(); + }); } public subscribe( @@ -95,8 +101,12 @@ class ValidationResults implements Readable { return this.accuracy; } - public setAccuracy(acc: number): void { - this.accuracy.set(acc); + public getMatrix(): Readable { + return derived([this as Readable, this.gestures], stores => { + const [valRes, gests] = stores; + const matrix = this.createValidationMatrixVisual(valRes, gests); + return matrix; + }); } public getEvaluatedGesture(recordingId: number): Gesture | undefined { @@ -108,6 +118,47 @@ class ValidationResults implements Readable { } return this.gestures.getGestures()[x.gestureIdx]; } + + public getAutoUpdate(): Writable { + return this.autoUpdate; + } + + private createValidationMatrixVisual = ( + validationResult: ValidationResult, + gestures: GestureData[], + ): ValidationSetMatrix => { + const matrixRaw = this.createValidationMatrix(validationResult, gestures); + + const accurateResults = gestures.reduce( + (pre, _, idx) => pre + matrixRaw.getValues()[idx][idx], + 0, + ); + return { + matrix: matrixRaw, + accurateResults: accurateResults, + }; + }; + + private createValidationMatrix = ( + validationResults: { + prediction: number[]; + gestureIdx: number; + }[][], + gestures: GestureData[], + ): Matrix => { + const matrix = gestures.map((_, row) => { + const results = validationResults[row]; + if (!results) { + return gestures.map(_ => 0); + } + return gestures.map((_, col) => { + return results.reduce((pre, cur) => { + return pre + (cur.gestureIdx === col ? 1 : 0); + }, 0); + }); + }); + return new Matrix(matrix); + }; } export default ValidationResults; diff --git a/src/pages/ValidationPage.svelte b/src/pages/ValidationPage.svelte index 24fc42edb..67f285e82 100644 --- a/src/pages/ValidationPage.svelte +++ b/src/pages/ValidationPage.svelte @@ -14,8 +14,21 @@ import StandardButton from '../components/ui/buttons/StandardButton.svelte'; import ConnectDialogContainer from '../components/features/connection-prompt/ConnectDialogContainer.svelte'; import Drawer from '../components/ui/drawer/Drawer.svelte'; - import { stores } from '../lib/stores/Stores'; import ValidationpageActionContentMinimized from './validation/ValidationpageActionContentMinimized.svelte'; + import { stores } from '../lib/stores/Stores'; + + const validationSets = stores.getValidationSets(); + const classifier = stores.getClassifier(); + const model = classifier.getModel(); + const validationResults = stores.getValidationResults(); + const autoUpdate = validationResults.getAutoUpdate(); + + $: { + // TODO: This should be encapsulated in the validation results store + if ($model.isTrained && $autoUpdate && $validationSets.length) { + validationResults.evaluateValidationSet(); + } + } let isConnectionDialogOpen = false; let isActionsOpen = false; @@ -31,7 +44,7 @@ class="overflow-x-auto flex-grow overflow-y-auto" style="height: calc(100vh - 48px - 160px - {isActionsOpen ? '152px' - : '32px'}); transition: height 0.3s ease;"> + : '36px'}); transition: height 0.3s ease;"> (isConnectionDialogOpen = true)} />
@@ -42,7 +55,7 @@ onClose={() => (isActionsOpen = false)} onOpen={() => (isActionsOpen = true)} heightMax="152px" - heightMin="32px"> + heightMin="36px"> diff --git a/src/pages/validation/ValidationPage.ts b/src/pages/validation/ValidationPage.ts index 97f69945c..eff73c15f 100644 --- a/src/pages/validation/ValidationPage.ts +++ b/src/pages/validation/ValidationPage.ts @@ -58,3 +58,8 @@ export const isValidationSetEmpty = derived( return validationSets.reduce((pre, cur) => pre + cur.recordings.length, 0) === 0; }, ); + +export const evaluateValidationSet = () => { + const validationResults = stores.getValidationResults(); + const accuracy = validationResults.getAccuracy(); +}; diff --git a/src/pages/validation/ValidationPageActionContent.svelte b/src/pages/validation/ValidationPageActionContent.svelte index 77faf2963..7862b3d56 100644 --- a/src/pages/validation/ValidationPageActionContent.svelte +++ b/src/pages/validation/ValidationPageActionContent.svelte @@ -6,47 +6,26 @@
@@ -60,7 +39,7 @@ + title={$tr('content.validation.tutorial.trainmodelfirst')}> {$tr('content.validation.testButton.test')} diff --git a/src/pages/validation/ValidationpageActionContentMinimized.svelte b/src/pages/validation/ValidationpageActionContentMinimized.svelte index f7121d434..a0d20d530 100644 --- a/src/pages/validation/ValidationpageActionContentMinimized.svelte +++ b/src/pages/validation/ValidationpageActionContentMinimized.svelte @@ -5,16 +5,40 @@ -->
-

Hello world

+
+

+ {$tr('content.validation.testButton.autoUpdate')}: +

+ + + + {$tr('content.validation.testButton.test')} + + +
{#if !isNaN($accuracy)} From 997850278d207cac4729f55165e4afba5da85efb Mon Sep 17 00:00:00 2001 From: "A. Malthe Henriksen" Date: Mon, 2 Jun 2025 21:50:06 +0200 Subject: [PATCH 11/49] Fix tooltip --- src/components/ui/Tooltip.svelte | 17 +++-------------- .../NeuralNetworkTrainingPageView.svelte | 19 +++++++++++++------ .../ValidationPageActionContent.svelte | 2 +- ...alidationpageActionContentMinimized.svelte | 2 +- 4 files changed, 18 insertions(+), 22 deletions(-) diff --git a/src/components/ui/Tooltip.svelte b/src/components/ui/Tooltip.svelte index cd7967da1..38fc81bc0 100644 --- a/src/components/ui/Tooltip.svelte +++ b/src/components/ui/Tooltip.svelte @@ -10,36 +10,25 @@ export let offset: { x: number; y: number } = { x: 0, y: 0 }; export let disabled: boolean = false; let isHovered = false; - let x: number; - let y: number; const mouseOver: MouseEventHandler = event => { isHovered = true; - x = event.pageX - 280; - y = event.pageY + 5; }; - const mouseMove: MouseEventHandler = event => { - x = event.pageX - 280; - y = event.pageY + 5; - }; const mouseLeave: MouseEventHandler = event => { isHovered = false; }; -
{}} - on:mouseover={mouseOver} - on:mouseleave={mouseLeave} - on:mousemove={mouseMove}> +
{}} on:mouseover={mouseOver} on:mouseleave={mouseLeave}>
+ {#if !disabled} {#if isHovered && !!title}
{title}
diff --git a/src/pages/training/NeuralNetworkTrainingPageView.svelte b/src/pages/training/NeuralNetworkTrainingPageView.svelte index d18521192..7a281bbee 100644 --- a/src/pages/training/NeuralNetworkTrainingPageView.svelte +++ b/src/pages/training/NeuralNetworkTrainingPageView.svelte @@ -11,6 +11,7 @@ import { Feature, hasFeature } from '../../lib/FeatureToggles'; import LossGraph from '../../components/features/graphs/LossGraph.svelte'; import StandardButton from '../../components/ui/buttons/StandardButton.svelte'; + import Tooltip from '../../components/ui/Tooltip.svelte'; const classifier = stores.getClassifier(); const model = classifier.getModel(); @@ -42,12 +43,18 @@

{$t('menu.trainer.TrainingFinished')}

{$t('menu.trainer.TrainingFinished.body')}

{/if} - - {$t(trainButtonSimpleLabel)} - +
+ + + {$t(trainButtonSimpleLabel)} + + +
{/if} {#if $loss.length > 0 && hasFeature(Feature.LOSS_GRAPH) && ($model.isTrained || $model.isTraining)} diff --git a/src/pages/validation/ValidationPageActionContent.svelte b/src/pages/validation/ValidationPageActionContent.svelte index 7862b3d56..5a7c9226b 100644 --- a/src/pages/validation/ValidationPageActionContent.svelte +++ b/src/pages/validation/ValidationPageActionContent.svelte @@ -38,7 +38,7 @@
{$tr('content.validation.testButton.test')} diff --git a/src/pages/validation/ValidationpageActionContentMinimized.svelte b/src/pages/validation/ValidationpageActionContentMinimized.svelte index a0d20d530..d509ce754 100644 --- a/src/pages/validation/ValidationpageActionContentMinimized.svelte +++ b/src/pages/validation/ValidationpageActionContentMinimized.svelte @@ -29,7 +29,7 @@ Date: Mon, 2 Jun 2025 22:39:43 +0200 Subject: [PATCH 12/49] Add missing translations --- src/assets/messages/ui.da.json | 5 +++-- src/assets/messages/ui.de.json | 5 +++-- src/pages/ValidationPage.svelte | 2 +- 3 files changed, 7 insertions(+), 5 deletions(-) diff --git a/src/assets/messages/ui.da.json b/src/assets/messages/ui.da.json index cf724cbc2..eb5771abe 100644 --- a/src/assets/messages/ui.da.json +++ b/src/assets/messages/ui.da.json @@ -111,8 +111,9 @@ "content.validation.infobox.classesContent": "Her kan du se de klasser, du har defineret på datasiden.", "content.validation.noGestures.title": "Ingen klasser...", "content.validation.noGestures.description": "Du har endnu ikke tilføjet nogen klasser. Gå til datatrinnet i menuen til venstre og tilføj nogle.", - "content.validation.tutorial.title": "Validierungssätze", - "content.validation.tutorial.description": "Erstelle einen separaten Datensatz, um die Leistung deines Modells zu bewerten. Das Modell wird nicht mit diesen Daten trainiert.", + "content.validation.tutorial.title": "Valideringssæt", + "content.validation.tutorial.description": "Opret et separat datasæt til at evaluere dit models ydeevne. Modellen vil ikke blive trænet på disse data.", + "content.validation.tutorial.trainmodelfirst": "Zuerst ein Modell trainieren", "footer.connectButtonNotConnected": "Tilslut din BBC micro:bit", "footer.disconnectButton": "Frakobl", "footer.helpHeader": "Live graf", diff --git a/src/assets/messages/ui.de.json b/src/assets/messages/ui.de.json index f5fb6bd88..6cd1d2ce3 100644 --- a/src/assets/messages/ui.de.json +++ b/src/assets/messages/ui.de.json @@ -111,8 +111,9 @@ "content.validation.infobox.classesContent": "Hier siehst du die Klassen, die du auf der Datenseite definiert hast.", "content.validation.noGestures.title": "Keine Gesten...", "content.validation.noGestures.description": "Du hast noch keine Gesten hinzugefügt. Gehe zum Schritt „Daten“ im Menü auf der linken Seite und füge welche hinzu.", - "content.validation.tutorial.title": "Valideringssæt", - "content.validation.tutorial.description": "Opret et separat datasæt til at evaluere dit models ydeevne. Modellen vil ikke blive trænet på disse data.", + "content.validation.tutorial.title": "Validierungssätze", + "content.validation.tutorial.description": "Erstelle einen separaten Datensatz, um die Leistung deines Modells zu bewerten. Das Modell wird nicht mit diesen Daten trainiert.", + "content.validation.tutorial.trainmodelfirst": "Zuerst ein Modell trainieren", "footer.connectButtonNotConnected": "Verbinde deinen micro:bit", "footer.disconnectButton": "Trennen", "footer.helpHeader": "Live-Diagramm", diff --git a/src/pages/ValidationPage.svelte b/src/pages/ValidationPage.svelte index 67f285e82..dbf17a1b5 100644 --- a/src/pages/ValidationPage.svelte +++ b/src/pages/ValidationPage.svelte @@ -31,7 +31,7 @@ } let isConnectionDialogOpen = false; - let isActionsOpen = false; + let isActionsOpen = true; From c6074ed85baaedb67c7294584605ee8b4b1fdc9b Mon Sep 17 00:00:00 2001 From: "A. Malthe Henriksen" Date: Wed, 4 Jun 2025 18:27:41 +0200 Subject: [PATCH 13/49] Default drawer to be closed --- src/pages/ValidationPage.svelte | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pages/ValidationPage.svelte b/src/pages/ValidationPage.svelte index dbf17a1b5..67f285e82 100644 --- a/src/pages/ValidationPage.svelte +++ b/src/pages/ValidationPage.svelte @@ -31,7 +31,7 @@ } let isConnectionDialogOpen = false; - let isActionsOpen = true; + let isActionsOpen = false; From 57f48b42160c26f12f4c3dc78d43a67f97d31d08 Mon Sep 17 00:00:00 2001 From: "A. Malthe Henriksen" Date: Wed, 4 Jun 2025 19:43:57 +0200 Subject: [PATCH 14/49] Mark models as untrained whenever another is selected --- src/lib/domain/SelectedModel.ts | 11 +++++++++-- src/lib/stores/Stores.ts | 2 +- 2 files changed, 10 insertions(+), 3 deletions(-) diff --git a/src/lib/domain/SelectedModel.ts b/src/lib/domain/SelectedModel.ts index 1a9181c22..60e289ad3 100644 --- a/src/lib/domain/SelectedModel.ts +++ b/src/lib/domain/SelectedModel.ts @@ -5,6 +5,7 @@ */ import { + get, type Invalidator, type Subscriber, type Unsubscriber, @@ -13,10 +14,15 @@ import { import ModelRegistry, { type ModelInfo } from './ModelRegistry'; import PersistantWritable from '../repository/PersistantWritable'; import Logger from '../utils/Logger'; +import type Classifier from './stores/Classifier'; class SelectedModel implements Writable { private store: Writable; - public constructor(private knnHasTrained: Writable) { + + public constructor( + private classifier: Classifier, + private knnHasTrained: Writable, + ) { this.store = new PersistantWritable( ModelRegistry.NeuralNetwork, 'selectedModel', @@ -28,11 +34,12 @@ class SelectedModel implements Writable { if (value.id === ModelRegistry.KNN.id) { this.knnHasTrained.set(false); } + this.classifier.getModel().markAsUntrained(); this.store.set(value); } public update(updater: (state: ModelInfo) => ModelInfo): void { - this.store.update(updater); + this.set(updater(get(this.store))); } public subscribe( diff --git a/src/lib/stores/Stores.ts b/src/lib/stores/Stores.ts index bdba3b61a..b05cfbaa1 100644 --- a/src/lib/stores/Stores.ts +++ b/src/lib/stores/Stores.ts @@ -69,7 +69,7 @@ class Stores implements Readable { this.classifier = repositories.getClassifierRepository().getClassifier(); this.confidences = repositories.getClassifierRepository().getConfidences(); this.gestures = new Gestures(repositories.getGestureRepository()); - this.selectedModel = new SelectedModel(knnHasTrained); + this.selectedModel = new SelectedModel(this.classifier, knnHasTrained); this.knnModelSettings = new KNNModelSettings(this.selectedModel, this.classifier); this.highlightedAxis = new HighlightedAxes( this.classifier, From b0fc5c8bca0fb331f21c4fe788dcd3fa17fc597d Mon Sep 17 00:00:00 2001 From: "A. Malthe Henriksen" Date: Wed, 4 Jun 2025 22:20:19 +0200 Subject: [PATCH 15/49] Rename application state type --- src/lib/domain/stores/HighlightedAxes.ts | 4 ++-- src/lib/stores/Stores.ts | 6 ++++-- src/lib/stores/applicationState.ts | 4 ++-- 3 files changed, 8 insertions(+), 6 deletions(-) diff --git a/src/lib/domain/stores/HighlightedAxes.ts b/src/lib/domain/stores/HighlightedAxes.ts index 71cad63be..a167503c4 100644 --- a/src/lib/domain/stores/HighlightedAxes.ts +++ b/src/lib/domain/stores/HighlightedAxes.ts @@ -19,7 +19,7 @@ import PersistantWritable from '../../repository/PersistantWritable'; import Logger from '../../utils/Logger'; import { t } from '../../../i18n'; import type Snackbar from '../../stores/Snackbar'; -import type { ApplicationState } from '../../stores/applicationState'; +import type { ApplicationStates } from '../../stores/applicationState'; import { knnHasTrained } from '../../stores/KNNStores'; import { trainKNNModel } from '../../../pages/training/TrainingPage'; @@ -29,7 +29,7 @@ class HighlightedAxes implements Writable { public constructor( private classifier: Classifier, private selectedModel: SelectedModel, - private applicationState: Readable, + private applicationState: Readable, private snackbar: Snackbar, ) { this.value = new PersistantWritable([], 'highlightedAxes'); diff --git a/src/lib/stores/Stores.ts b/src/lib/stores/Stores.ts index b05cfbaa1..e5ee87e6b 100644 --- a/src/lib/stores/Stores.ts +++ b/src/lib/stores/Stores.ts @@ -33,7 +33,7 @@ import ValidationSets from '../domain/stores/ValidationSets'; import { Recorder } from '../domain/stores/Recorder'; import ValidationResults from '../domain/stores/ValidationResults'; import Snackbar from './Snackbar'; -import { state, type ApplicationState } from './applicationState'; +import { state, type ApplicationStates } from './applicationState'; import { knnHasTrained } from './KNNStores'; type StoresType = { @@ -44,6 +44,7 @@ type StoresType = { * Stores is a container object, that allows for management of global stores. */ class Stores implements Readable { + private liveData: Writable | undefined>; private engine: Engine | undefined; private classifier: Classifier; @@ -58,8 +59,9 @@ class Stores implements Readable { private validationSets: ValidationSets; private validationResults: ValidationResults; private recorder: Recorder; + // private state: ApplicationState - public constructor(private applicationState: Readable) { + public constructor(private applicationState: Readable) { this.neuralNetworkSettings = new NeuralNetworkSettings(); this.snackbar = new Snackbar(); this.liveData = writable(undefined); diff --git a/src/lib/stores/applicationState.ts b/src/lib/stores/applicationState.ts index 39966138e..436c49a3e 100644 --- a/src/lib/stores/applicationState.ts +++ b/src/lib/stores/applicationState.ts @@ -15,7 +15,7 @@ export enum ModelView { TILE, STACK, } -export interface ApplicationState { +export interface ApplicationStates { isRequestingDevice: DeviceRequestStates; isFlashingDevice: boolean; isRecording: boolean; @@ -35,7 +35,7 @@ export interface ApplicationState { isOutputOutdated: boolean; } // TODO: Application state, used as a dumping ground for shared variables. Should be split up -export const state = writable({ +export const state = writable({ isRequestingDevice: DeviceRequestStates.NONE, isFlashingDevice: false, isRecording: false, From 5a68c9f57b7c961737502f5a5648ac23e4ef23a1 Mon Sep 17 00:00:00 2001 From: "A. Malthe Henriksen" Date: Wed, 4 Jun 2025 22:20:33 +0200 Subject: [PATCH 16/49] Prettier --- src/lib/stores/Stores.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/lib/stores/Stores.ts b/src/lib/stores/Stores.ts index e5ee87e6b..b112623bf 100644 --- a/src/lib/stores/Stores.ts +++ b/src/lib/stores/Stores.ts @@ -44,7 +44,6 @@ type StoresType = { * Stores is a container object, that allows for management of global stores. */ class Stores implements Readable { - private liveData: Writable | undefined>; private engine: Engine | undefined; private classifier: Classifier; From ad70e5874b1adaf9f472baba08398c47b9654008 Mon Sep 17 00:00:00 2001 From: "A. Malthe Henriksen" Date: Wed, 4 Jun 2025 22:22:22 +0200 Subject: [PATCH 17/49] Rename ApplicationState file --- src/App.svelte | 2 +- src/components/features/OutdatedMicrobitWarning.svelte | 2 +- src/components/features/ReconnectPrompt.svelte | 2 +- src/components/features/bottom/BottomPanel.svelte | 2 +- src/components/features/bottom/ConnectedLiveGraphButtons.svelte | 2 +- .../features/bottom/LiveGraphInformationSection.svelte | 2 +- .../features/connection-prompt/ConnectDialogContainer.svelte | 2 +- .../connection-prompt/bluetooth/BluetoothConnectDialog.svelte | 2 +- src/components/features/datacollection/Gesture.svelte | 2 +- src/components/features/graphs/LiveGraph.svelte | 2 +- src/components/features/graphs/knngraph/KNNModelGraphDrawer.ts | 2 +- src/components/features/model/ModelGestureStack.svelte | 2 +- src/components/layout/OverlayView.svelte | 2 +- src/components/layout/SideBarMenuView.svelte | 2 +- src/components/sidemenu/ModelMenu.svelte | 2 +- src/lib/domain/stores/HighlightedAxes.ts | 2 +- src/lib/microbit-interfacing/InputMicrobitHandler.ts | 2 +- src/lib/microbit-interfacing/OutputMicrobitHandler.ts | 2 +- src/lib/stores/{applicationState.ts => ApplicationState.ts} | 0 src/lib/stores/Stores.ts | 2 +- src/lib/stores/connectDialogStore.ts | 2 +- src/lib/stores/uiStore.ts | 2 +- src/lib/utils/Recording.ts | 2 +- src/pages/Homepage.svelte | 2 +- src/pages/data/DataPageNoData.svelte | 2 +- src/pages/filter/D3Plot.svelte | 2 +- src/pages/home-page-content-tiles/IntroVideoTile.svelte | 2 +- src/pages/model/ModelPage.svelte | 2 +- src/pages/model/stackview/ModelPageStackView.svelte | 2 +- src/pages/model/stackview/ModelPageStackViewContent.svelte | 2 +- src/pages/model/tileview/ModelPageTileView.svelte | 2 +- src/pages/model/tileview/ModelPageTileViewTiles.svelte | 2 +- src/pages/training/PredictionLegend.svelte | 2 +- src/pages/training/TrainingPageModelView.svelte | 2 +- src/pages/validation/ValidationGestureSelectGestureCard.svelte | 2 +- 35 files changed, 34 insertions(+), 34 deletions(-) rename src/lib/stores/{applicationState.ts => ApplicationState.ts} (100%) diff --git a/src/App.svelte b/src/App.svelte index 2c4ccaba0..65286359e 100644 --- a/src/App.svelte +++ b/src/App.svelte @@ -38,7 +38,7 @@ import OverlayView from './components/layout/OverlayView.svelte'; import SideBarMenuView from './components/layout/SideBarMenuView.svelte'; import PageContentView from './components/layout/PageContentView.svelte'; - import { DeviceRequestStates, state } from './lib/stores/applicationState'; + import { DeviceRequestStates, state } from './lib/stores/ApplicationState'; welcomeLog(); if (CookieManager.isReconnectFlagSet()) { diff --git a/src/components/features/OutdatedMicrobitWarning.svelte b/src/components/features/OutdatedMicrobitWarning.svelte index 2107eb0b5..8161d5749 100644 --- a/src/components/features/OutdatedMicrobitWarning.svelte +++ b/src/components/features/OutdatedMicrobitWarning.svelte @@ -15,7 +15,7 @@ import Microbits from '../../lib/microbit-interfacing/Microbits'; import { HexOrigin } from '../../lib/microbit-interfacing/HexOrigin'; import StandardButton from '../ui/buttons/StandardButton.svelte'; - import { DeviceRequestStates } from '../../lib/stores/applicationState'; + import { DeviceRequestStates } from '../../lib/stores/ApplicationState'; let hasBeenClosed = false; export let targetRole: 'INPUT' | 'OUTPUT'; let showMakeCodeUpdateMessage = diff --git a/src/components/features/ReconnectPrompt.svelte b/src/components/features/ReconnectPrompt.svelte index bbb3777d3..6856141e0 100644 --- a/src/components/features/ReconnectPrompt.svelte +++ b/src/components/features/ReconnectPrompt.svelte @@ -11,7 +11,7 @@ import Microbits from '../../lib/microbit-interfacing/Microbits'; import { MBSpecs } from 'microbyte'; import StandardButton from '../ui/buttons/StandardButton.svelte'; - import { DeviceRequestStates, state } from '../../lib/stores/applicationState'; + import { DeviceRequestStates, state } from '../../lib/stores/ApplicationState'; let reconnectText: string; let reconnectButtonText: string; diff --git a/src/components/features/bottom/BottomPanel.svelte b/src/components/features/bottom/BottomPanel.svelte index 076747de4..e9dae920c 100644 --- a/src/components/features/bottom/BottomPanel.svelte +++ b/src/components/features/bottom/BottomPanel.svelte @@ -16,7 +16,7 @@ import BaseDialog from '../../ui/dialogs/BaseDialog.svelte'; import MicrobitLiveGraph from '../graphs/MicrobitLiveGraph.svelte'; import StandardButton from '../../ui/buttons/StandardButton.svelte'; - import { state } from '../../../lib/stores/applicationState'; + import { state } from '../../../lib/stores/ApplicationState'; let componentWidth: number; let connectDialogReference: ConnectDialogContainer; diff --git a/src/components/features/bottom/ConnectedLiveGraphButtons.svelte b/src/components/features/bottom/ConnectedLiveGraphButtons.svelte index 7eca9ca60..7ca6504da 100644 --- a/src/components/features/bottom/ConnectedLiveGraphButtons.svelte +++ b/src/components/features/bottom/ConnectedLiveGraphButtons.svelte @@ -6,7 +6,7 @@ diff --git a/src/components/features/connection-prompt/ConnectDialogContainer.svelte b/src/components/features/connection-prompt/ConnectDialogContainer.svelte index b285528a0..9df0117f5 100644 --- a/src/components/features/connection-prompt/ConnectDialogContainer.svelte +++ b/src/components/features/connection-prompt/ConnectDialogContainer.svelte @@ -21,7 +21,7 @@ import { btPatternInput, btPatternOutput } from '../../../lib/stores/connectionStore'; import BrokenFirmwareDetected from './usb/BrokenFirmwareDetected.svelte'; import { MBSpecs } from 'microbyte'; - import { DeviceRequestStates } from '../../../lib/stores/applicationState'; + import { DeviceRequestStates } from '../../../lib/stores/ApplicationState'; let flashProgress = 0; diff --git a/src/components/features/connection-prompt/bluetooth/BluetoothConnectDialog.svelte b/src/components/features/connection-prompt/bluetooth/BluetoothConnectDialog.svelte index 1b9535045..e29f16504 100644 --- a/src/components/features/connection-prompt/bluetooth/BluetoothConnectDialog.svelte +++ b/src/components/features/connection-prompt/bluetooth/BluetoothConnectDialog.svelte @@ -20,7 +20,7 @@ import Logger from '../../../../lib/utils/Logger'; import { MBSpecs } from 'microbyte'; import StandardButton from '../../../ui/buttons/StandardButton.svelte'; - import { DeviceRequestStates, state } from '../../../../lib/stores/applicationState'; + import { DeviceRequestStates, state } from '../../../../lib/stores/ApplicationState'; // callbacks export let deviceState: DeviceRequestStates; diff --git a/src/components/features/datacollection/Gesture.svelte b/src/components/features/datacollection/Gesture.svelte index cb2ec64ed..3e0c26f76 100644 --- a/src/components/features/datacollection/Gesture.svelte +++ b/src/components/features/datacollection/Gesture.svelte @@ -25,7 +25,7 @@ import { startRecording } from '../../../lib/utils/Recording'; import GestureDot from '../../ui/GestureDot.svelte'; import StandardButton from '../../ui/buttons/StandardButton.svelte'; - import { state } from '../../../lib/stores/applicationState'; + import { state } from '../../../lib/stores/ApplicationState'; export let onNoMicrobitSelect: () => void; export let gesture: Gesture; diff --git a/src/components/features/graphs/LiveGraph.svelte b/src/components/features/graphs/LiveGraph.svelte index 58f8aa554..ee2291615 100644 --- a/src/components/features/graphs/LiveGraph.svelte +++ b/src/components/features/graphs/LiveGraph.svelte @@ -13,7 +13,7 @@ import type { LiveDataVector } from '../../../lib/domain/stores/LiveDataVector'; import StaticConfiguration from '../../../StaticConfiguration'; import SmoothedLiveData from '../../../lib/livedata/SmoothedLiveData'; - import { state } from '../../../lib/stores/applicationState'; + import { state } from '../../../lib/stores/ApplicationState'; import { stores } from '../../../lib/stores/Stores'; /** diff --git a/src/components/features/graphs/knngraph/KNNModelGraphDrawer.ts b/src/components/features/graphs/knngraph/KNNModelGraphDrawer.ts index dd4c712c5..ead1e58f1 100644 --- a/src/components/features/graphs/knngraph/KNNModelGraphDrawer.ts +++ b/src/components/features/graphs/knngraph/KNNModelGraphDrawer.ts @@ -11,7 +11,7 @@ import { knnNeighbours } from '../../../../lib/stores/KNNStores'; import type { Point3D, Point3DTransformed } from '../../../../lib/utils/graphUtils'; import { stores } from '../../../../lib/stores/Stores'; import StaticConfiguration from '../../../../StaticConfiguration'; -import { state } from '../../../../lib/stores/applicationState'; +import { state } from '../../../../lib/stores/ApplicationState'; export type GraphDrawConfig = { xRot: number; diff --git a/src/components/features/model/ModelGestureStack.svelte b/src/components/features/model/ModelGestureStack.svelte index 624cb44e7..8990bef9c 100644 --- a/src/components/features/model/ModelGestureStack.svelte +++ b/src/components/features/model/ModelGestureStack.svelte @@ -32,7 +32,7 @@ import PinSelector from './ModelPinSelector.svelte'; import { PinTurnOnState } from '../../../lib/PinTurnOnState'; import { MBSpecs } from 'microbyte'; - import { state } from '../../../lib/stores/applicationState'; + import { state } from '../../../lib/stores/ApplicationState'; const gestures = stores.getGestures(); type TriggerAction = 'turnOn' | 'turnOff' | 'none'; diff --git a/src/components/layout/OverlayView.svelte b/src/components/layout/OverlayView.svelte index cdf92ea28..667faa076 100644 --- a/src/components/layout/OverlayView.svelte +++ b/src/components/layout/OverlayView.svelte @@ -12,7 +12,7 @@ import OutdatedMicrobitWarning from '../features/OutdatedMicrobitWarning.svelte'; import { isInputPatternValid } from '../../lib/stores/connectionStore'; import FilterListFilterPreview from '../features/filters/FilterListFilterPreview.svelte'; - import { state } from '../../lib/stores/applicationState'; + import { state } from '../../lib/stores/ApplicationState'; // Helps show error messages on top of page let latestMessage = ''; diff --git a/src/components/layout/SideBarMenuView.svelte b/src/components/layout/SideBarMenuView.svelte index 7808b8ac1..4233bb01b 100644 --- a/src/components/layout/SideBarMenuView.svelte +++ b/src/components/layout/SideBarMenuView.svelte @@ -13,7 +13,7 @@ import { Feature, getFeature } from '../../lib/FeatureToggles'; import Menus from '../sidemenu/Menus'; import MenuButton from '../sidemenu/MenuButton.svelte'; - import { state } from '../../lib/stores/applicationState'; + import { state } from '../../lib/stores/ApplicationState'; $: shouldBeExpanded = (menuProps: MenuProperties) => { let path = $currentPath; diff --git a/src/components/sidemenu/ModelMenu.svelte b/src/components/sidemenu/ModelMenu.svelte index 80bdb64ad..a274293ea 100644 --- a/src/components/sidemenu/ModelMenu.svelte +++ b/src/components/sidemenu/ModelMenu.svelte @@ -7,7 +7,7 @@ diff --git a/src/pages/filter/D3Plot.svelte b/src/pages/filter/D3Plot.svelte index 800807b39..a52379db7 100644 --- a/src/pages/filter/D3Plot.svelte +++ b/src/pages/filter/D3Plot.svelte @@ -14,7 +14,7 @@ import StaticConfiguration from '../../StaticConfiguration'; import type { RecordingData } from '../../lib/domain/RecordingData'; import { stores } from '../../lib/stores/Stores'; - import { state } from '../../lib/stores/applicationState'; + import { state } from '../../lib/stores/ApplicationState'; export let filterType: FilterType; export let fullScreen: boolean = false; diff --git a/src/pages/home-page-content-tiles/IntroVideoTile.svelte b/src/pages/home-page-content-tiles/IntroVideoTile.svelte index b3e970627..4d6493f3e 100644 --- a/src/pages/home-page-content-tiles/IntroVideoTile.svelte +++ b/src/pages/home-page-content-tiles/IntroVideoTile.svelte @@ -6,7 +6,7 @@

diff --git a/src/pages/model/ModelPage.svelte b/src/pages/model/ModelPage.svelte index 943315bdf..7d63916da 100644 --- a/src/pages/model/ModelPage.svelte +++ b/src/pages/model/ModelPage.svelte @@ -10,7 +10,7 @@ import ControlBar from '../../components/ui/control-bar/ControlBar.svelte'; import ExpandableControlBarMenu from '../../components/ui/control-bar/control-bar-items/ExpandableControlBarMenu.svelte'; import { Feature, hasFeature } from '../../lib/FeatureToggles'; - import { ModelView, state } from '../../lib/stores/applicationState'; + import { ModelView, state } from '../../lib/stores/ApplicationState'; import ModelPageStackView from './stackview/ModelPageStackView.svelte'; import ModelPageTileView from './tileview/ModelPageTileView.svelte'; diff --git a/src/pages/model/stackview/ModelPageStackView.svelte b/src/pages/model/stackview/ModelPageStackView.svelte index 03ef0bc3c..1dcb4abb9 100644 --- a/src/pages/model/stackview/ModelPageStackView.svelte +++ b/src/pages/model/stackview/ModelPageStackView.svelte @@ -13,7 +13,7 @@ import StaticConfiguration from '../../../StaticConfiguration'; import { stores } from '../../../lib/stores/Stores'; import PleaseConnect from '../../../components/features/PleaseConnect.svelte'; - import { state } from '../../../lib/stores/applicationState'; + import { state } from '../../../lib/stores/ApplicationState'; const classifier = stores.getClassifier(); // In case of manual classification, variables for evaluation diff --git a/src/pages/model/stackview/ModelPageStackViewContent.svelte b/src/pages/model/stackview/ModelPageStackViewContent.svelte index b73d918b0..88b09e2e3 100644 --- a/src/pages/model/stackview/ModelPageStackViewContent.svelte +++ b/src/pages/model/stackview/ModelPageStackViewContent.svelte @@ -9,7 +9,7 @@ import { t } from './../../../i18n'; import { stores } from '../../../lib/stores/Stores'; import OutputGesture from '../../../components/features/model/ModelGesture.svelte'; - import { state } from '../../../lib/stores/applicationState'; + import { state } from '../../../lib/stores/ApplicationState'; const gestures = stores.getGestures(); // Bool flags to know whether output microbit popup should be show diff --git a/src/pages/model/tileview/ModelPageTileView.svelte b/src/pages/model/tileview/ModelPageTileView.svelte index e9c9400ea..bcc65e1f8 100644 --- a/src/pages/model/tileview/ModelPageTileView.svelte +++ b/src/pages/model/tileview/ModelPageTileView.svelte @@ -11,7 +11,7 @@ import Microbits from '../../../lib/microbit-interfacing/Microbits'; import ModelPageTileViewTiles from './ModelPageTileViewTiles.svelte'; import StaticConfiguration from '../../../StaticConfiguration'; - import { state } from '../../../lib/stores/applicationState'; + import { state } from '../../../lib/stores/ApplicationState'; import { stores } from '../../../lib/stores/Stores'; const classifier = stores.getClassifier(); diff --git a/src/pages/model/tileview/ModelPageTileViewTiles.svelte b/src/pages/model/tileview/ModelPageTileViewTiles.svelte index 530eb6829..ed1ae84f4 100644 --- a/src/pages/model/tileview/ModelPageTileViewTiles.svelte +++ b/src/pages/model/tileview/ModelPageTileViewTiles.svelte @@ -12,7 +12,7 @@ import StaticConfiguration from '../../../StaticConfiguration'; import { stores } from '../../../lib/stores/Stores'; import OutputGesture from '../../../components/features/model/ModelGesture.svelte'; - import { state } from '../../../lib/stores/applicationState'; + import { state } from '../../../lib/stores/ApplicationState'; // In case of manual classification, variables for evaluation let recordingTime = 0; diff --git a/src/pages/training/PredictionLegend.svelte b/src/pages/training/PredictionLegend.svelte index d587bd3a9..08d1a5ff7 100644 --- a/src/pages/training/PredictionLegend.svelte +++ b/src/pages/training/PredictionLegend.svelte @@ -5,7 +5,7 @@ --> @@ -59,7 +62,7 @@

{reconnectText}

- reconnect($state.reconnectState)}> + reconnect($devices.reconnectState)}> {reconnectButtonText}
diff --git a/src/components/features/bottom/BottomPanel.svelte b/src/components/features/bottom/BottomPanel.svelte index e9dae920c..15074568a 100644 --- a/src/components/features/bottom/BottomPanel.svelte +++ b/src/components/features/bottom/BottomPanel.svelte @@ -16,7 +16,9 @@ import BaseDialog from '../../ui/dialogs/BaseDialog.svelte'; import MicrobitLiveGraph from '../graphs/MicrobitLiveGraph.svelte'; import StandardButton from '../../ui/buttons/StandardButton.svelte'; - import { state } from '../../../lib/stores/ApplicationState'; + import { stores } from '../../../lib/stores/Stores'; + + const devices = stores.getDevices(); let componentWidth: number; let connectDialogReference: ConnectDialogContainer; @@ -39,10 +41,10 @@
+ class:bg-gray-300={$devices.isInputAssigned && !$devices.isInputReady}> - {#if !$state.isInputAssigned} + {#if !$devices.isInputAssigned}
@@ -55,7 +57,7 @@
- {#if $state.isInputInitializing} + {#if $devices.isInputInitializing}
diff --git a/src/components/features/bottom/ConnectedLiveGraphButtons.svelte b/src/components/features/bottom/ConnectedLiveGraphButtons.svelte index 7ca6504da..4ace0ac0e 100644 --- a/src/components/features/bottom/ConnectedLiveGraphButtons.svelte +++ b/src/components/features/bottom/ConnectedLiveGraphButtons.svelte @@ -6,10 +6,10 @@
@@ -23,8 +24,8 @@

Live

+ class:text-red-500={$devices.isInputReady} + class:text-gray-500={!$devices.isInputReady}> •

{#if hasFeature(Feature.LIVE_GRAPH_INPUT_VALUES)} diff --git a/src/components/features/connection-prompt/ConnectDialogContainer.svelte b/src/components/features/connection-prompt/ConnectDialogContainer.svelte index 9df0117f5..7f930ec0d 100644 --- a/src/components/features/connection-prompt/ConnectDialogContainer.svelte +++ b/src/components/features/connection-prompt/ConnectDialogContainer.svelte @@ -21,7 +21,7 @@ import { btPatternInput, btPatternOutput } from '../../../lib/stores/connectionStore'; import BrokenFirmwareDetected from './usb/BrokenFirmwareDetected.svelte'; import { MBSpecs } from 'microbyte'; - import { DeviceRequestStates } from '../../../lib/stores/ApplicationState'; + import { DeviceRequestStates } from '../../../lib/domain/Devices'; let flashProgress = 0; diff --git a/src/components/features/connection-prompt/bluetooth/BluetoothConnectDialog.svelte b/src/components/features/connection-prompt/bluetooth/BluetoothConnectDialog.svelte index e29f16504..cf10fe99f 100644 --- a/src/components/features/connection-prompt/bluetooth/BluetoothConnectDialog.svelte +++ b/src/components/features/connection-prompt/bluetooth/BluetoothConnectDialog.svelte @@ -20,9 +20,11 @@ import Logger from '../../../../lib/utils/Logger'; import { MBSpecs } from 'microbyte'; import StandardButton from '../../../ui/buttons/StandardButton.svelte'; - import { DeviceRequestStates, state } from '../../../../lib/stores/ApplicationState'; + import { DeviceRequestStates } from '../../../../lib/domain/Devices'; + import { stores } from '../../../../lib/stores/Stores'; + + const devices = stores.getDevices(); - // callbacks export let deviceState: DeviceRequestStates; export let onBluetoothConnected: () => void; @@ -101,7 +103,7 @@ onMount(() => { // Resets the bluetooth connection prompt for cancelled device requests - $state.requestDeviceWasCancelled = false; + $devices.requestDeviceWasCancelled = false; }); const handleSearchWithoutName = () => { @@ -114,7 +116,7 @@ {$t('popup.connectMB.bluetooth.heading')} - {#if $state.requestDeviceWasCancelled && !isConnecting} + {#if $devices.requestDeviceWasCancelled && !isConnecting}

{$t('popup.connectMB.bluetooth.cancelledConnection')}

{$t('popup.connectMB.bluetooth.cancelledConnection.noNameDescription')} diff --git a/src/components/features/datacollection/Gesture.svelte b/src/components/features/datacollection/Gesture.svelte index 3e0c26f76..c554b6703 100644 --- a/src/components/features/datacollection/Gesture.svelte +++ b/src/components/features/datacollection/Gesture.svelte @@ -25,10 +25,10 @@ import { startRecording } from '../../../lib/utils/Recording'; import GestureDot from '../../ui/GestureDot.svelte'; import StandardButton from '../../ui/buttons/StandardButton.svelte'; - import { state } from '../../../lib/stores/ApplicationState'; export let onNoMicrobitSelect: () => void; export let gesture: Gesture; + const devices = stores.getDevices(); const gestures = stores.getGestures(); const defaultNewName = $t('content.data.classPlaceholderNewClass'); @@ -86,7 +86,7 @@ // If gesture is already selected, the selection is removed. // If bluetooth is not connected, open connection prompt by calling callback function selectClicked(): void { - if (!$state.isInputConnected) { + if (!$devices.isInputConnected) { chosenGesture.update(gesture => { gesture = null; return gesture; diff --git a/src/components/features/graphs/LiveGraph.svelte b/src/components/features/graphs/LiveGraph.svelte index ee2291615..c1928e386 100644 --- a/src/components/features/graphs/LiveGraph.svelte +++ b/src/components/features/graphs/LiveGraph.svelte @@ -13,7 +13,6 @@ import type { LiveDataVector } from '../../../lib/domain/stores/LiveDataVector'; import StaticConfiguration from '../../../StaticConfiguration'; import SmoothedLiveData from '../../../lib/livedata/SmoothedLiveData'; - import { state } from '../../../lib/stores/ApplicationState'; import { stores } from '../../../lib/stores/Stores'; /** @@ -33,6 +32,7 @@ let axisColors = StaticConfiguration.graphColors; const highlightedAxes = stores.getHighlightedAxes(); + const devices = stores.getDevices(); // Smoothes real-time data by using the 3 most recent data points let smoothedLiveData = new SmoothedLiveData(liveData, 3); @@ -99,7 +99,7 @@ const model = classifier.getModel(); $: { if (chart !== undefined) { - if ($state.isInputReady) { + if ($devices.isInputReady) { if (!$model.isTraining) { chart.start(); } else { @@ -115,7 +115,7 @@ // The jagged edges problem is caused by repeating the recordingStarted function. // We will simply block the recording from starting, while it's recording let blockRecordingStart = false; - $: recordingStarted($state.isRecording); + $: recordingStarted($devices.isRecording); // Function to clearly diplay the area in which users are recording function recordingStarted(isRecording: boolean): void { @@ -136,15 +136,15 @@ }, StaticConfiguration.recordingDuration); } - // When state changes, update the state of the canvas + // When devices changes, update the devices of the canvas $: { - const isConnected = $state.isInputReady; + const isConnected = $devices.isInputReady; updateCanvas(isConnected); } let unsubscribeFromData: Unsubscriber | undefined; - // If state is connected. Start updating the graph whenever there is new data + // If devices is connected. Start updating the graph whenever there is new data // From the Micro:Bit function updateCanvas(isConnected: boolean) { if (isConnected || !unsubscribeFromData) { @@ -181,7 +181,7 @@ {#key cnt}

{/if} - {#if $state.offerReconnect && isInputPatternValid()} + {#if $devices.offerReconnect && isInputPatternValid()} {/if} - {#if $state.isInputOutdated || $state.isOutputOutdated} - + {#if $devices.isInputOutdated || $devices.isOutputOutdated} + {/if}
diff --git a/src/components/layout/SideBarMenuView.svelte b/src/components/layout/SideBarMenuView.svelte index 4233bb01b..2ef203409 100644 --- a/src/components/layout/SideBarMenuView.svelte +++ b/src/components/layout/SideBarMenuView.svelte @@ -13,7 +13,7 @@ import { Feature, getFeature } from '../../lib/FeatureToggles'; import Menus from '../sidemenu/Menus'; import MenuButton from '../sidemenu/MenuButton.svelte'; - import { state } from '../../lib/stores/ApplicationState'; + import { isLoading } from '../../lib/stores/ApplicationState'; $: shouldBeExpanded = (menuProps: MenuProperties) => { let path = $currentPath; @@ -27,7 +27,7 @@ }; const onLoad = () => { - $state.isLoading = false; + $isLoading = false; }; diff --git a/src/components/sidemenu/ModelMenu.svelte b/src/components/sidemenu/ModelMenu.svelte index a274293ea..a3f7c9adb 100644 --- a/src/components/sidemenu/ModelMenu.svelte +++ b/src/components/sidemenu/ModelMenu.svelte @@ -7,14 +7,14 @@
@@ -53,8 +53,8 @@ class="grid break-words mr-auto ml-auto w-3/4 h-70px border-2 rounded-lg border-solid text-center align-center content-center">

+ class:text-2xl={$devices.isInputReady} + class:text-md={!$devices.isInputReady}> {predictionLabel}

diff --git a/src/lib/domain/Devices.ts b/src/lib/domain/Devices.ts new file mode 100644 index 000000000..21dfb442f --- /dev/null +++ b/src/lib/domain/Devices.ts @@ -0,0 +1,77 @@ +import { + get, + writable, + type Invalidator, + type Subscriber, + type Unsubscriber, + type Updater, + type Writable, +} from 'svelte/store'; + +export enum DeviceRequestStates { + NONE, + INPUT, + OUTPUT, +} + +interface DevicesType { + isRequestingDevice: DeviceRequestStates; + isFlashingDevice: boolean; + + /** + * @deprecated should be moved to the 'Recorder' store + */ + isRecording: boolean; + isInputConnected: boolean; + isOutputConnected: boolean; + offerReconnect: boolean; + requestDeviceWasCancelled: boolean; + reconnectState: DeviceRequestStates; + isInputReady: boolean; + isInputAssigned: boolean; + isOutputAssigned: boolean; + isOutputReady: boolean; + isInputInitializing: boolean; + isInputOutdated: boolean; + isOutputOutdated: boolean; +} + +class Devices implements Writable { + private store: Writable; + + public constructor() { + this.store = writable({ + isRequestingDevice: DeviceRequestStates.NONE, + isFlashingDevice: false, + isRecording: false, + isInputConnected: false, + isOutputConnected: false, + offerReconnect: false, + requestDeviceWasCancelled: false, + reconnectState: DeviceRequestStates.NONE, + isInputReady: false, + isInputAssigned: false, + isOutputAssigned: false, + isOutputReady: false, + isInputInitializing: false, + isInputOutdated: false, + isOutputOutdated: false, + }); + } + + public set(value: DevicesType): void { + this.store.set(value); + } + + public update(updater: Updater): void { + this.set(updater(get(this.store))); + } + + public subscribe( + run: Subscriber, + invalidate?: Invalidator | undefined, + ): Unsubscriber { + return this.store.subscribe(run, invalidate); + } +} +export default Devices; diff --git a/src/lib/domain/stores/HighlightedAxes.ts b/src/lib/domain/stores/HighlightedAxes.ts index 07f13fc10..b5051c043 100644 --- a/src/lib/domain/stores/HighlightedAxes.ts +++ b/src/lib/domain/stores/HighlightedAxes.ts @@ -19,9 +19,9 @@ import PersistantWritable from '../../repository/PersistantWritable'; import Logger from '../../utils/Logger'; import { t } from '../../../i18n'; import type Snackbar from '../../stores/Snackbar'; -import type { ApplicationStates } from '../../stores/ApplicationState'; import { knnHasTrained } from '../../stores/KNNStores'; import { trainKNNModel } from '../../../pages/training/TrainingPage'; +import type Devices from '../Devices'; class HighlightedAxes implements Writable { private value: PersistantWritable; // Use this.set instead of this.value.set! @@ -29,7 +29,7 @@ class HighlightedAxes implements Writable { public constructor( private classifier: Classifier, private selectedModel: SelectedModel, - private applicationState: Readable, + private devices: Devices, private snackbar: Snackbar, ) { this.value = new PersistantWritable([], 'highlightedAxes'); @@ -100,7 +100,7 @@ class HighlightedAxes implements Writable { if ( get(this.selectedModel).id === ModelRegistry.KNN.id && - get(this.applicationState).isInputConnected + get(this.devices).isInputConnected ) { if (get(knnHasTrained)) { Logger.log('HighlightedAxes', 'Retraining KNN model due to axes changed'); diff --git a/src/lib/microbit-interfacing/CombinedMicrobitHandler.ts b/src/lib/microbit-interfacing/CombinedMicrobitHandler.ts index 8f6b3dd98..f9185e7ef 100644 --- a/src/lib/microbit-interfacing/CombinedMicrobitHandler.ts +++ b/src/lib/microbit-interfacing/CombinedMicrobitHandler.ts @@ -7,10 +7,14 @@ import { MBSpecs } from 'microbyte'; import Microbits from './Microbits'; import InputMicrobitHandler from './InputMicrobitHandler'; import OutputMicrobitHandler from './OutputMicrobitHandler'; +import type Devices from '../domain/Devices'; class CombinedMicrobitHandler extends InputMicrobitHandler { - public constructor(private outputHandler: OutputMicrobitHandler) { - super(); + public constructor( + private outputHandler: OutputMicrobitHandler, + devices: Devices, + ) { + super(devices); } public onConnected(versionNumber?: MBSpecs.MBVersion | undefined): void { diff --git a/src/lib/microbit-interfacing/InputMicrobitHandler.ts b/src/lib/microbit-interfacing/InputMicrobitHandler.ts index 2e393449a..a0841a596 100644 --- a/src/lib/microbit-interfacing/InputMicrobitHandler.ts +++ b/src/lib/microbit-interfacing/InputMicrobitHandler.ts @@ -16,12 +16,15 @@ import StaticConfiguration from '../../StaticConfiguration'; import Microbits from './Microbits'; import { HexOrigin } from './HexOrigin'; import { stores } from '../stores/Stores'; -import { DeviceRequestStates, ModelView, state } from '../stores/ApplicationState'; +import Devices, { DeviceRequestStates } from '../domain/Devices'; +import { ModelView, modelView } from '../stores/ApplicationState'; class InputMicrobitHandler implements MicrobitHandler { private reconnectTimeout = setTimeout(TypingUtils.emptyFunction, 0); private lastConnectedVersion: MBSpecs.MBVersion | undefined; + public constructor(private devices: Devices) {} + public onConnected(versionNumber?: MBSpecs.MBVersion | undefined): void { Logger.log('InputMicrobitHandler', 'onConnected', versionNumber); @@ -31,7 +34,7 @@ class InputMicrobitHandler implements MicrobitHandler { ); stores.setLiveData(new MicrobitAccelerometerLiveData(buffer)); - state.update(s => { + this.devices.update(s => { s.isInputConnected = true; s.isRequestingDevice = DeviceRequestStates.NONE; s.offerReconnect = false; @@ -64,7 +67,7 @@ class InputMicrobitHandler implements MicrobitHandler { public onInitializing(): void { Logger.log('InputMicrobitHandler', 'onInitializing'); - state.update(s => { + this.devices.update(s => { s.isInputInitializing = true; return s; }); @@ -99,10 +102,7 @@ class InputMicrobitHandler implements MicrobitHandler { //Logger.log("InputMicrobitHandler", "onMessageReceived", data); if (data === 'id_mkcd') { Microbits.setInputOrigin(HexOrigin.MAKECODE); - state.update(s => { - s.modelView = ModelView.TILE; - return s; - }); + modelView.set(ModelView.TILE); } if (data === 'id_prop') { Microbits.setInputOrigin(HexOrigin.PROPRIETARY); @@ -120,7 +120,7 @@ class InputMicrobitHandler implements MicrobitHandler { public onDisconnected(): void { Logger.log('InputMicrobitHandler', 'onDisconnected'); - state.update(s => { + this.devices.update(s => { s.isInputConnected = false; s.offerReconnect = false; s.isInputReady = false; @@ -142,7 +142,7 @@ class InputMicrobitHandler implements MicrobitHandler { public onConnectError(error: Error): void { Logger.log('InputMicrobitHandler', 'onConnectError', error); - state.update(s => { + this.devices.update(s => { s.isInputConnected = false; s.isInputAssigned = false; s.isInputReady = false; @@ -153,7 +153,7 @@ class InputMicrobitHandler implements MicrobitHandler { public onReconnectError(error: Error): void { Logger.log('InputMicrobitHandler', 'onReconnectError', error); this.onConnectError(error); - state.update(s => { + this.devices.update(s => { s.offerReconnect = true; s.reconnectState = DeviceRequestStates.INPUT; return s; @@ -162,7 +162,7 @@ class InputMicrobitHandler implements MicrobitHandler { public onClosed(): void { Logger.log('InputMicrobitHandler', 'onClosed'); - state.update(s => { + this.devices.update(s => { s.isInputConnected = false; s.isInputAssigned = false; s.isInputReady = false; diff --git a/src/lib/microbit-interfacing/Microbits.ts b/src/lib/microbit-interfacing/Microbits.ts index 492cab1a9..be3e8c90e 100644 --- a/src/lib/microbit-interfacing/Microbits.ts +++ b/src/lib/microbit-interfacing/Microbits.ts @@ -18,6 +18,7 @@ import Logger from '../utils/Logger'; import OutputMicrobitHandler from './OutputMicrobitHandler'; import CombinedMicrobitHandler from './CombinedMicrobitHandler'; import { HexOrigin } from './HexOrigin'; +import { stores } from '../stores/Stores'; type UARTMessageType = 'g' | 's'; // Gesture or sound @@ -42,8 +43,11 @@ class Microbits { private static outputOrigin = HexOrigin.UNKNOWN; private static inputOrigin = HexOrigin.UNKNOWN; - private static outputHandler = new OutputMicrobitHandler(); - private static inputHandler = new CombinedMicrobitHandler(this.outputHandler); + private static outputHandler = new OutputMicrobitHandler(stores.getDevices()); + private static inputHandler = new CombinedMicrobitHandler( + this.outputHandler, + stores.getDevices(), + ); private static linkedMicrobit: Microbit = new Microbit(); diff --git a/src/lib/microbit-interfacing/OutputMicrobitHandler.ts b/src/lib/microbit-interfacing/OutputMicrobitHandler.ts index 615acfbda..433fb6984 100644 --- a/src/lib/microbit-interfacing/OutputMicrobitHandler.ts +++ b/src/lib/microbit-interfacing/OutputMicrobitHandler.ts @@ -10,12 +10,16 @@ import TypingUtils from '../TypingUtils'; import Logger from '../utils/Logger'; import Microbits from './Microbits'; import { HexOrigin } from './HexOrigin'; -import { DeviceRequestStates, ModelView, state } from '../stores/ApplicationState'; +import type Devices from '../domain/Devices'; +import { ModelView, modelView } from '../stores/ApplicationState'; +import { DeviceRequestStates } from '../domain/Devices'; class OutputMicrobitHandler implements MicrobitHandler { private reconnectTimeout = setTimeout(TypingUtils.emptyFunction, 0); private lastConnectedVersion: MBSpecs.MBVersion | undefined; + public constructor(private devices: Devices) {} + public onConnected(versionNumber?: MBSpecs.MBVersion | undefined): void { Logger.log('OutputMicrobitHandler', 'onConnected', versionNumber); @@ -26,9 +30,11 @@ class OutputMicrobitHandler implements MicrobitHandler { }); Microbits.sendToOutputPin(pinResetArguments); - state.update(s => { + this.devices.update(s => { if (Microbits.isInputOutputTheSame()) { - s.modelView = Microbits.isOutputMakecode() ? ModelView.TILE : s.modelView; + if (Microbits.isOutputMakecode()) { + modelView.set(ModelView.TILE); + } } s.isOutputConnected = true; s.isOutputAssigned = true; @@ -56,7 +62,7 @@ class OutputMicrobitHandler implements MicrobitHandler { public onDisconnected(): void { Logger.log('OutputMicrobitHandler', 'onDisconnected'); - state.update(s => { + this.devices.update(s => { s.isOutputConnected = false; s.isOutputReady = false; s.isOutputOutdated = false; @@ -73,17 +79,11 @@ class OutputMicrobitHandler implements MicrobitHandler { public onMessageReceived(data: string): void { if (data === 'id_mkcd') { Microbits.setOutputOrigin(HexOrigin.MAKECODE); - state.update(s => { - s.modelView = ModelView.TILE; - return s; - }); + modelView.set(ModelView.TILE); } if (data === 'id_prop') { Microbits.setOutputOrigin(HexOrigin.PROPRIETARY); - state.update(s => { - s.modelView = ModelView.STACK; - return s; - }); + modelView.set(ModelView.STACK); } if (data.includes('vi_')) { const version = parseInt(data.substring(3)); @@ -107,7 +107,7 @@ class OutputMicrobitHandler implements MicrobitHandler { public onConnectError(error: Error): void { Logger.log('OutputMicrobitHandler', 'onConnectError', error); - state.update(s => { + this.devices.update(s => { s.isOutputConnected = false; s.isOutputAssigned = false; s.isOutputReady = false; @@ -122,7 +122,7 @@ class OutputMicrobitHandler implements MicrobitHandler { public onClosed() { Logger.log('OutputMicrobitHandler', 'onClosed'); - state.update(s => { + this.devices.update(s => { s.isOutputConnected = false; s.isOutputAssigned = false; s.isOutputReady = false; diff --git a/src/lib/stores/ApplicationState.ts b/src/lib/stores/ApplicationState.ts index 436c49a3e..ab0532d95 100644 --- a/src/lib/stores/ApplicationState.ts +++ b/src/lib/stores/ApplicationState.ts @@ -6,51 +6,11 @@ import { writable } from 'svelte/store'; -export enum DeviceRequestStates { - NONE, - INPUT, - OUTPUT, -} export enum ModelView { TILE, STACK, } -export interface ApplicationStates { - isRequestingDevice: DeviceRequestStates; - isFlashingDevice: boolean; - isRecording: boolean; - isInputConnected: boolean; - isOutputConnected: boolean; - offerReconnect: boolean; - requestDeviceWasCancelled: boolean; - reconnectState: DeviceRequestStates; - isInputReady: boolean; - isInputAssigned: boolean; - isOutputAssigned: boolean; - isOutputReady: boolean; - isInputInitializing: boolean; - isLoading: boolean; - modelView: ModelView; - isInputOutdated: boolean; - isOutputOutdated: boolean; -} -// TODO: Application state, used as a dumping ground for shared variables. Should be split up -export const state = writable({ - isRequestingDevice: DeviceRequestStates.NONE, - isFlashingDevice: false, - isRecording: false, - isInputConnected: false, - isOutputConnected: false, - offerReconnect: false, - requestDeviceWasCancelled: false, - reconnectState: DeviceRequestStates.NONE, - isInputReady: false, - isInputAssigned: false, - isOutputAssigned: false, - isOutputReady: false, - isInputInitializing: false, - isLoading: true, - modelView: ModelView.STACK, - isInputOutdated: false, - isOutputOutdated: false, -}); + +export const modelView = writable(ModelView.STACK); + +export const isLoading = writable(true); diff --git a/src/lib/stores/Stores.ts b/src/lib/stores/Stores.ts index 371a48724..8abc2aad2 100644 --- a/src/lib/stores/Stores.ts +++ b/src/lib/stores/Stores.ts @@ -33,8 +33,8 @@ import ValidationSets from '../domain/stores/ValidationSets'; import { Recorder } from '../domain/stores/Recorder'; import ValidationResults from '../domain/stores/ValidationResults'; import Snackbar from './Snackbar'; -import { state, type ApplicationStates } from './ApplicationState'; import { knnHasTrained } from './KNNStores'; +import Devices from '../domain/Devices'; type StoresType = { liveData: LiveData | undefined; @@ -58,9 +58,10 @@ class Stores implements Readable { private validationSets: ValidationSets; private validationResults: ValidationResults; private recorder: Recorder; - // private state: ApplicationState + private devices: Devices; - public constructor(private applicationState: Readable) { + public constructor() { + this.devices = new Devices(); this.neuralNetworkSettings = new NeuralNetworkSettings(); this.snackbar = new Snackbar(); this.liveData = writable(undefined); @@ -75,7 +76,7 @@ class Stores implements Readable { this.highlightedAxis = new HighlightedAxes( this.classifier, this.selectedModel, - this.applicationState, + this.devices, this.snackbar, ); this.availableAxes = new AvailableAxes(this.liveData, this.gestures); @@ -177,6 +178,10 @@ class Stores implements Readable { public getRecorder(): Recorder { return this.recorder; } + + public getDevices(): Devices { + return this.devices; + } } -export const stores = new Stores(state); +export const stores = new Stores(); diff --git a/src/lib/stores/connectDialogStore.ts b/src/lib/stores/connectDialogStore.ts index a54d9fc03..fc2fde4b8 100644 --- a/src/lib/stores/connectDialogStore.ts +++ b/src/lib/stores/connectDialogStore.ts @@ -5,7 +5,8 @@ */ import { get, writable } from 'svelte/store'; -import { DeviceRequestStates, state } from './ApplicationState'; +import { DeviceRequestStates } from '../domain/Devices'; +import { stores } from './Stores'; export enum ConnectDialogStates { NONE, // No connection in progress -> Dialog box closed @@ -32,10 +33,10 @@ export const connectionDialogState = writable<{ export const startConnectionProcess = (): void => { // Updating the state will cause a popup to appear, from where the connection process will take place connectionDialogState.update(s => { - s.connectionState = get(state).isInputConnected + s.connectionState = get(stores.getDevices()).isInputConnected ? ConnectDialogStates.START_OUTPUT : ConnectDialogStates.START; - s.deviceState = get(state).isInputConnected + s.deviceState = get(stores.getDevices()).isInputConnected ? DeviceRequestStates.OUTPUT : DeviceRequestStates.INPUT; return s; diff --git a/src/lib/stores/uiStore.ts b/src/lib/stores/uiStore.ts index 7ce5a5ae2..95f895f71 100644 --- a/src/lib/stores/uiStore.ts +++ b/src/lib/stores/uiStore.ts @@ -14,7 +14,6 @@ import CookieManager from '../CookieManager'; import { isInputPatternValid } from './connectionStore'; import Gesture from '../domain/stores/gesture/Gesture'; import { stores } from './Stores'; -import { state } from './ApplicationState'; let text: (key: string, vars?: object) => string; t.subscribe(t => (text = t)); @@ -60,13 +59,13 @@ export function areActionsAllowed(actionAllowed = true, alertIfNotReady = true): // Assess status and return message to alert user. function assessStateStatus(actionAllowed = true): { isReady: boolean; msg: string } { - const currentState = get(state); + const devices = get(stores.getDevices()); const model = stores.getClassifier().getModel(); - if (currentState.isRecording) return { isReady: false, msg: text('alert.isRecording') }; + if (devices.isRecording) return { isReady: false, msg: text('alert.isRecording') }; if (model.isTraining()) return { isReady: false, msg: text('alert.isTraining') }; - if (!currentState.isInputConnected && actionAllowed) + if (!devices.isInputConnected && actionAllowed) return { isReady: false, msg: text('alert.isNotConnected') }; return { isReady: true, msg: '' }; diff --git a/src/lib/utils/Recording.ts b/src/lib/utils/Recording.ts index 27cbf0085..e94488e21 100644 --- a/src/lib/utils/Recording.ts +++ b/src/lib/utils/Recording.ts @@ -11,13 +11,12 @@ import StaticConfiguration from '../../StaticConfiguration'; import Logger from './Logger'; import { alertUser } from '../stores/uiStore'; import { t } from '../../i18n'; -import { state } from '../stores/ApplicationState'; /** * @deprecated Will be removed in the future. Use store.getRecorder().startRecording(...) instead. */ export const startRecording = (onFinished: (recording: RecordingData) => void) => { - if (get(state).isRecording) { + if (get(stores.getDevices()).isRecording) { Logger.warn('Recording', 'Failed to start recording, already recording'); return; } @@ -26,7 +25,7 @@ export const startRecording = (onFinished: (recording: RecordingData) => void) = throw new Error('Cannot start recording, no live-data store'); } - state.update(e => { + stores.getDevices().update(e => { e.isRecording = true; return e; }); @@ -44,7 +43,7 @@ export const startRecording = (onFinished: (recording: RecordingData) => void) = }); setTimeout(() => { unsubscriber(); - state.update(e => { + stores.getDevices().update(e => { e.isRecording = false; return e; }); diff --git a/src/pages/Homepage.svelte b/src/pages/Homepage.svelte index a3bb15117..4be5ae98a 100644 --- a/src/pages/Homepage.svelte +++ b/src/pages/Homepage.svelte @@ -26,7 +26,7 @@ import { t } from '../i18n'; import Environment from '../lib/Environment'; import DevTools from '../components/features/GoToPlaygroundButton.svelte'; - import { state } from '../lib/stores/ApplicationState'; + import { isLoading } from '../lib/stores/ApplicationState'; type ContentTile = { tile: ComponentType; spanColumns: number }; // Just add the content titles you wish to put on front page, in the order you wish them to be there @@ -38,7 +38,7 @@
-
+
diff --git a/src/pages/data/DataPageNoData.svelte b/src/pages/data/DataPageNoData.svelte index 376e3fd3b..6d8312d75 100644 --- a/src/pages/data/DataPageNoData.svelte +++ b/src/pages/data/DataPageNoData.svelte @@ -9,18 +9,20 @@ import PleaseConnect from '../../components/features/PleaseConnect.svelte'; import StandardButton from '../../components/ui/buttons/StandardButton.svelte'; import { t } from '../../i18n'; - import { state } from '../../lib/stores/ApplicationState'; + import { stores } from '../../lib/stores/Stores'; import { importExampleDataset } from './DataPage'; + + const devices = stores.getDevices();
- {#if !$state.isInputConnected} + {#if !$devices.isInputConnected}
{/if} - {#if $state.isInputConnected} + {#if $devices.isInputConnected}

{$t('content.data.noData')}

diff --git a/src/pages/filter/D3Plot.svelte b/src/pages/filter/D3Plot.svelte index a52379db7..8569f3b39 100644 --- a/src/pages/filter/D3Plot.svelte +++ b/src/pages/filter/D3Plot.svelte @@ -14,12 +14,13 @@ import StaticConfiguration from '../../StaticConfiguration'; import type { RecordingData } from '../../lib/domain/RecordingData'; import { stores } from '../../lib/stores/Stores'; - import { state } from '../../lib/stores/ApplicationState'; + + const devices = stores.getDevices(); export let filterType: FilterType; export let fullScreen: boolean = false; - $: showLive = $state.isInputConnected; + $: showLive = $devices.isInputConnected; $: liveData = $stores.liveData; const highlightedAxes = stores.getHighlightedAxes(); diff --git a/src/pages/home-page-content-tiles/IntroVideoTile.svelte b/src/pages/home-page-content-tiles/IntroVideoTile.svelte index 4d6493f3e..a58db5a0f 100644 --- a/src/pages/home-page-content-tiles/IntroVideoTile.svelte +++ b/src/pages/home-page-content-tiles/IntroVideoTile.svelte @@ -6,7 +6,7 @@

@@ -15,7 +15,7 @@

- {#if $state.modelView == ModelView.TILE} + {#if $modelView == ModelView.TILE} {:else} diff --git a/src/pages/model/stackview/ModelPageStackView.svelte b/src/pages/model/stackview/ModelPageStackView.svelte index 1dcb4abb9..b2d83f9d2 100644 --- a/src/pages/model/stackview/ModelPageStackView.svelte +++ b/src/pages/model/stackview/ModelPageStackView.svelte @@ -13,8 +13,8 @@ import StaticConfiguration from '../../../StaticConfiguration'; import { stores } from '../../../lib/stores/Stores'; import PleaseConnect from '../../../components/features/PleaseConnect.svelte'; - import { state } from '../../../lib/stores/ApplicationState'; + const devices = stores.getDevices(); const classifier = stores.getClassifier(); // In case of manual classification, variables for evaluation let recordingTime = 0; @@ -26,7 +26,7 @@ function classifyClicked() { if (!areActionsAllowed()) return; - $state.isRecording = true; + $devices.isRecording = true; // lastRecording = undefined; // Get duration @@ -42,7 +42,7 @@ setTimeout(() => { clearInterval(loadingInterval); // lastRecording = getPrevData(); - $state.isRecording = false; + $devices.isRecording = false; recordingTime = 0; // classify(); }, duration); @@ -77,7 +77,7 @@
{#if $model.isTrained} - {#if $state.isInputReady} + {#if $devices.isInputReady} {:else} diff --git a/src/pages/model/stackview/ModelPageStackViewContent.svelte b/src/pages/model/stackview/ModelPageStackViewContent.svelte index 88b09e2e3..3d6ac2140 100644 --- a/src/pages/model/stackview/ModelPageStackViewContent.svelte +++ b/src/pages/model/stackview/ModelPageStackViewContent.svelte @@ -9,8 +9,8 @@ import { t } from './../../../i18n'; import { stores } from '../../../lib/stores/Stores'; import OutputGesture from '../../../components/features/model/ModelGesture.svelte'; - import { state } from '../../../lib/stores/ApplicationState'; + const devices = stores.getDevices(); const gestures = stores.getGestures(); // Bool flags to know whether output microbit popup should be show let hasClosedPopup = false; @@ -60,7 +60,7 @@ {/each}
- {#if !$state.isOutputConnected && !hasClosedPopup && hasInteracted} + {#if !$devices.isOutputConnected && !hasClosedPopup && hasInteracted}
{ clearInterval(loadingInterval); // lastRecording = getPrevData(); - $state.isRecording = false; + $devices.isRecording = false; recordingTime = 0; // classify(); }, duration); diff --git a/src/pages/model/tileview/ModelPageTileViewTiles.svelte b/src/pages/model/tileview/ModelPageTileViewTiles.svelte index ed1ae84f4..20c7701d1 100644 --- a/src/pages/model/tileview/ModelPageTileViewTiles.svelte +++ b/src/pages/model/tileview/ModelPageTileViewTiles.svelte @@ -12,8 +12,8 @@ import StaticConfiguration from '../../../StaticConfiguration'; import { stores } from '../../../lib/stores/Stores'; import OutputGesture from '../../../components/features/model/ModelGesture.svelte'; - import { state } from '../../../lib/stores/ApplicationState'; + const devices = stores.getDevices(); // In case of manual classification, variables for evaluation let recordingTime = 0; // let lastRecording; @@ -32,7 +32,7 @@ function classifyClicked() { if (!areActionsAllowed()) return; - $state.isRecording = true; + $devices.isRecording = true; // lastRecording = undefined; // Get duration @@ -48,7 +48,7 @@ setTimeout(() => { clearInterval(loadingInterval); // lastRecording = getPrevData(); - $state.isRecording = false; + $devices.isRecording = false; recordingTime = 0; // classify(); }, duration); diff --git a/src/pages/training/PredictionLegend.svelte b/src/pages/training/PredictionLegend.svelte index 08d1a5ff7..fa315773f 100644 --- a/src/pages/training/PredictionLegend.svelte +++ b/src/pages/training/PredictionLegend.svelte @@ -5,11 +5,11 @@ --> {#each $gestures as gesture} @@ -20,7 +20,7 @@

{gesture.name}

- {#if $state.isInputReady} + {#if $devices.isInputReady}

{(($confidences.get(gesture.ID) ?? 0) * 100).toFixed(1)}%

diff --git a/src/pages/training/TrainingPageModelView.svelte b/src/pages/training/TrainingPageModelView.svelte index 7c2c384ab..7d3ade07f 100644 --- a/src/pages/training/TrainingPageModelView.svelte +++ b/src/pages/training/TrainingPageModelView.svelte @@ -12,8 +12,8 @@ import { stores } from '../../lib/stores/Stores'; import PleaseConnect from '../../components/features/PleaseConnect.svelte'; import FiltersList from '../../components/features/filters/FiltersList.svelte'; - import { state } from '../../lib/stores/ApplicationState'; + const devices = stores.getDevices(); const selectedModel = stores.getSelectedModel(); const showFilterList = hasFeature(Feature.KNN_MODEL); @@ -23,14 +23,14 @@ {#if showFilterList} {/if} - {#if $selectedModel.id === ModelRegistry.KNN.id && $state.isInputConnected} + {#if $selectedModel.id === ModelRegistry.KNN.id && $devices.isInputConnected} {:else if $selectedModel.id === ModelRegistry.NeuralNetwork.id} {/if}
-{#if !$state.isInputConnected} +{#if !$devices.isInputConnected}
diff --git a/src/pages/validation/ValidationGestureSelectGestureCard.svelte b/src/pages/validation/ValidationGestureSelectGestureCard.svelte index 7a1466094..623bd3fab 100644 --- a/src/pages/validation/ValidationGestureSelectGestureCard.svelte +++ b/src/pages/validation/ValidationGestureSelectGestureCard.svelte @@ -18,18 +18,18 @@ import { get } from 'svelte/store'; import Logger from '../../lib/utils/Logger'; import StandardButton from '../../components/ui/buttons/StandardButton.svelte'; - import { state } from '../../lib/stores/ApplicationState'; export let gesture: Gesture; export let onNoMicrobitSelect: () => void; + const devices = stores.getDevices(); const validationSets = stores.getValidationSets(); const recorder = stores.getRecorder(); $: isThisRecording = $recorder.recordingGesture === gesture.getId(); const selectClicked = (gesture: Gesture): void => { - if (!$state.isInputConnected) { + if (!$devices.isInputConnected) { chosenGesture.update(gesture => { gesture = null; return gesture; From 2377f59c1f2ebb1044c859d5e47ca9a5dd719f2e Mon Sep 17 00:00:00 2001 From: "A. Malthe Henriksen" Date: Thu, 5 Jun 2025 13:36:52 +0200 Subject: [PATCH 19/49] Add license identifier to devices store --- src/lib/domain/Devices.ts | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/lib/domain/Devices.ts b/src/lib/domain/Devices.ts index 21dfb442f..8885d363f 100644 --- a/src/lib/domain/Devices.ts +++ b/src/lib/domain/Devices.ts @@ -1,3 +1,8 @@ +/** + * (c) 2023-2025, Center for Computational Thinking and Design at Aarhus University and contributors + * + * SPDX-License-Identifier: MIT + */ import { get, writable, From 315172c3d6871b912b7bce125eaea89bd973103b Mon Sep 17 00:00:00 2001 From: "A. Malthe Henriksen" Date: Tue, 10 Jun 2025 23:41:07 +0200 Subject: [PATCH 20/49] Move config into the training page --- features.json | 3 +- .../ml-machine-simple/features.json | 3 +- .../ml-machine/features.json | 3 +- .../unbranded/features.json | 3 +- src/components/features/PleaseConnect.svelte | 4 +- .../knngraph/AxesFilterVectorView.svelte | 4 +- .../features/training/KNNModelSettings.svelte | 33 +++++++ .../training/NeuralNetworkSettings.svelte | 61 ++++++++++++ .../training/TrainingPageModelSettings.svelte | 20 ++++ src/components/ui/NumberSelector.svelte | 5 +- src/lib/FeatureToggles.ts | 1 + .../training/KnnModelTrainingPageView.svelte | 49 +++++----- .../NeuralNetworkTrainingPageView.svelte | 63 ++++++------ .../training/TrainingPageModelView.svelte | 22 +++-- .../controlbar/KNNModelDropdown.svelte | 50 ---------- .../controlbar/NeuralNetworkDropdown.svelte | 96 ------------------- .../controlbar/TrainingPageTabs.svelte | 16 ++-- 17 files changed, 209 insertions(+), 227 deletions(-) create mode 100644 src/components/features/training/KNNModelSettings.svelte create mode 100644 src/components/features/training/NeuralNetworkSettings.svelte create mode 100644 src/components/features/training/TrainingPageModelSettings.svelte delete mode 100644 src/pages/training/controlbar/KNNModelDropdown.svelte delete mode 100644 src/pages/training/controlbar/NeuralNetworkDropdown.svelte diff --git a/features.json b/features.json index 9b18c3739..7d66c099a 100644 --- a/features.json +++ b/features.json @@ -5,5 +5,6 @@ "makecode": true, "liveGraphInputValues": true, "recordingScrubberValues": true, - "modelValidation": true + "modelValidation": true, + "modelSettings": true } diff --git a/src/__viteBuildVariants__/ml-machine-simple/features.json b/src/__viteBuildVariants__/ml-machine-simple/features.json index 973184add..1da6f6323 100644 --- a/src/__viteBuildVariants__/ml-machine-simple/features.json +++ b/src/__viteBuildVariants__/ml-machine-simple/features.json @@ -5,5 +5,6 @@ "makecode": false, "liveGraphInputValues": false, "recordingScrubberValues": false, - "modelValidation": false + "modelValidation": false, + "modelSettings": false } diff --git a/src/__viteBuildVariants__/ml-machine/features.json b/src/__viteBuildVariants__/ml-machine/features.json index d8623ddc3..ba3c632a0 100644 --- a/src/__viteBuildVariants__/ml-machine/features.json +++ b/src/__viteBuildVariants__/ml-machine/features.json @@ -5,5 +5,6 @@ "makecode": true, "liveGraphInputValues": true, "recordingScrubberValues": true, - "modelValidation": true + "modelValidation": true, + "modelSettings": true } diff --git a/src/__viteBuildVariants__/unbranded/features.json b/src/__viteBuildVariants__/unbranded/features.json index 9b18c3739..7d66c099a 100644 --- a/src/__viteBuildVariants__/unbranded/features.json +++ b/src/__viteBuildVariants__/unbranded/features.json @@ -5,5 +5,6 @@ "makecode": true, "liveGraphInputValues": true, "recordingScrubberValues": true, - "modelValidation": true + "modelValidation": true, + "modelSettings": true } diff --git a/src/components/features/PleaseConnect.svelte b/src/components/features/PleaseConnect.svelte index eebf28970..efd838f37 100644 --- a/src/components/features/PleaseConnect.svelte +++ b/src/components/features/PleaseConnect.svelte @@ -16,10 +16,10 @@
-

+

{$t('menu.trainer.notConnected1')}

-

+

{$t('menu.trainer.notConnected2')}

diff --git a/src/components/features/graphs/knngraph/AxesFilterVectorView.svelte b/src/components/features/graphs/knngraph/AxesFilterVectorView.svelte index 975914728..65345b6f1 100644 --- a/src/components/features/graphs/knngraph/AxesFilterVectorView.svelte +++ b/src/components/features/graphs/knngraph/AxesFilterVectorView.svelte @@ -97,7 +97,7 @@
{#if $highlightedAxis !== undefined} -
+
{#each $availableAxes as axis}
@@ -115,7 +115,7 @@ {/each}
{#if $highlightedAxis.length === 1} -
+
{#each $filters as filter, index}

{filter.getName()}

{/each} diff --git a/src/components/features/training/KNNModelSettings.svelte b/src/components/features/training/KNNModelSettings.svelte new file mode 100644 index 000000000..11301e360 --- /dev/null +++ b/src/components/features/training/KNNModelSettings.svelte @@ -0,0 +1,33 @@ + + + +
+

Neighbours (K)

+
+ knnModelSettings.setK(val)} /> +
+

Normalize

+
+ +
+
diff --git a/src/components/features/training/NeuralNetworkSettings.svelte b/src/components/features/training/NeuralNetworkSettings.svelte new file mode 100644 index 000000000..bca2f6257 --- /dev/null +++ b/src/components/features/training/NeuralNetworkSettings.svelte @@ -0,0 +1,61 @@ + + + +
+

Learning rate

+
+ +
+ +

Epochs

+
+ neuralNetworkSettings.setNoOfEpochs(val)} /> +
+ +

Nodes

+
+ neuralNetworkSettings.setNoOfUnits(val)} /> +
+ +

Batch size

+
+ neuralNetworkSettings.setBatchSize(val)} /> +
+
diff --git a/src/components/features/training/TrainingPageModelSettings.svelte b/src/components/features/training/TrainingPageModelSettings.svelte new file mode 100644 index 000000000..4dfe287bc --- /dev/null +++ b/src/components/features/training/TrainingPageModelSettings.svelte @@ -0,0 +1,20 @@ + + + + +
+ {#if $selectedModel.id === ModelRegistry.NeuralNetwork.id} + {:else} + {/if} +
diff --git a/src/components/ui/NumberSelector.svelte b/src/components/ui/NumberSelector.svelte index 3edf3f69b..4d66bdbdd 100644 --- a/src/components/ui/NumberSelector.svelte +++ b/src/components/ui/NumberSelector.svelte @@ -43,7 +43,7 @@
- @@ -53,10 +53,11 @@ on:blur={() => { handleChange(); }} - class="max-w-20 text-center border-solid border-1 px-2 bg-primary border-primaryborder" /> + class="max-w-20 text-secondarytext text-center border-solid border-1 px-2 bg-primary border-primaryborder" /> + diff --git a/src/lib/FeatureToggles.ts b/src/lib/FeatureToggles.ts index a7b706392..40dea7452 100644 --- a/src/lib/FeatureToggles.ts +++ b/src/lib/FeatureToggles.ts @@ -15,6 +15,7 @@ export enum Feature { LIVE_GRAPH_INPUT_VALUES = 'liveGraphInputValues', RECORDING_SCRUBBER_VALUES = 'recordingScrubberValues', MODEL_VALIDATION = 'modelValidation', + MODEL_SETTINGS = 'modelSettings', } export const hasFeature = (feature: Feature): boolean => { diff --git a/src/pages/training/KnnModelTrainingPageView.svelte b/src/pages/training/KnnModelTrainingPageView.svelte index d1612e9be..637c17842 100644 --- a/src/pages/training/KnnModelTrainingPageView.svelte +++ b/src/pages/training/KnnModelTrainingPageView.svelte @@ -13,6 +13,10 @@ import StandardButton from '../../components/ui/buttons/StandardButton.svelte'; import { knnHasTrained } from '../../lib/stores/KNNStores'; import { trainKNNModel } from './TrainingPage'; + import KnnModelSettings from '../../components/features/training/KNNModelSettings.svelte'; + import { onMount } from 'svelte'; + + const devices = stores.getDevices(); const classifier = stores.getClassifier(); const gestures = stores.getGestures(); const filters = classifier.getFilters(); @@ -46,45 +50,40 @@ } -
+
{#if !$knnHasTrained} -
- trainKNNModel()} - >{$t('menu.trainer.trainModelButtonSimple')} +
+
+ +
+ {#if $highlightedAxis.length === 1} +
+ trainKNNModel()}> + {$t('menu.trainer.trainModelButtonSimple')} + +
+ {/if}
{/if} {#if $highlightedAxis.length === 1}
-
-
-
-
changeK(-1)} - class="bg-secondary font-bold text-secondarytext cursor-pointer select-none hover:bg-opacity-60 border-primary border-r-1 content-center px-2 rounded-l-xl"> - - -
-
changeK(1)} - class="bg-secondary border-primary text-secondarytext cursor-pointer hover:bg-opacity-60 select-none content-center px-2 rounded-r-xl"> - + -
-
-

- {$knnModelSettings.k} - {$t('content.trainer.knn.neighbours')} -

+
+
+ +
+
+
- -
+
{#if $filters.length == 2 && $classifier.model.isTrained && $highlightedAxis.length === 1} {:else} -
+

{$t('menu.trainer.knn.onlyTwoFilters')}

diff --git a/src/pages/training/NeuralNetworkTrainingPageView.svelte b/src/pages/training/NeuralNetworkTrainingPageView.svelte index 7a281bbee..e6454f679 100644 --- a/src/pages/training/NeuralNetworkTrainingPageView.svelte +++ b/src/pages/training/NeuralNetworkTrainingPageView.svelte @@ -12,6 +12,7 @@ import LossGraph from '../../components/features/graphs/LossGraph.svelte'; import StandardButton from '../../components/ui/buttons/StandardButton.svelte'; import Tooltip from '../../components/ui/Tooltip.svelte'; + import NeuralNetworkSettings from '../../components/features/training/NeuralNetworkSettings.svelte'; const classifier = stores.getClassifier(); const model = classifier.getModel(); @@ -29,34 +30,40 @@ : 'menu.trainer.trainNewModelButtonSimple'; -
- {#if $model.isTraining} -
- -
- {#if !hasFeature(Feature.LOSS_GRAPH)} -

{$t('menu.trainer.isTrainingModelButton')}

+
+
+ +
+ +
+ {#if $model.isTraining} +
+ +
+ {#if !hasFeature(Feature.LOSS_GRAPH)} +

{$t('menu.trainer.isTrainingModelButton')}

+ {/if} + {:else} + {#if $model.isTrained && !hasFeature(Feature.LOSS_GRAPH)} +

{$t('menu.trainer.TrainingFinished')}

+

{$t('menu.trainer.TrainingFinished.body')}

+ {/if} +
+ + + {$t(trainButtonSimpleLabel)} + + +
{/if} - {:else} - {#if $model.isTrained && !hasFeature(Feature.LOSS_GRAPH)} -

{$t('menu.trainer.TrainingFinished')}

-

{$t('menu.trainer.TrainingFinished.body')}

+ {#if $loss.length > 0 && hasFeature(Feature.LOSS_GRAPH) && ($model.isTrained || $model.isTraining)} + {/if} -
- - - {$t(trainButtonSimpleLabel)} - - -
- {/if} - {#if $loss.length > 0 && hasFeature(Feature.LOSS_GRAPH) && ($model.isTrained || $model.isTraining)} - - {/if} +
diff --git a/src/pages/training/TrainingPageModelView.svelte b/src/pages/training/TrainingPageModelView.svelte index 7d3ade07f..3710482e9 100644 --- a/src/pages/training/TrainingPageModelView.svelte +++ b/src/pages/training/TrainingPageModelView.svelte @@ -12,26 +12,32 @@ import { stores } from '../../lib/stores/Stores'; import PleaseConnect from '../../components/features/PleaseConnect.svelte'; import FiltersList from '../../components/features/filters/FiltersList.svelte'; + import TrainingPageModelSettings from '../../components/features/training/TrainingPageModelSettings.svelte'; const devices = stores.getDevices(); const selectedModel = stores.getSelectedModel(); const showFilterList = hasFeature(Feature.KNN_MODEL); -
-
+
+
{#if showFilterList} {/if} - {#if $selectedModel.id === ModelRegistry.KNN.id && $devices.isInputConnected} - - {:else if $selectedModel.id === ModelRegistry.NeuralNetwork.id} - - {/if} +
+
+ +
+ {#if $selectedModel.id === ModelRegistry.KNN.id} + + {:else if $selectedModel.id === ModelRegistry.NeuralNetwork.id} + + {/if} +
{#if !$devices.isInputConnected} -
+
{/if} diff --git a/src/pages/training/controlbar/KNNModelDropdown.svelte b/src/pages/training/controlbar/KNNModelDropdown.svelte deleted file mode 100644 index e4ea0a71d..000000000 --- a/src/pages/training/controlbar/KNNModelDropdown.svelte +++ /dev/null @@ -1,50 +0,0 @@ - - - - { - selectModel(ModelRegistry.KNN); - }} - fillOnHover - outlined={!isSelected} - small> -

KNN Model

- -
-
-
-

Neighbours (K)

-
- knnModelSettings.setK(val)} /> -
-

Normalize

-
- -
-
-
-
-
diff --git a/src/pages/training/controlbar/NeuralNetworkDropdown.svelte b/src/pages/training/controlbar/NeuralNetworkDropdown.svelte deleted file mode 100644 index 501b7366a..000000000 --- a/src/pages/training/controlbar/NeuralNetworkDropdown.svelte +++ /dev/null @@ -1,96 +0,0 @@ - - - - { - selectModel(ModelRegistry.NeuralNetwork); - }} - fillOnHover - outlined={!isSelected} - small> -

Neural Network

- -
-
-
-

Learning rate

-
- -
- -

Epochs

-
- neuralNetworkSettings.setNoOfEpochs(val)} /> -
- -

Nodes

-
- neuralNetworkSettings.setNoOfUnits(val)} /> -
- -

Batch size

-
- neuralNetworkSettings.setBatchSize(val)} /> -
- - -
-
-
-
diff --git a/src/pages/training/controlbar/TrainingPageTabs.svelte b/src/pages/training/controlbar/TrainingPageTabs.svelte index 3ffdaa0b8..0b1dee866 100644 --- a/src/pages/training/controlbar/TrainingPageTabs.svelte +++ b/src/pages/training/controlbar/TrainingPageTabs.svelte @@ -9,8 +9,6 @@ import { Feature, hasFeature } from '../../../lib/FeatureToggles'; import { t } from '../../../i18n'; import { stores } from '../../../lib/stores/Stores'; - import NeuralNetworkDropdown from './NeuralNetworkDropdown.svelte'; - import KnnModelDropdown from './KNNModelDropdown.svelte'; import { navigate, Paths } from '../../../router/Router'; import StandardButton from '../../../components/ui/buttons/StandardButton.svelte'; @@ -29,14 +27,12 @@ {#if showTabBar}
-
-
- -
-
- -
+
+ {#each ModelRegistry.getModels() as model} + selectedModel.set(model)}> + {model.title} + + {/each}
From 51ca447ef6997773cecb2ff9b33be96482b34259 Mon Sep 17 00:00:00 2001 From: "A. Malthe Henriksen" Date: Tue, 10 Jun 2025 23:43:33 +0200 Subject: [PATCH 21/49] Add failsafe for model settings features --- .../features/training/KNNModelSettings.svelte | 37 +++++---- .../training/NeuralNetworkSettings.svelte | 77 ++++++++++--------- .../training/TrainingPageModelSettings.svelte | 4 +- .../controlbar/TrainingPageTabs.svelte | 5 +- 4 files changed, 65 insertions(+), 58 deletions(-) diff --git a/src/components/features/training/KNNModelSettings.svelte b/src/components/features/training/KNNModelSettings.svelte index 11301e360..ad5f0f5d0 100644 --- a/src/components/features/training/KNNModelSettings.svelte +++ b/src/components/features/training/KNNModelSettings.svelte @@ -4,30 +4,33 @@ SPDX-License-Identifier: MIT --> -
-

Neighbours (K)

-
- knnModelSettings.setK(val)} /> -
-

Normalize

-
- +{#if hasFeature(Feature.MODEL_SETTINGS)} +
+

Neighbours (K)

+
+ knnModelSettings.setK(val)} /> +
+

Normalize

+
+ +
-
+{/if} diff --git a/src/components/features/training/NeuralNetworkSettings.svelte b/src/components/features/training/NeuralNetworkSettings.svelte index bca2f6257..4a99fc2ab 100644 --- a/src/components/features/training/NeuralNetworkSettings.svelte +++ b/src/components/features/training/NeuralNetworkSettings.svelte @@ -8,6 +8,7 @@ import windi from '../../../../windi.config'; import RangeSlider from 'svelte-range-slider-pips'; import NumberSelector from '../../ui/NumberSelector.svelte'; + import { Feature, hasFeature } from '../../../lib/FeatureToggles'; const neuralNetworkSettings = stores.getNeuralNetworkSettings(); const color = windi.theme.extend.colors.primary; @@ -18,44 +19,46 @@ } -
-

Learning rate

-
- -
+{#if hasFeature(Feature.MODEL_SETTINGS)} +
+

Learning rate

+
+ +
-

Epochs

-
- neuralNetworkSettings.setNoOfEpochs(val)} /> -
+

Epochs

+
+ neuralNetworkSettings.setNoOfEpochs(val)} /> +
-

Nodes

-
- neuralNetworkSettings.setNoOfUnits(val)} /> -
+

Nodes

+
+ neuralNetworkSettings.setNoOfUnits(val)} /> +
-

Batch size

-
- neuralNetworkSettings.setBatchSize(val)} /> +

Batch size

+
+ neuralNetworkSettings.setBatchSize(val)} /> +
-
+{/if} diff --git a/src/components/features/training/TrainingPageModelSettings.svelte b/src/components/features/training/TrainingPageModelSettings.svelte index 4dfe287bc..12043d44e 100644 --- a/src/components/features/training/TrainingPageModelSettings.svelte +++ b/src/components/features/training/TrainingPageModelSettings.svelte @@ -14,7 +14,5 @@
- {#if $selectedModel.id === ModelRegistry.NeuralNetwork.id} - {:else} - {/if} + {#if $selectedModel.id === ModelRegistry.NeuralNetwork.id}{:else}{/if}
diff --git a/src/pages/training/controlbar/TrainingPageTabs.svelte b/src/pages/training/controlbar/TrainingPageTabs.svelte index 0b1dee866..1d04e4650 100644 --- a/src/pages/training/controlbar/TrainingPageTabs.svelte +++ b/src/pages/training/controlbar/TrainingPageTabs.svelte @@ -29,7 +29,10 @@
{#each ModelRegistry.getModels() as model} - selectedModel.set(model)}> + selectedModel.set(model)}> {model.title} {/each} From 8fd62cdfdd03a58d549fad9a39a414a1dd96507b Mon Sep 17 00:00:00 2001 From: "A. Malthe Henriksen" Date: Mon, 16 Jun 2025 20:16:35 +0200 Subject: [PATCH 22/49] Add functionality to export to csv --- src/__tests__/csv/csv.test.ts | 71 +++++++++++++++++++++++++++++++++++ src/lib/utils/CSVUtils.ts | 26 +++++++++++++ 2 files changed, 97 insertions(+) create mode 100644 src/__tests__/csv/csv.test.ts create mode 100644 src/lib/utils/CSVUtils.ts diff --git a/src/__tests__/csv/csv.test.ts b/src/__tests__/csv/csv.test.ts new file mode 100644 index 000000000..4e4628ecc --- /dev/null +++ b/src/__tests__/csv/csv.test.ts @@ -0,0 +1,71 @@ +/** + * @vitest-environment jsdom + */ +/** + * (c) 2023-2025, Center for Computational Thinking and Design at Aarhus University and contributors + * + * SPDX-License-Identifier: MIT + */ + +import { writable } from "svelte/store"; +import type { RecordingData } from "../../lib/domain/RecordingData"; +import Gesture from "../../lib/domain/stores/gesture/Gesture"; +import type { PersistedGestureData } from "../../lib/domain/stores/gesture/Gestures"; +import type GestureConfidence from "../../lib/domain/stores/gesture/GestureConfidence"; +import { serializeGestureRecordingsToCSV } from "../../lib/utils/CSVUtils"; + +describe('CSV Test', () => { + // A crude way to enforce direction of dependencies, inspired by ArchUnit for java + test('Convert recording', () => { + const input: RecordingData = { + ID: 123, + labels: ["x", "y", "z"], + samples: [{ + vector: [1, 2, 3], + }, { + vector: [4, 5, 6], + }, { + vector: [7, 8, 9], + }] + } + const data = writable({recordings:[input], name:"Test;Gesture"} as PersistedGestureData); + const confidence = writable({}) as unknown as GestureConfidence; + const gesture: Gesture = new Gesture(data, confidence, () => void 0); + const result = serializeGestureRecordingsToCSV([gesture]); + expect(result).toBe("gesture;sample;x;y;z\nTest\\;Gesture;0;1;2;3\nTest\\;Gesture;1;4;5;6\nTest\\;Gesture;2;7;8;9"); + }); + + test('Convert multiple gestures', () => { + const input1: RecordingData = { + ID: 123, + labels: ["x", "y", "z"], + samples: [{ + vector: [1, 2, 3], + }, { + vector: [4, 5, 6], + }] + }; + const input2: RecordingData = { + ID: 456, + labels: ["x", "y", "z"], + samples: [{ + vector: [7, 8, 9], + }, { + vector: [10, 11, 12], + }] + }; + const data1 = writable({recordings:[input1], name:"Gesture1"} as PersistedGestureData); + const data2 = writable({recordings:[input2], name:"Gesture2"} as PersistedGestureData); + const confidence = writable({}) as unknown as GestureConfidence; + const gesture1: Gesture = new Gesture(data1, confidence, () => void 0); + const gesture2: Gesture = new Gesture(data2, confidence, () => void 0); + const result = serializeGestureRecordingsToCSV([gesture1, gesture2]); + expect(result).toBe( + "gesture;sample;x;y;z\n" + + "Gesture1;0;1;2;3\n" + + "Gesture1;1;4;5;6\n" + + "Gesture2;0;7;8;9\n" + + "Gesture2;1;10;11;12" + ); + }); +}); diff --git a/src/lib/utils/CSVUtils.ts b/src/lib/utils/CSVUtils.ts new file mode 100644 index 000000000..b4e598fd0 --- /dev/null +++ b/src/lib/utils/CSVUtils.ts @@ -0,0 +1,26 @@ +/** + * (c) 2023-2025, Center for Computational Thinking and Design at Aarhus University and contributors + * + * SPDX-License-Identifier: MIT + */ + +import type { RecordingData } from "../domain/RecordingData"; +import type Gesture from "../domain/stores/gesture/Gesture"; + +export const serializeGestureRecordingsToCSV = (gestures: Gesture[]) => { + const axes = gestures[0].getRecordings()[0].labels + const headers = ["gesture","sample",...axes].join(";") + return [ + headers, + gestures.map(gesture => serializeGestureToCSV(gesture)).join("\n") + ].join("\n") +} + +const serializeGestureToCSV = (gesture: Gesture) => { + const gestureName = gesture.getName() + return gesture.getRecordings().map(recording => serializeRecordingToCsv(recording, gestureName)).join("\n") +} + +const serializeRecordingToCsv = (recording: RecordingData, gestureName: string): string => { + return recording.samples.map((sample, idx) => gestureName.replace(";", "\\;") + ";" + idx + ";" + sample.vector.join(";")).join("\n") +} \ No newline at end of file From 0371e3f64d00104e7b2fccf81bdaefe506a923da Mon Sep 17 00:00:00 2001 From: "A. Malthe Henriksen" Date: Mon, 16 Jun 2025 21:48:41 +0200 Subject: [PATCH 23/49] Implement CSV export --- src/__tests__/csv/csv.test.ts | 161 +++++++++++------- .../features/datacollection/Gesture.svelte | 6 +- src/components/ui/Recording.svelte | 31 +++- src/lib/utils/CSVUtils.ts | 51 ++++-- .../training/TrainingPageModelView.svelte | 2 - .../ValidationGestureRecordingsCard.svelte | 1 + 6 files changed, 175 insertions(+), 77 deletions(-) diff --git a/src/__tests__/csv/csv.test.ts b/src/__tests__/csv/csv.test.ts index 4e4628ecc..d864626eb 100644 --- a/src/__tests__/csv/csv.test.ts +++ b/src/__tests__/csv/csv.test.ts @@ -7,65 +7,110 @@ * SPDX-License-Identifier: MIT */ -import { writable } from "svelte/store"; -import type { RecordingData } from "../../lib/domain/RecordingData"; -import Gesture from "../../lib/domain/stores/gesture/Gesture"; -import type { PersistedGestureData } from "../../lib/domain/stores/gesture/Gestures"; -import type GestureConfidence from "../../lib/domain/stores/gesture/GestureConfidence"; -import { serializeGestureRecordingsToCSV } from "../../lib/utils/CSVUtils"; +import { writable } from 'svelte/store'; +import type { RecordingData } from '../../lib/domain/RecordingData'; +import Gesture from '../../lib/domain/stores/gesture/Gesture'; +import type { PersistedGestureData } from '../../lib/domain/stores/gesture/Gestures'; +import type GestureConfidence from '../../lib/domain/stores/gesture/GestureConfidence'; +import { + serializeGestureRecordingsToCSV, + serializeRecordingToCsvWithoutGestureName, +} from '../../lib/utils/CSVUtils'; describe('CSV Test', () => { - // A crude way to enforce direction of dependencies, inspired by ArchUnit for java - test('Convert recording', () => { - const input: RecordingData = { - ID: 123, - labels: ["x", "y", "z"], - samples: [{ - vector: [1, 2, 3], - }, { - vector: [4, 5, 6], - }, { - vector: [7, 8, 9], - }] - } - const data = writable({recordings:[input], name:"Test;Gesture"} as PersistedGestureData); - const confidence = writable({}) as unknown as GestureConfidence; - const gesture: Gesture = new Gesture(data, confidence, () => void 0); - const result = serializeGestureRecordingsToCSV([gesture]); - expect(result).toBe("gesture;sample;x;y;z\nTest\\;Gesture;0;1;2;3\nTest\\;Gesture;1;4;5;6\nTest\\;Gesture;2;7;8;9"); - }); + // A crude way to enforce direction of dependencies, inspired by ArchUnit for java + test('Convert recording', () => { + const input: RecordingData = { + ID: 123, + labels: ['x', 'y', 'z'], + samples: [ + { + vector: [1, 2, 3], + }, + { + vector: [4, 5, 6], + }, + { + vector: [7, 8, 9], + }, + ], + }; + const data = writable({ + recordings: [input], + name: 'Test;Gesture', + } as PersistedGestureData); + const confidence = writable({}) as unknown as GestureConfidence; + const gesture: Gesture = new Gesture(data, confidence, () => void 0); + const result = serializeGestureRecordingsToCSV([gesture]); + expect(result).toBe( + 'gesture;sample;x;y;z\nTest\\;Gesture;0;1;2;3\nTest\\;Gesture;1;4;5;6\nTest\\;Gesture;2;7;8;9', + ); + }); - test('Convert multiple gestures', () => { - const input1: RecordingData = { - ID: 123, - labels: ["x", "y", "z"], - samples: [{ - vector: [1, 2, 3], - }, { - vector: [4, 5, 6], - }] - }; - const input2: RecordingData = { - ID: 456, - labels: ["x", "y", "z"], - samples: [{ - vector: [7, 8, 9], - }, { - vector: [10, 11, 12], - }] - }; - const data1 = writable({recordings:[input1], name:"Gesture1"} as PersistedGestureData); - const data2 = writable({recordings:[input2], name:"Gesture2"} as PersistedGestureData); - const confidence = writable({}) as unknown as GestureConfidence; - const gesture1: Gesture = new Gesture(data1, confidence, () => void 0); - const gesture2: Gesture = new Gesture(data2, confidence, () => void 0); - const result = serializeGestureRecordingsToCSV([gesture1, gesture2]); - expect(result).toBe( - "gesture;sample;x;y;z\n" + - "Gesture1;0;1;2;3\n" + - "Gesture1;1;4;5;6\n" + - "Gesture2;0;7;8;9\n" + - "Gesture2;1;10;11;12" - ); - }); + test('Convert multiple gestures', () => { + const input1: RecordingData = { + ID: 123, + labels: ['x', 'y', 'z'], + samples: [ + { + vector: [1, 2, 3], + }, + { + vector: [4, 5, 6], + }, + ], + }; + const input2: RecordingData = { + ID: 456, + labels: ['x', 'y', 'z'], + samples: [ + { + vector: [7, 8, 9], + }, + { + vector: [10, 11, 12], + }, + ], + }; + const data1 = writable({ + recordings: [input1], + name: 'Gesture1', + } as PersistedGestureData); + const data2 = writable({ + recordings: [input2], + name: 'Gesture2', + } as PersistedGestureData); + const confidence = writable({}) as unknown as GestureConfidence; + const gesture1: Gesture = new Gesture(data1, confidence, () => void 0); + const gesture2: Gesture = new Gesture(data2, confidence, () => void 0); + const result = serializeGestureRecordingsToCSV([gesture1, gesture2]); + expect(result).toBe( + 'gesture;sample;x;y;z\n' + + 'Gesture1;0;1;2;3\n' + + 'Gesture1;1;4;5;6\n' + + 'Gesture2;0;7;8;9\n' + + 'Gesture2;1;10;11;12', + ); + }); + + test('Serialize recording without gesture name (with headers)', () => { + const input: RecordingData = { + ID: 123, + labels: ['x', 'y', 'z'], + samples: [ + { + vector: [1, 2, 3], + }, + { + vector: [4, 5, 6], + }, + { + vector: [7, 8, 9], + }, + ], + }; + + const result = serializeRecordingToCsvWithoutGestureName(input); + expect(result).toBe('sample;x;y;z\n' + '0;1;2;3\n' + '1;4;5;6\n' + '2;7;8;9'); + }); }); diff --git a/src/components/features/datacollection/Gesture.svelte b/src/components/features/datacollection/Gesture.svelte index c554b6703..db962ca06 100644 --- a/src/components/features/datacollection/Gesture.svelte +++ b/src/components/features/datacollection/Gesture.svelte @@ -218,7 +218,11 @@
{#each $gesture.recordings as recording (String($gesture.ID) + String(recording.ID))} - + {/each}
diff --git a/src/components/ui/Recording.svelte b/src/components/ui/Recording.svelte index 069832fbc..e42654d97 100644 --- a/src/components/ui/Recording.svelte +++ b/src/components/ui/Recording.svelte @@ -11,16 +11,21 @@ import GestureDot from './GestureDot.svelte'; import RecordingGraph from '../features/graphs/recording/RecordingGraph.svelte'; import type { RecordingData } from '../../lib/domain/RecordingData'; + import Tooltip from './Tooltip.svelte'; + import { serializeRecordingToCsvWithoutGestureName } from '../../lib/utils/CSVUtils'; // get recording from mother prop export let recording: RecordingData; + export let gestureId: GestureID; export let onDelete: (recording: RecordingData) => void; export let dot: { gesture: GestureID; color: string } | undefined = undefined; + export let downloadable: boolean = false; $: dotGesture = dot?.gesture ? stores.getGestures().getGesture(dot?.gesture) : undefined; + $: gesture = stores.getGestures().getGesture(gestureId); let hide = false; // Method for propagating deletion of recording @@ -35,7 +40,20 @@ onDelete(recording); }, 450); } - let isDotHovered = false; + + function bottomRightButtonClicked() { + const csvContent = serializeRecordingToCsvWithoutGestureName(recording); + const blob = new Blob([csvContent], { type: 'text/csv;charset=utf-8;' }); + const url = URL.createObjectURL(blob); + + const link = document.createElement('a'); + link.href = url; + link.download = `${gesture.getName()}_recording_${recording.ID}.csv`; + link.click(); + + // Clean up the URL object + URL.revokeObjectURL(url); + }
@@ -60,4 +78,15 @@ on:click={deleteClicked} />
+ + + {#if downloadable} + + + + {/if}
diff --git a/src/lib/utils/CSVUtils.ts b/src/lib/utils/CSVUtils.ts index b4e598fd0..6ebab88b1 100644 --- a/src/lib/utils/CSVUtils.ts +++ b/src/lib/utils/CSVUtils.ts @@ -4,23 +4,44 @@ * SPDX-License-Identifier: MIT */ -import type { RecordingData } from "../domain/RecordingData"; -import type Gesture from "../domain/stores/gesture/Gesture"; +import type { RecordingData } from '../domain/RecordingData'; +import type Gesture from '../domain/stores/gesture/Gesture'; export const serializeGestureRecordingsToCSV = (gestures: Gesture[]) => { - const axes = gestures[0].getRecordings()[0].labels - const headers = ["gesture","sample",...axes].join(";") - return [ - headers, - gestures.map(gesture => serializeGestureToCSV(gesture)).join("\n") - ].join("\n") -} + const axes = gestures[0].getRecordings()[0].labels; + const headers = ['gesture', 'sample', ...axes].join(';'); + return [ + headers, + gestures.map(gesture => serializeGestureToCSV(gesture)).join('\n'), + ].join('\n'); +}; const serializeGestureToCSV = (gesture: Gesture) => { - const gestureName = gesture.getName() - return gesture.getRecordings().map(recording => serializeRecordingToCsv(recording, gestureName)).join("\n") -} + const gestureName = gesture.getName(); + return gesture + .getRecordings() + .map(recording => serializeRecordingToCsv(recording, gestureName)) + .join('\n'); +}; -const serializeRecordingToCsv = (recording: RecordingData, gestureName: string): string => { - return recording.samples.map((sample, idx) => gestureName.replace(";", "\\;") + ";" + idx + ";" + sample.vector.join(";")).join("\n") -} \ No newline at end of file +const serializeRecordingToCsv = ( + recording: RecordingData, + gestureName: string, +): string => { + return recording.samples + .map( + (sample, idx) => + gestureName.replace(';', '\\;') + ';' + idx + ';' + sample.vector.join(';'), + ) + .join('\n'); +}; + +export const serializeRecordingToCsvWithoutGestureName = ( + recording: RecordingData, +): string => { + const headers = ['sample', ...recording.labels].join(';'); + const rows = recording.samples + .map((sample, idx) => idx + ';' + sample.vector.join(';')) + .join('\n'); + return [headers, rows].join('\n'); +}; diff --git a/src/pages/training/TrainingPageModelView.svelte b/src/pages/training/TrainingPageModelView.svelte index 3710482e9..6c839fe16 100644 --- a/src/pages/training/TrainingPageModelView.svelte +++ b/src/pages/training/TrainingPageModelView.svelte @@ -12,7 +12,6 @@ import { stores } from '../../lib/stores/Stores'; import PleaseConnect from '../../components/features/PleaseConnect.svelte'; import FiltersList from '../../components/features/filters/FiltersList.svelte'; - import TrainingPageModelSettings from '../../components/features/training/TrainingPageModelSettings.svelte'; const devices = stores.getDevices(); const selectedModel = stores.getSelectedModel(); @@ -26,7 +25,6 @@ {/if}
-
{#if $selectedModel.id === ModelRegistry.KNN.id} diff --git a/src/pages/validation/ValidationGestureRecordingsCard.svelte b/src/pages/validation/ValidationGestureRecordingsCard.svelte index 4641ea5c4..944425e7b 100644 --- a/src/pages/validation/ValidationGestureRecordingsCard.svelte +++ b/src/pages/validation/ValidationGestureRecordingsCard.svelte @@ -48,6 +48,7 @@ {#key recording.ID} validationSets.removeValidationRecording(recording.ID)} /> From d16895d82529367544ff65894b3c68d669b5a04b Mon Sep 17 00:00:00 2001 From: "A. Malthe Henriksen" Date: Mon, 16 Jun 2025 22:06:57 +0200 Subject: [PATCH 24/49] Remove unused component --- .../training/TrainingPageModelSettings.svelte | 18 ------------------ 1 file changed, 18 deletions(-) delete mode 100644 src/components/features/training/TrainingPageModelSettings.svelte diff --git a/src/components/features/training/TrainingPageModelSettings.svelte b/src/components/features/training/TrainingPageModelSettings.svelte deleted file mode 100644 index 12043d44e..000000000 --- a/src/components/features/training/TrainingPageModelSettings.svelte +++ /dev/null @@ -1,18 +0,0 @@ - - - - -
- {#if $selectedModel.id === ModelRegistry.NeuralNetwork.id}{:else}{/if} -
From 1875e446eb9580105d7de0c6d538482d3d655125 Mon Sep 17 00:00:00 2001 From: "A. Malthe Henriksen" Date: Wed, 25 Jun 2025 21:44:44 +0200 Subject: [PATCH 25/49] Slight updates to the live data buffer --- src/lib/domain/LiveDataBuffer.ts | 40 +++++++++++++++++--------------- 1 file changed, 21 insertions(+), 19 deletions(-) diff --git a/src/lib/domain/LiveDataBuffer.ts b/src/lib/domain/LiveDataBuffer.ts index d19a2773c..1b60bd065 100644 --- a/src/lib/domain/LiveDataBuffer.ts +++ b/src/lib/domain/LiveDataBuffer.ts @@ -9,10 +9,16 @@ export type TimestampedData = { value: T; timestamp: number; }; + +/** + * A buffer for storing live data vectors, which allows for efficient retrieval of the most recent values. + * Implemented as a circular buffer that can hold a fixed number of elements. + */ class LiveDataBuffer { + private buffer: (TimestampedData | null)[]; private bufferPtr = 0; // The buffer pointer keeps increasing from 0 to infinity each time a new item is added - private bufferUtilization = 0; + constructor(private maxLen: number) { this.buffer = new Array | null>(maxLen).fill(null); } @@ -30,23 +36,28 @@ class LiveDataBuffer { }; } - public getNewestValues(noOfValues: number): (T | null)[] { - const values = []; - for (let i = 0; i < noOfValues; i++) { + /** + * Returns the newest n values in the buffer. + */ + public getNewestValues(noOfDataPoints: number): (T | null)[] { + const resultSize = Math.min(noOfDataPoints, this.maxLen); + const values = new Array(resultSize); + + for (let i = 0; i < resultSize; i++) { const item = this.buffer[this.getBufferIndexFrom(this.bufferPtr - (i + 1))]; - if (item) { - values.push(item.value); - } else { - values.push(null); - } + values[i] = item ? item.value : null; } return values; } + /** + * Returns the series of data points that are within the specified time frame. + * The time is specified in milliseconds, and the number of elements to return is also specified. + * If there are not enough elements in the buffer to satisfy the request within the specified timeframe, an error is thrown. + */ public getSeries(time: number, noOfElements: number) { let searchPointer = this.bufferPtr; - this.bufferUtilization = 0; // Search for elements that fit the time frame const series = []; const dateStart = Date.now(); @@ -65,11 +76,6 @@ class LiveDataBuffer { i++; } - this.bufferUtilization = Math.max( - series.length / this.maxLen, - noOfElements / this.maxLen, - ); - // Now the series array is filled with elements within the timeframe. // We should now find `noOfElements` number of items to return if (series.length < noOfElements) { @@ -87,10 +93,6 @@ class LiveDataBuffer { return resultSeries; } - public getBufferUtilization() { - return this.bufferUtilization; - } - private getBufferIndex(): number { return this.getBufferIndexFrom(this.bufferPtr); } From 4c80afceebc5f05374e208875812d3fd6400c2e0 Mon Sep 17 00:00:00 2001 From: "A. Malthe Henriksen" Date: Wed, 25 Jun 2025 21:58:35 +0200 Subject: [PATCH 26/49] Increase the performance of the livedatabuffer --- src/lib/domain/LiveDataBuffer.ts | 46 +++++++++++++++++++------------- 1 file changed, 28 insertions(+), 18 deletions(-) diff --git a/src/lib/domain/LiveDataBuffer.ts b/src/lib/domain/LiveDataBuffer.ts index 1b60bd065..1755db642 100644 --- a/src/lib/domain/LiveDataBuffer.ts +++ b/src/lib/domain/LiveDataBuffer.ts @@ -57,39 +57,49 @@ class LiveDataBuffer { * If there are not enough elements in the buffer to satisfy the request within the specified timeframe, an error is thrown. */ public getSeries(time: number, noOfElements: number) { - let searchPointer = this.bufferPtr; - // Search for elements that fit the time frame - const series = []; - const dateStart = Date.now(); - let i = 0; - while (i < this.maxLen) { - const element = this.buffer[this.getBufferIndexFrom(searchPointer - 1)]; + const now = Date.now(); + let searchPointer = this.bufferPtr - 1; + const resultSeries: TimestampedData[] = []; + + let foundElements = 0; + + // First, find the starting point: the oldest element within the timeframe + while (foundElements < this.maxLen) { + const idx = this.getBufferIndexFrom(searchPointer); + const element = this.buffer[idx]; + if (!element) { throw new Error('Found null element in LiveDataBuffer'); } - const timeElapsed = dateStart - element.timestamp; - if (timeElapsed > time) { + + if (now - element.timestamp > time) { break; } - series.push(element); + + foundElements++; searchPointer--; - i++; } - // Now the series array is filled with elements within the timeframe. - // We should now find `noOfElements` number of items to return - if (series.length < noOfElements) { + if (foundElements < noOfElements) { throw new Error( 'Insufficient buffer data! Try increasing the polling rate or decrease the number of elements requested', ); } - // We will spread out the values evenly and return the result - const resultSeries = []; + // Evenly sample the required number of elements + const step = foundElements / noOfElements; for (let i = 0; i < noOfElements; i++) { - const index = Math.floor(series.length / noOfElements) * i; - resultSeries.push(series[index]); + const offset = Math.floor(i * step); + const idx = this.getBufferIndexFrom(searchPointer + 1 + offset); + const element = this.buffer[idx]; + + if (!element) { + throw new Error('Found null element in LiveDataBuffer during sampling'); + } + + resultSeries.push(element); } + return resultSeries; } From 618aaa9b1ba13e3c9ebc4d335187e4d2abe91676 Mon Sep 17 00:00:00 2001 From: "A. Malthe Henriksen" Date: Wed, 25 Jun 2025 22:01:02 +0200 Subject: [PATCH 27/49] Add documentation of LiveDataBuffer --- src/lib/domain/LiveDataBuffer.ts | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/lib/domain/LiveDataBuffer.ts b/src/lib/domain/LiveDataBuffer.ts index 1755db642..79f5f3ae6 100644 --- a/src/lib/domain/LiveDataBuffer.ts +++ b/src/lib/domain/LiveDataBuffer.ts @@ -23,10 +23,16 @@ class LiveDataBuffer { this.buffer = new Array | null>(maxLen).fill(null); } + /** + * Returns true if no data points have been added to the buffer. + */ public isEmpty(): boolean { return this.bufferPtr === 0; } + /** + * Adds a new value to the buffer. + */ public addValue(value: T) { const bufferIndex = this.getBufferIndex(); this.bufferPtr++; From 211f3414c0a1b038e87e3944e95703717c48c27d Mon Sep 17 00:00:00 2001 From: "A. Malthe Henriksen" Date: Wed, 25 Jun 2025 22:02:32 +0200 Subject: [PATCH 28/49] Prettier --- src/lib/domain/LiveDataBuffer.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/lib/domain/LiveDataBuffer.ts b/src/lib/domain/LiveDataBuffer.ts index 79f5f3ae6..0675436cc 100644 --- a/src/lib/domain/LiveDataBuffer.ts +++ b/src/lib/domain/LiveDataBuffer.ts @@ -15,7 +15,6 @@ export type TimestampedData = { * Implemented as a circular buffer that can hold a fixed number of elements. */ class LiveDataBuffer { - private buffer: (TimestampedData | null)[]; private bufferPtr = 0; // The buffer pointer keeps increasing from 0 to infinity each time a new item is added From bb5d8f4beb957832e9d64e5a148acc6842c214c4 Mon Sep 17 00:00:00 2001 From: "A. Malthe Henriksen" Date: Wed, 25 Jun 2025 22:47:10 +0200 Subject: [PATCH 29/49] Add predicted filtered value to store --- src/lib/domain/stores/Classifier.ts | 10 +++++++++- src/lib/engine/PollingPredictorEngine.ts | 2 -- 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/src/lib/domain/stores/Classifier.ts b/src/lib/domain/stores/Classifier.ts index a8b660033..e15699f96 100644 --- a/src/lib/domain/stores/Classifier.ts +++ b/src/lib/domain/stores/Classifier.ts @@ -7,8 +7,10 @@ import { type Readable, type Subscriber, type Unsubscriber, + type Writable, derived, get, + writable, } from 'svelte/store'; import Filters from '../Filters'; import Model, { type ModelData } from './Model'; @@ -19,15 +21,18 @@ import BaseVector from '../BaseVector'; type ClassifierData = { model: ModelData; + filteredInput: BaseVector; }; class Classifier implements Readable { + private filteredInput: Writable; constructor( private model: Model, private filters: Filters, private gestures: Readable, private confidenceSetter: (gestureId: GestureID, confidence: number) => void, ) { + this.filteredInput = writable(new BaseVector([])); Logger.log('classifier', 'Initialized classifier'); } @@ -35,10 +40,12 @@ class Classifier implements Readable { run: Subscriber, invalidate?: ((value?: ClassifierData | undefined) => void) | undefined, ): Unsubscriber { - return derived([this.model], stores => { + return derived([this.model, this.filteredInput], stores => { const modelStore = stores[0]; + const filteredInputStore = stores[1]; return { model: modelStore, + filteredInput: filteredInputStore, }; }).subscribe(run, invalidate); } @@ -48,6 +55,7 @@ class Classifier implements Readable { */ public async classify(input: ClassifierInput): Promise { const filteredInput = new BaseVector(input.getInput(this.filters)); + this.filteredInput.set(filteredInput); const predictions = await this.getModel().predict(filteredInput); predictions.forEach((confidence, index) => { const gesture = get(this.gestures)[index]; diff --git a/src/lib/engine/PollingPredictorEngine.ts b/src/lib/engine/PollingPredictorEngine.ts index d3dfb7e21..e0f2e2106 100644 --- a/src/lib/engine/PollingPredictorEngine.ts +++ b/src/lib/engine/PollingPredictorEngine.ts @@ -19,8 +19,6 @@ import type { Engine, EngineData } from '../domain/stores/Engine'; import type { LiveData } from '../domain/stores/LiveData'; import type HighlightedAxes from '../domain/stores/HighlightedAxes'; import { ClassifierInput } from '../domain/ClassifierInput'; -import BaseLiveDataVector from '../domain/BaseLiveDataVector'; -import BaseVector from '../domain/BaseVector'; /** * The PollingPredictorEngine will predict on the current input with consistent intervals. From d9378315e896353e289a2af2d1a8ff3a6a93d181 Mon Sep 17 00:00:00 2001 From: "A. Malthe Henriksen" Date: Wed, 25 Jun 2025 23:23:01 +0200 Subject: [PATCH 30/49] Add normalized input calculations --- src/lib/domain/ClassifierInput.ts | 11 +++++++++++ src/lib/domain/Filters.ts | 20 ++++++++++++++++++++ src/lib/domain/stores/Classifier.ts | 18 ++++++++++++++---- 3 files changed, 45 insertions(+), 4 deletions(-) diff --git a/src/lib/domain/ClassifierInput.ts b/src/lib/domain/ClassifierInput.ts index 99a6c9f76..23090354c 100644 --- a/src/lib/domain/ClassifierInput.ts +++ b/src/lib/domain/ClassifierInput.ts @@ -22,6 +22,17 @@ export class ClassifierInput { ).flat(); } + public getNormalizedInput(filters: Filters) { + if (this.samples.length === 0) { + return []; + } + const vectorSize = this.samples[0].getSize(); + + return Array.from({ length: vectorSize }, (_, i) => + filters.computeNormalized(this.samples.map(e => e.getValue()[i])), + ).flat(); + } + public static getInputForAxes(samples: Vector[], axes: Axis[]): ClassifierInput { return new ClassifierInput( samples.map( diff --git a/src/lib/domain/Filters.ts b/src/lib/domain/Filters.ts index b29bd0b3a..23faf2f38 100644 --- a/src/lib/domain/Filters.ts +++ b/src/lib/domain/Filters.ts @@ -13,6 +13,9 @@ import { import FilterTypes, { FilterType } from './FilterTypes'; import Logger from '../utils/Logger'; import type { Filter } from './Filter'; +import FilterGraphLimits from '../utils/FilterLimits'; +import type { Vector } from './Vector'; +import BaseVector from './BaseVector'; class Filters implements Readable { constructor(private filters: Writable) {} @@ -29,6 +32,23 @@ class Filters implements Readable { }); } + public computeNormalized(values: number[]): number[] { + return get(this.filters).map(filter => { + return this.normalizeFilterResult(filter.filter(values), filter); + }); + } + + private normalizeFilterResult(value: number, filter: Filter): number { + const { min, max } = FilterGraphLimits.getFilterLimits(filter); + const newMin = 0; + const newMax = 1; + const existingMin = min; + const existingMax = max; + return ( + ((newMax - newMin) * (value - existingMin)) / (existingMax - existingMin) + newMin + ); + } + public set(filterTypes: FilterType[]) { const newFilters = filterTypes.map(filterType => FilterTypes.createFilter(filterType), diff --git a/src/lib/domain/stores/Classifier.ts b/src/lib/domain/stores/Classifier.ts index e15699f96..310f08ddb 100644 --- a/src/lib/domain/stores/Classifier.ts +++ b/src/lib/domain/stores/Classifier.ts @@ -18,21 +18,28 @@ import Gesture, { type GestureID } from './gesture/Gesture'; import type { ClassifierInput } from '../ClassifierInput'; import Logger from '../../utils/Logger'; import BaseVector from '../BaseVector'; +import type { Vector } from '../Vector'; type ClassifierData = { model: ModelData; - filteredInput: BaseVector; + filteredInput: { + raw: Vector; + normalized: Vector; + }; }; class Classifier implements Readable { - private filteredInput: Writable; + private filteredInput: Writable<{ raw: Vector; normalized: Vector }>; constructor( private model: Model, private filters: Filters, private gestures: Readable, private confidenceSetter: (gestureId: GestureID, confidence: number) => void, ) { - this.filteredInput = writable(new BaseVector([])); + this.filteredInput = writable({ + raw: new BaseVector([]), + normalized: new BaseVector([]), + }); Logger.log('classifier', 'Initialized classifier'); } @@ -55,7 +62,10 @@ class Classifier implements Readable { */ public async classify(input: ClassifierInput): Promise { const filteredInput = new BaseVector(input.getInput(this.filters)); - this.filteredInput.set(filteredInput); + const filteredInputNormalized = new BaseVector( + input.getNormalizedInput(this.filters), + ); + this.filteredInput.set({ raw: filteredInput, normalized: filteredInputNormalized }); const predictions = await this.getModel().predict(filteredInput); predictions.forEach((confidence, index) => { const gesture = get(this.gestures)[index]; From 298f4f2aff6ce05dcb3ab793719ebec028c6f6f6 Mon Sep 17 00:00:00 2001 From: "A. Malthe Henriksen" Date: Wed, 25 Jun 2025 23:56:23 +0200 Subject: [PATCH 31/49] Simplify logger --- src/lib/utils/Logger.ts | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/src/lib/utils/Logger.ts b/src/lib/utils/Logger.ts index 57c098168..6f951213e 100644 --- a/src/lib/utils/Logger.ts +++ b/src/lib/utils/Logger.ts @@ -28,9 +28,7 @@ class Logger { if (!Environment.isInDevelopment) { return; } - if (!(window as typeof window & { hasLogged: boolean }).hasLogged) { - welcomeLog(); - } + welcomeLog(); const outputMessage = `[${origin}] ${message} ${params}`; !get(nsStore) && console.trace(outputMessage); get(nsStore) && console.warn(outputMessage); @@ -43,9 +41,7 @@ class Logger { if (!Environment.isInDevelopment) { return; } - if (!(window as typeof window & { hasLogged: boolean }).hasLogged) { - welcomeLog(); - } + welcomeLog(); const outputMessage = `[${origin}] ${message} ${params}`; !get(nsStore) && console.trace(outputMessage); get(nsStore) && console.log(outputMessage); From ed1253c9d2c9d9cf87fd10e855f4196d2668f337 Mon Sep 17 00:00:00 2001 From: "A. Malthe Henriksen" Date: Wed, 25 Jun 2025 23:59:52 +0200 Subject: [PATCH 32/49] Simplify logger --- src/lib/utils/Logger.ts | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/src/lib/utils/Logger.ts b/src/lib/utils/Logger.ts index 6f951213e..762b28e3a 100644 --- a/src/lib/utils/Logger.ts +++ b/src/lib/utils/Logger.ts @@ -11,7 +11,7 @@ import PersistantWritable from '../repository/PersistantWritable'; const nsStore = new PersistantWritable(false, 'dev_ns'); class Logger { - constructor(private origin: any) {} + constructor(private origin: any) { } public log(message: any, ...params: any[]) { Logger.log(this.origin, message, params); @@ -55,11 +55,17 @@ export const welcomeLog = () => { ) { return; } - console.log(`⚙️ Development Mode : - Welcome to the ML-Machine development mode. - To disable stacktrace in logs, ds() in console. - To Enable again stacktraces using es(). - If you experience any bugs, please report them at https://github.com/microbit-foundation/cctd-ml-machine/issues`); + console.log(`⚙️ Development Mode: +Welcome to the ML-Machine development environment. +You are currently running the application in development mode, which provides enhanced debugging capabilities. + +To disable stack traces in logs for a cleaner console output, use the 'ds()' command in the browser console. +To re-enable stack traces for detailed debugging, use the 'es()' command. + +If you encounter any issues, unexpected behavior, or bugs, please report them to our team by opening an issue at: +https://github.com/microbit-foundation/cctd-ml-machine/issues. + +Thank you for contributing to the improvement of ML-Machine!`); Object.assign(window, { hasLogged: true }); }; From cbf52249d321456bb0229aa062922b0885522524 Mon Sep 17 00:00:00 2001 From: "A. Malthe Henriksen" Date: Thu, 26 Jun 2025 00:00:05 +0200 Subject: [PATCH 33/49] prettier --- src/lib/utils/Logger.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/lib/utils/Logger.ts b/src/lib/utils/Logger.ts index 762b28e3a..278981600 100644 --- a/src/lib/utils/Logger.ts +++ b/src/lib/utils/Logger.ts @@ -11,7 +11,7 @@ import PersistantWritable from '../repository/PersistantWritable'; const nsStore = new PersistantWritable(false, 'dev_ns'); class Logger { - constructor(private origin: any) { } + constructor(private origin: any) {} public log(message: any, ...params: any[]) { Logger.log(this.origin, message, params); From 6e921935acbb0e5a1a8e6b22c361661beba1975b Mon Sep 17 00:00:00 2001 From: "A. Malthe Henriksen" Date: Sat, 28 Jun 2025 00:49:51 +0200 Subject: [PATCH 34/49] Remove unused part of playground --- ...LiveDataBufferUtilizationPercentage.svelte | 21 ------------------- src/pages/PlaygroundPage.svelte | 2 -- 2 files changed, 23 deletions(-) delete mode 100644 src/components/features/playground/LiveDataBufferUtilizationPercentage.svelte diff --git a/src/components/features/playground/LiveDataBufferUtilizationPercentage.svelte b/src/components/features/playground/LiveDataBufferUtilizationPercentage.svelte deleted file mode 100644 index b6013006c..000000000 --- a/src/components/features/playground/LiveDataBufferUtilizationPercentage.svelte +++ /dev/null @@ -1,21 +0,0 @@ - - - - -

Buffer utilization = {utilization * 100}%

-

- Buffer utilization should be about 20-50%. Too high means performance spikes may cause - the buffer to lack sufficient data to classify. Too low means performance is wasted -

diff --git a/src/pages/PlaygroundPage.svelte b/src/pages/PlaygroundPage.svelte index 0052ae77e..7f9c09ceb 100644 --- a/src/pages/PlaygroundPage.svelte +++ b/src/pages/PlaygroundPage.svelte @@ -7,7 +7,6 @@ + +
+
+ +
+
diff --git a/src/components/features/datacollection/Gesture.svelte b/src/components/features/datacollection/Gesture.svelte index db962ca06..5cdb2c98f 100644 --- a/src/components/features/datacollection/Gesture.svelte +++ b/src/components/features/datacollection/Gesture.svelte @@ -14,7 +14,7 @@ MicrobitInteractions, chosenGesture, } from '../../../lib/stores/uiStore'; - import Recording from '../../ui/Recording.svelte'; + import Recording from '../../ui/recording/Recording.svelte'; import { t } from '../../../i18n'; import ImageSkeleton from '../../ui/skeletonloading/ImageSkeleton.svelte'; import GestureCard from '../../ui/Card.svelte'; @@ -219,6 +219,7 @@
{#each $gesture.recordings as recording (String($gesture.ID) + String(recording.ID))} void = TypingUtils.emptyFunction; export let disabled = false; export let small = false; + export let medium = false; export let tiny = false; export let outlined = false; export let fillOnHover = false; @@ -94,7 +99,8 @@ class:font-bold={bold} class:tiny class:small - class:normal={!small && !tiny} + class:medium + class:normal={!small && !tiny && !medium} class:outlined class:cursor-default={disabled} on:click={onClick}> @@ -115,7 +121,8 @@ class:font-bold={bold} class:small class:tiny - class:normal={!small && !tiny} + class:medium + class:normal={!small && !tiny && !medium} class:outlined class:filled={!outlined} class:fillOnHover diff --git a/src/components/ui/recording/Fingerprint.svelte b/src/components/ui/recording/Fingerprint.svelte new file mode 100644 index 000000000..02dd89d8f --- /dev/null +++ b/src/components/ui/recording/Fingerprint.svelte @@ -0,0 +1,54 @@ + + + +
diff --git a/src/components/ui/Recording.svelte b/src/components/ui/recording/Recording.svelte similarity index 56% rename from src/components/ui/Recording.svelte rename to src/components/ui/recording/Recording.svelte index e42654d97..5175bc14a 100644 --- a/src/components/ui/Recording.svelte +++ b/src/components/ui/recording/Recording.svelte @@ -6,13 +6,15 @@ -
+
{#if dotGesture !== undefined} -
+
{/if} {#if hide} -
+
{:else} -
- +
+
+ +
+ {#if enableFingerprint} +
+ +
+ {/if}
{/if} diff --git a/src/components/ui/recording/RecordingFingerprint.svelte b/src/components/ui/recording/RecordingFingerprint.svelte new file mode 100644 index 000000000..a7d0c3d98 --- /dev/null +++ b/src/components/ui/recording/RecordingFingerprint.svelte @@ -0,0 +1,47 @@ + + + +
+
+ +
+
diff --git a/src/pages/validation/ValidationGestureRecordingsCard.svelte b/src/pages/validation/ValidationGestureRecordingsCard.svelte index 944425e7b..d26d834a1 100644 --- a/src/pages/validation/ValidationGestureRecordingsCard.svelte +++ b/src/pages/validation/ValidationGestureRecordingsCard.svelte @@ -7,10 +7,10 @@ -
-
+
+ {#if !!filteredNormalizedInput} -
+ fingerprint={filteredNormalizedInput} /> + {/if}
diff --git a/src/components/ui/Tooltip.svelte b/src/components/ui/Tooltip.svelte index 38fc81bc0..7abd4af6c 100644 --- a/src/components/ui/Tooltip.svelte +++ b/src/components/ui/Tooltip.svelte @@ -29,7 +29,7 @@ {#if isHovered && !!title}
+ class="absolute p-1 rounded-sm bg-white shadow-md border-1 border-solid z-1"> {title}
{/if} diff --git a/src/components/ui/recording/Fingerprint.svelte b/src/components/ui/recording/Fingerprint.svelte index 02dd89d8f..5d0d92047 100644 --- a/src/components/ui/recording/Fingerprint.svelte +++ b/src/components/ui/recording/Fingerprint.svelte @@ -4,51 +4,96 @@ SPDX-License-Identifier: MIT --> -
+
+
+ {#each fingerprint as value, index} +
showTooltip(index, value)} + on:mouseleave={hideTooltip} + on:click|stopPropagation + role="button" + tabindex="0"/> + {/each} +
+
+ + +{#if tooltipVisible} +
+
{tooltipLabel}
+
Value: {tooltipValue.toFixed(3)}
+
+{/if} diff --git a/src/components/ui/recording/Recording.svelte b/src/components/ui/recording/Recording.svelte index 5175bc14a..a3ae87005 100644 --- a/src/components/ui/recording/Recording.svelte +++ b/src/components/ui/recording/Recording.svelte @@ -13,8 +13,9 @@ import type { RecordingData } from '../../../lib/domain/RecordingData'; import Tooltip from './../Tooltip.svelte'; import { serializeRecordingToCsvWithoutGestureName } from '../../../lib/utils/CSVUtils'; - import Fingerprint from './Fingerprint.svelte'; import RecordingFingerprint from './RecordingFingerprint.svelte'; + import { Feature, hasFeature } from '../../../lib/FeatureToggles'; + import { tr } from '../../../i18n'; // get recording from mother prop export let recording: RecordingData; @@ -57,17 +58,19 @@ // Clean up the URL object URL.revokeObjectURL(url); } + + const shouldDisplayFingerprint = enableFingerprint && hasFeature(Feature.FINGERPRINT);
+ class:w-40={!shouldDisplayFingerprint} + class:w-50={shouldDisplayFingerprint}> {#if dotGesture !== undefined}
+ class:right-1={!shouldDisplayFingerprint} + class:right-10={shouldDisplayFingerprint}>
{/if} @@ -75,24 +78,25 @@
+ class:w-40={!shouldDisplayFingerprint} + class:w-50={shouldDisplayFingerprint} /> {:else}
+ class:w-40={!shouldDisplayFingerprint} + class:w-50={shouldDisplayFingerprint}>
- {#if enableFingerprint} -
+ {#if shouldDisplayFingerprint} +
{/if}
{/if} + + - + {#if downloadable} - + {/if} diff --git a/src/components/ui/recording/RecordingFingerprint.svelte b/src/components/ui/recording/RecordingFingerprint.svelte index a7d0c3d98..a805c6c4c 100644 --- a/src/components/ui/recording/RecordingFingerprint.svelte +++ b/src/components/ui/recording/RecordingFingerprint.svelte @@ -40,8 +40,4 @@ const fingerprint: number[] = getFilteredInput().getValue(); -
-
- -
-
+ diff --git a/src/lib/FeatureToggles.ts b/src/lib/FeatureToggles.ts index 40dea7452..fe675fd56 100644 --- a/src/lib/FeatureToggles.ts +++ b/src/lib/FeatureToggles.ts @@ -16,6 +16,7 @@ export enum Feature { RECORDING_SCRUBBER_VALUES = 'recordingScrubberValues', MODEL_VALIDATION = 'modelValidation', MODEL_SETTINGS = 'modelSettings', + FINGERPRINT = 'fingerprint', } export const hasFeature = (feature: Feature): boolean => { From fce65f5545ef597e8520ecbf5bd460d983782432 Mon Sep 17 00:00:00 2001 From: "A. Malthe Henriksen" Date: Tue, 8 Jul 2025 21:59:12 +0200 Subject: [PATCH 37/49] Make tooltip for fingerprint feel better by using polling intervals --- .../ui/recording/Fingerprint.svelte | 27 +++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/src/components/ui/recording/Fingerprint.svelte b/src/components/ui/recording/Fingerprint.svelte index 5d0d92047..8550852f8 100644 --- a/src/components/ui/recording/Fingerprint.svelte +++ b/src/components/ui/recording/Fingerprint.svelte @@ -5,6 +5,7 @@ -->
From bdacaece76dbdb8804e22ecf1c261150556c7a70 Mon Sep 17 00:00:00 2001 From: "A. Malthe Henriksen" Date: Tue, 8 Jul 2025 22:20:47 +0200 Subject: [PATCH 38/49] Make recording fingerprint reactive to highlighted axes and filters --- .../ui/recording/RecordingFingerprint.svelte | 51 ++++++++++++------- 1 file changed, 32 insertions(+), 19 deletions(-) diff --git a/src/components/ui/recording/RecordingFingerprint.svelte b/src/components/ui/recording/RecordingFingerprint.svelte index a805c6c4c..98b315ca5 100644 --- a/src/components/ui/recording/RecordingFingerprint.svelte +++ b/src/components/ui/recording/RecordingFingerprint.svelte @@ -13,31 +13,44 @@ export let recording: RecordingData; export let gestureName: string; const classifier = stores.getClassifier(); - - const filtersLabels: string[] = []; + const highlightedAxes = stores.getHighlightedAxes(); const filters = classifier.getFilters(); - $filters.forEach(filter => { - const filterName = filter.getName(); - filtersLabels.push(`${filterName} - x`, `${filterName} - y`, `${filterName} - z`); - }); - const getFilteredInput = (): Vector => { - const [xs, ys, zs] = recording.samples.reduce( + + $: filtersLabels = (() => { + const labels: string[] = []; + $filters.forEach(filter => { + const filterName = filter.getName(); + $highlightedAxes.forEach(axis => { + labels.push(`${filterName} - ${axis.label}`); + }); + }); + return labels; + })(); + + $: fingerprint = (() => { + const sampleInputVectorIndices = $highlightedAxes.map(axis => axis.index); + const sampleInput = recording.samples.reduce( (pre, cur) => { - pre[0].push(cur.vector[0]); - pre[1].push(cur.vector[1]); - pre[2].push(cur.vector[2]); + sampleInputVectorIndices.forEach(idx => { + if (pre[idx.toString()] === undefined) { + pre[idx.toString()] = [cur.vector[idx]]; + } else { + pre[idx.toString()]!.push(cur.vector[idx]); + } + }); return pre; }, - [[] as number[], [] as number[], [] as number[]], + {} as { [key: string]: number[] | undefined }, ); - return new BaseVector([ - ...filters.computeNormalized(xs), - ...filters.computeNormalized(ys), - ...filters.computeNormalized(zs), - ]); - }; - const fingerprint: number[] = getFilteredInput().getValue(); + const vectorInput: number[] = []; + Object.entries(sampleInput).forEach(([key, val]) => { + if (!val) return; + vectorInput.push(...filters.computeNormalized(val)); + }); + + return new BaseVector(vectorInput).getValue(); + })(); From d3bf3cb2c99985907eca71551c9e7e35a923ab0e Mon Sep 17 00:00:00 2001 From: "A. Malthe Henriksen" Date: Tue, 8 Jul 2025 23:24:16 +0200 Subject: [PATCH 39/49] Fix bug where live data shows wrong labels --- .../archtest/default-build-config.test.ts | 5 --- .../features/bottom/BottomPanel.svelte | 11 ++++-- .../bottom/LiveDataFingerprint.svelte | 3 +- .../ui/recording/Fingerprint.svelte | 34 +++++++++---------- src/components/ui/recording/Recording.svelte | 24 ++++++------- .../ui/recording/RecordingFingerprint.svelte | 1 - 6 files changed, 38 insertions(+), 40 deletions(-) diff --git a/src/__tests__/archtest/default-build-config.test.ts b/src/__tests__/archtest/default-build-config.test.ts index e7538825a..dc15c98d5 100644 --- a/src/__tests__/archtest/default-build-config.test.ts +++ b/src/__tests__/archtest/default-build-config.test.ts @@ -31,9 +31,4 @@ describe('Default build config test', () => { expect(windiContent.includes("secondary: '#2CCAC0'"), message).toBeFalsy(); expect(features.title, message).toBe('Learning tool'); }); - - test("deleteme pls", () => { - // TODO: Remember to add feature flag for fingerprint! - expect(true).toBeFalsy(); - }); }); diff --git a/src/components/features/bottom/BottomPanel.svelte b/src/components/features/bottom/BottomPanel.svelte index 77b64fb0d..c807b6343 100644 --- a/src/components/features/bottom/BottomPanel.svelte +++ b/src/components/features/bottom/BottomPanel.svelte @@ -18,6 +18,7 @@ import StandardButton from '../../ui/buttons/StandardButton.svelte'; import { stores } from '../../../lib/stores/Stores'; import LiveDataFingerprint from './LiveDataFingerprint.svelte'; + import { Feature, hasFeature } from '../../../lib/FeatureToggles'; const devices = stores.getDevices(); @@ -79,16 +80,20 @@ onOutputDisconnectButtonClicked={outputDisconnectButtonClicked} />
+
(isLive3DOpen = true)}>
-
- -
+ {#if hasFeature(Feature.FINGERPRINT)} +
+ +
+ {/if}
+ (isLive3DOpen = false)}>
{ const filterName = filter.getName(); - return [`${filterName} - x`, `${filterName} - y`, `${filterName} - z`]; + return $highlightedAxes.map(axis => `${filterName} - ${axis.label}`); }); // $: fingerprint = $classifier.filteredInput.normalized.getValue(); onMount(() => { diff --git a/src/components/ui/recording/Fingerprint.svelte b/src/components/ui/recording/Fingerprint.svelte index 8550852f8..ef01e18fc 100644 --- a/src/components/ui/recording/Fingerprint.svelte +++ b/src/components/ui/recording/Fingerprint.svelte @@ -4,8 +4,7 @@ SPDX-License-Identifier: MIT -->
+
(isLive3DOpen = true)}> -
- {#if hasFeature(Feature.FINGERPRINT)} -
- -
- {/if} - +
+

Fingerprint:

+ toggleEnabled(e)} + on:click|stopPropagation /> +
+ + {#if isFingerprintEnabled} +
+ +
+ {/if} + +
+
diff --git a/src/components/ui/recording/Recording.svelte b/src/components/ui/recording/Recording.svelte index 6237e0279..70ba25efb 100644 --- a/src/components/ui/recording/Recording.svelte +++ b/src/components/ui/recording/Recording.svelte @@ -59,7 +59,7 @@ URL.revokeObjectURL(url); } - const shouldDisplayFingerprint = enableFingerprint && hasFeature(Feature.FINGERPRINT); + $: shouldDisplayFingerprint = enableFingerprint && hasFeature(Feature.FINGERPRINT);
Recording) const results = stores.getValidationResults(); - const gestures = stores.getGestures(); + const enableFingerprint = stores.getEnableFingerprint(); $: recordings = $gestureValidationSet.recordings; @@ -47,7 +47,7 @@ {#each recordings as recording} {#key recording.ID} Date: Sun, 20 Jul 2025 23:25:05 +0200 Subject: [PATCH 43/49] Move hovering UI on live 3d --- src/components/features/bottom/BottomPanel.svelte | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/components/features/bottom/BottomPanel.svelte b/src/components/features/bottom/BottomPanel.svelte index 188417263..7fc180733 100644 --- a/src/components/features/bottom/BottomPanel.svelte +++ b/src/components/features/bottom/BottomPanel.svelte @@ -88,9 +88,7 @@
-
(isLive3DOpen = true)}> +

Fingerprint:

{/if} -
+
(isLive3DOpen = true)}>
From b6d57643ff5a49155486f0465e890af1fc43fbed Mon Sep 17 00:00:00 2001 From: "A. Malthe Henriksen" Date: Thu, 24 Jul 2025 22:16:38 +0200 Subject: [PATCH 44/49] Fix blinking arrows --- .../knngraph/AxesFilterVectorView.svelte | 9 +++++++- src/lib/domain/stores/Classifier.ts | 22 +++++-------------- 2 files changed, 14 insertions(+), 17 deletions(-) diff --git a/src/components/features/graphs/knngraph/AxesFilterVectorView.svelte b/src/components/features/graphs/knngraph/AxesFilterVectorView.svelte index 65345b6f1..13608bdef 100644 --- a/src/components/features/graphs/knngraph/AxesFilterVectorView.svelte +++ b/src/components/features/graphs/knngraph/AxesFilterVectorView.svelte @@ -87,6 +87,8 @@ }; }); + $: console.log($filters); + unsubscribe = derived([highlightedAxis, classifier], s => s).subscribe(s => { init(); }); @@ -114,7 +116,9 @@
{/each}
+ {#if $highlightedAxis.length === 1} +
{#each $filters as filter, index}

{filter.getName()}

@@ -128,7 +132,10 @@ width="20px" /> {/each}
-
+ + +
{#each liveFilteredAxesData as val, index}

{val.toFixed(2)} diff --git a/src/lib/domain/stores/Classifier.ts b/src/lib/domain/stores/Classifier.ts index 310f08ddb..41802f744 100644 --- a/src/lib/domain/stores/Classifier.ts +++ b/src/lib/domain/stores/Classifier.ts @@ -22,24 +22,15 @@ import type { Vector } from '../Vector'; type ClassifierData = { model: ModelData; - filteredInput: { - raw: Vector; - normalized: Vector; - }; }; class Classifier implements Readable { - private filteredInput: Writable<{ raw: Vector; normalized: Vector }>; constructor( private model: Model, private filters: Filters, private gestures: Readable, private confidenceSetter: (gestureId: GestureID, confidence: number) => void, ) { - this.filteredInput = writable({ - raw: new BaseVector([]), - normalized: new BaseVector([]), - }); Logger.log('classifier', 'Initialized classifier'); } @@ -47,12 +38,10 @@ class Classifier implements Readable { run: Subscriber, invalidate?: ((value?: ClassifierData | undefined) => void) | undefined, ): Unsubscriber { - return derived([this.model, this.filteredInput], stores => { + return derived([this.model], stores => { const modelStore = stores[0]; - const filteredInputStore = stores[1]; return { model: modelStore, - filteredInput: filteredInputStore, }; }).subscribe(run, invalidate); } @@ -62,10 +51,11 @@ class Classifier implements Readable { */ public async classify(input: ClassifierInput): Promise { const filteredInput = new BaseVector(input.getInput(this.filters)); - const filteredInputNormalized = new BaseVector( - input.getNormalizedInput(this.filters), - ); - this.filteredInput.set({ raw: filteredInput, normalized: filteredInputNormalized }); + // Uncommented due to performance issues, caused too many re-renders + // const filteredInputNormalized = new BaseVector( + // input.getNormalizedInput(this.filters), + // ); + // this.filteredInput.set({ raw: filteredInput, normalized: filteredInputNormalized }); const predictions = await this.getModel().predict(filteredInput); predictions.forEach((confidence, index) => { const gesture = get(this.gestures)[index]; From cca112ec45a6942eaac5d781ef5cceadaecf861a Mon Sep 17 00:00:00 2001 From: "A. Malthe Henriksen" Date: Mon, 28 Jul 2025 20:36:28 +0200 Subject: [PATCH 45/49] Implement a switch component --- .../features/bottom/BottomPanel.svelte | 10 ++-- src/components/ui/Switch.svelte | 58 +++++++++++++++++++ .../ValidationPageActionContent.svelte | 5 +- ...alidationpageActionContentMinimized.svelte | 3 +- 4 files changed, 68 insertions(+), 8 deletions(-) create mode 100644 src/components/ui/Switch.svelte diff --git a/src/components/features/bottom/BottomPanel.svelte b/src/components/features/bottom/BottomPanel.svelte index 7fc180733..ac40592b7 100644 --- a/src/components/features/bottom/BottomPanel.svelte +++ b/src/components/features/bottom/BottomPanel.svelte @@ -19,6 +19,7 @@ import { stores } from '../../../lib/stores/Stores'; import LiveDataFingerprint from './LiveDataFingerprint.svelte'; import { Feature, hasFeature } from '../../../lib/FeatureToggles'; + import Switch from '../../ui/Switch.svelte'; const devices = stores.getDevices(); const enableFingerprint = stores.getEnableFingerprint(); @@ -91,11 +92,10 @@

Fingerprint:

- toggleEnabled(e)} - on:click|stopPropagation /> + enableFingerprint.set(e.detail.checked)} />
{#if isFingerprintEnabled} diff --git a/src/components/ui/Switch.svelte b/src/components/ui/Switch.svelte new file mode 100644 index 000000000..00e1d8c6a --- /dev/null +++ b/src/components/ui/Switch.svelte @@ -0,0 +1,58 @@ + + + + +
+ {#if label} + + {/if} + +
diff --git a/src/pages/validation/ValidationPageActionContent.svelte b/src/pages/validation/ValidationPageActionContent.svelte index 5a7c9226b..b969aba18 100644 --- a/src/pages/validation/ValidationPageActionContent.svelte +++ b/src/pages/validation/ValidationPageActionContent.svelte @@ -12,6 +12,7 @@ import { tr } from '../../i18n'; import Tooltip from '../../components/ui/Tooltip.svelte'; import StandardButton from '../../components/ui/buttons/StandardButton.svelte'; + import Switch from '../../components/ui/Switch.svelte'; const classifier = stores.getClassifier(); const model = classifier.getModel(); @@ -29,12 +30,12 @@
-
+

{$tr('content.validation.testButton.autoUpdate')}:

- +
import StandardButton from '../../components/ui/buttons/StandardButton.svelte'; + import Switch from '../../components/ui/Switch.svelte'; import Tooltip from '../../components/ui/Tooltip.svelte'; import { tr } from '../../i18n'; import { stores } from '../../lib/stores/Stores'; @@ -26,7 +27,7 @@

{$tr('content.validation.testButton.autoUpdate')}:

- + Date: Mon, 28 Jul 2025 21:24:34 +0200 Subject: [PATCH 46/49] Fix aria issue --- src/components/ui/Switch.svelte | 5 ----- 1 file changed, 5 deletions(-) diff --git a/src/components/ui/Switch.svelte b/src/components/ui/Switch.svelte index 00e1d8c6a..65642be79 100644 --- a/src/components/ui/Switch.svelte +++ b/src/components/ui/Switch.svelte @@ -8,7 +8,6 @@ import { createEventDispatcher } from 'svelte'; export let checked: boolean = false; export let disabled: boolean = false; - export let label: string = ''; export let size: 'sm' | 'md' | 'lg' = 'md'; const dispatch = createEventDispatcher(); @@ -30,9 +29,6 @@
- {#if label} - - {/if}