@@ -101,17 +90,4 @@ const Results = ({
);
};
-export default connect(
- (state: RootState) => ({
- historicResults: state.historicResults,
- showResultsDetails: state.showResultsDetails,
- }),
- (dispatch: Dispatch) => ({
- setResultsPhase(phase: number) {
- dispatch(setResultsPhase(phase));
- },
- setShowResultsDetails(show: boolean) {
- dispatch(setShowResultsDetails(show));
- },
- }),
-)(Results);
+export default Results;
diff --git a/frontend/packages/labs/ailab/src/UIComponents/ResultsDetails.tsx b/frontend/packages/labs/ailab/src/UIComponents/ResultsDetails.tsx
index 834ed1249e82c..9a64c331c6738 100644
--- a/frontend/packages/labs/ailab/src/UIComponents/ResultsDetails.tsx
+++ b/frontend/packages/labs/ailab/src/UIComponents/ResultsDetails.tsx
@@ -2,8 +2,6 @@
import {faTimes} from '@fortawesome/free-solid-svg-icons';
import {FontAwesomeIcon} from '@fortawesome/react-fontawesome';
import {useCallback} from 'react';
-import {connect} from 'react-redux';
-import type {Dispatch} from 'redux';
import {ResultsGrades, styles} from '../constants';
import {
@@ -11,31 +9,22 @@ import {
getCorrectResults,
getIncorrectResults,
} from '../helpers/accuracy';
-import type {RootState} from '../redux';
+import {useAppDispatch, useAppSelector} from '../hooks';
import {setShowResultsDetails} from '../redux';
-import type {ResultsData} from '../types';
import ResultsTable from './ResultsTable';
import ResultsToggle from './ResultsToggle';
-interface ResultsDetailsProps {
- resultsTab: string;
- percentCorrect: string;
- setShowResultsDetails: (show: boolean) => void;
- correctResults: ResultsData;
- incorrectResults: ResultsData;
-}
+const ResultsDetails = () => {
+ const dispatch = useAppDispatch();
+ const resultsTab = useAppSelector(state => state.resultsTab);
+ const percentCorrect = useAppSelector(getPercentCorrect);
+ const correctResults = useAppSelector(getCorrectResults);
+ const incorrectResults = useAppSelector(getIncorrectResults);
-const ResultsDetails = ({
- resultsTab,
- percentCorrect,
- setShowResultsDetails,
- correctResults,
- incorrectResults,
-}: ResultsDetailsProps) => {
const onClose = useCallback(() => {
- setShowResultsDetails(false);
- }, [setShowResultsDetails]);
+ dispatch(setShowResultsDetails(false));
+ }, [dispatch]);
const results =
resultsTab === ResultsGrades.CORRECT ? correctResults : incorrectResults;
@@ -59,16 +48,4 @@ const ResultsDetails = ({
);
};
-export default connect(
- (state: RootState) => ({
- resultsTab: state.resultsTab,
- percentCorrect: getPercentCorrect(state),
- correctResults: getCorrectResults(state),
- incorrectResults: getIncorrectResults(state),
- }),
- (dispatch: Dispatch) => ({
- setShowResultsDetails(show: boolean) {
- dispatch(setShowResultsDetails(show));
- },
- }),
-)(ResultsDetails);
+export default ResultsDetails;
diff --git a/frontend/packages/labs/ailab/src/UIComponents/ResultsTable.tsx b/frontend/packages/labs/ailab/src/UIComponents/ResultsTable.tsx
index 7145bef524bc1..e13d9eedbf84b 100644
--- a/frontend/packages/labs/ailab/src/UIComponents/ResultsTable.tsx
+++ b/frontend/packages/labs/ailab/src/UIComponents/ResultsTable.tsx
@@ -1,35 +1,31 @@
/* React component to handle displaying test data and A.I. Bot's guesses. */
import {useCallback} from 'react';
-import {connect} from 'react-redux';
-import type {Dispatch} from 'redux';
import {styles, colors, REGRESSION_ERROR_TOLERANCE} from '../constants';
import {getLocalizedColumnName, isRegression} from '../helpers/columnDetails';
import {getLocalizedValue} from '../helpers/valueDetails';
+import {useAppDispatch, useAppSelector} from '../hooks';
import I18n from '../i18n';
-import type {RootState} from '../redux';
import {setResultsHighlightRow} from '../redux';
import type {ResultsData} from '../types';
interface ResultsTableProps {
- selectedFeatures: string[];
- labelColumn: string;
results: ResultsData;
- isRegression: boolean;
- setResultsHighlightRow: (row: number | undefined) => void;
- resultsHighlightRow: number | undefined;
- datasetId: string;
}
-const ResultsTable = ({
- selectedFeatures,
- labelColumn,
- results,
- isRegression: isRegressionMode,
- setResultsHighlightRow,
- resultsHighlightRow,
- datasetId,
-}: ResultsTableProps) => {
+const ResultsTable = ({results}: ResultsTableProps) => {
+ const dispatch = useAppDispatch();
+ const selectedFeatures = useAppSelector(state => state.selectedFeatures);
+ const labelColumn = useAppSelector(state => state.labelColumn || 'unknown');
+ const isRegressionMode = useAppSelector(isRegression);
+ const resultsHighlightRow = useAppSelector(
+ state => state.resultsHighlightRow,
+ );
+ const datasetId = useAppSelector(state => state.metadata?.name || 'unknown');
+
+ const highlightRow = (row: number | undefined) =>
+ dispatch(setResultsHighlightRow(row as number));
+
const getRowCellStyle = useCallback(
(index: number) => {
return {
@@ -122,8 +118,8 @@ const ResultsTable = ({
return (
setResultsHighlightRow(index)}
- onMouseLeave={() => setResultsHighlightRow(undefined)}
+ onMouseEnter={() => highlightRow(index)}
+ onMouseLeave={() => highlightRow(undefined)}
>
{examples.map((example, i) => {
return (
@@ -151,17 +147,4 @@ const ResultsTable = ({
);
};
-export default connect(
- (state: RootState) => ({
- selectedFeatures: state.selectedFeatures,
- labelColumn: state.labelColumn || 'unknown',
- isRegression: isRegression(state),
- resultsHighlightRow: state.resultsHighlightRow,
- datasetId: state.metadata?.name || 'unknown',
- }),
- (dispatch: Dispatch) => ({
- setResultsHighlightRow(column: number | undefined) {
- dispatch(setResultsHighlightRow(column as number));
- },
- }),
-)(ResultsTable);
+export default ResultsTable;
diff --git a/frontend/packages/labs/ailab/src/UIComponents/ResultsToggle.tsx b/frontend/packages/labs/ailab/src/UIComponents/ResultsToggle.tsx
index 65f6f667bd89e..2201e164234e8 100644
--- a/frontend/packages/labs/ailab/src/UIComponents/ResultsToggle.tsx
+++ b/frontend/packages/labs/ailab/src/UIComponents/ResultsToggle.tsx
@@ -1,19 +1,16 @@
/* React component to handle toggling between correct/incorrect test results */
import {faTimes, faCheck} from '@fortawesome/free-solid-svg-icons';
import {FontAwesomeIcon} from '@fortawesome/react-fontawesome';
-import {connect} from 'react-redux';
import {ResultsGrades, styles} from '../constants';
+import {useAppDispatch, useAppSelector} from '../hooks';
import I18n from '../i18n';
-import type {RootState} from '../redux';
import {setResultsTab} from '../redux';
-interface ResultsToggleProps {
- resultsTab?: string;
- setResultsTab?: (key: string) => void;
-}
+const ResultsToggle = () => {
+ const resultsTab = useAppSelector(state => state.resultsTab);
+ const dispatch = useAppDispatch();
-const ResultsToggle = ({resultsTab, setResultsTab}: ResultsToggleProps) => {
const getTogglePillStyle = (key: string) => {
let style;
if (key === resultsTab) {
@@ -46,8 +43,8 @@ const ResultsToggle = ({resultsTab, setResultsTab}: ResultsToggleProps) => {
setResultsTab!(tab.key)}
- onKeyDown={() => setResultsTab!(tab.key)}
+ onClick={() => dispatch(setResultsTab(tab.key))}
+ onKeyDown={() => dispatch(setResultsTab(tab.key))}
role="button"
tabIndex={0}
>
@@ -60,13 +57,4 @@ const ResultsToggle = ({resultsTab, setResultsTab}: ResultsToggleProps) => {
);
};
-export default connect(
- (state: RootState) => ({
- resultsTab: state.resultsTab,
- }),
- dispatch => ({
- setResultsTab(key: string) {
- dispatch(setResultsTab(key));
- },
- }),
-)(ResultsToggle);
+export default ResultsToggle;
diff --git a/frontend/packages/labs/ailab/src/UIComponents/SaveModel.tsx b/frontend/packages/labs/ailab/src/UIComponents/SaveModel.tsx
index 0d6ac3e44f5ed..7983c53885428 100644
--- a/frontend/packages/labs/ailab/src/UIComponents/SaveModel.tsx
+++ b/frontend/packages/labs/ailab/src/UIComponents/SaveModel.tsx
@@ -1,8 +1,6 @@
/* React component to handle saving a trained model. */
import type React from 'react';
import {useState} from 'react';
-import {connect} from 'react-redux';
-import type {Dispatch} from 'redux';
import {styles, ModelNameMaxLength} from '../constants';
import {getLocalizedColumnName} from '../helpers/columnDetails';
@@ -10,35 +8,24 @@ import {
getDatasetDescription,
isUserUploadedDataset,
} from '../helpers/datasetDetails';
+import {useAppDispatch, useAppSelector} from '../hooks';
import I18n from '../i18n';
-import type {RootState} from '../redux';
import {setTrainedModelDetail} from '../redux';
import {getSelectedColumnsDescriptions} from '../selectors';
import ScrollableContent from './ScrollableContent';
import Statement from './Statement';
-interface SaveModelProps {
- setTrainedModelDetail: (
- field: string,
- value: string,
- isColumn: boolean,
- ) => void;
- labelColumn: string | undefined;
- columnDescriptions: {id: string; description: string | null}[];
- dataDescription: string | undefined;
- isUserUploadedDataset: boolean;
- datasetId: string | undefined;
-}
+const SaveModel = () => {
+ const dispatch = useAppDispatch();
+ const labelColumn = useAppSelector(state => state.labelColumn);
+ const columnDescriptions = useAppSelector(getSelectedColumnsDescriptions);
+ const dataDescription = useAppSelector(getDatasetDescription);
+ const isUserUploaded = useAppSelector(isUserUploadedDataset);
+ const datasetId = useAppSelector(
+ state => state.metadata && state.metadata.name,
+ );
-const SaveModel = ({
- setTrainedModelDetail,
- labelColumn,
- columnDescriptions,
- dataDescription,
- isUserUploadedDataset: isUserUploaded,
- datasetId,
-}: SaveModelProps) => {
const [showColumnDescriptions, setShowColumnDescriptions] =
useState(isUserUploaded);
@@ -51,7 +38,7 @@ const SaveModel = ({
field: string,
isColumn: boolean,
) => {
- setTrainedModelDetail(field, event.target.value, isColumn);
+ dispatch(setTrainedModelDetail(field, event.target.value, isColumn));
};
const getColumnFields = () => {
@@ -250,17 +237,4 @@ const SaveModel = ({
);
};
-export default connect(
- (state: RootState) => ({
- labelColumn: state.labelColumn,
- columnDescriptions: getSelectedColumnsDescriptions(state),
- dataDescription: getDatasetDescription(state),
- isUserUploadedDataset: isUserUploadedDataset(state),
- datasetId: state.metadata && state.metadata.name,
- }),
- (dispatch: Dispatch) => ({
- setTrainedModelDetail(field: string, value: string, isColumn: boolean) {
- dispatch(setTrainedModelDetail(field, value, isColumn));
- },
- }),
-)(SaveModel);
+export default SaveModel;
diff --git a/frontend/packages/labs/ailab/src/UIComponents/ScatterPlot.tsx b/frontend/packages/labs/ailab/src/UIComponents/ScatterPlot.tsx
index 4d79605799608..881b7ef505e91 100644
--- a/frontend/packages/labs/ailab/src/UIComponents/ScatterPlot.tsx
+++ b/frontend/packages/labs/ailab/src/UIComponents/ScatterPlot.tsx
@@ -1,9 +1,8 @@
import {Scatter} from 'react-chartjs-2';
-import {connect} from 'react-redux';
import {styles, colors} from '../constants';
+import {useAppSelector} from '../hooks';
import I18n from '../i18n';
-import type {RootState} from '../redux';
import {getScatterPlotData} from '../selectors/visualizationSelectors';
const scatterDataBase = {
@@ -55,15 +54,9 @@ const chartOptionsBase = {
maintainAspectRatio: false,
};
-interface ScatterPlotProps {
- scatterPlotData: {
- coordinates: {x: number; y: number}[];
- feature: string;
- label: string;
- } | null;
-}
+const ScatterPlot = () => {
+ const scatterPlotData = useAppSelector(getScatterPlotData);
-const ScatterPlot = ({scatterPlotData}: ScatterPlotProps) => {
const scatterDataCombined = {
...scatterDataBase,
};
@@ -93,9 +86,4 @@ const ScatterPlot = ({scatterPlotData}: ScatterPlotProps) => {
);
};
-export default connect(
- (state: RootState) => ({
- scatterPlotData: getScatterPlotData(state),
- }),
- {},
-)(ScatterPlot);
+export default ScatterPlot;
diff --git a/frontend/packages/labs/ailab/src/UIComponents/SelectDataset.tsx b/frontend/packages/labs/ailab/src/UIComponents/SelectDataset.tsx
index 37db22db5da7c..2381838740dbb 100644
--- a/frontend/packages/labs/ailab/src/UIComponents/SelectDataset.tsx
+++ b/frontend/packages/labs/ailab/src/UIComponents/SelectDataset.tsx
@@ -1,50 +1,40 @@
/* React component to handle importing CSVs and pushing data to Redux store. */
import {useState} from 'react';
-import {connect} from 'react-redux';
-import type {Dispatch} from 'redux';
import {styles} from '../constants';
import {parseCSV, MIN_CSV_ROWS, MIN_CSV_COLUMNS} from '../csvReaderWrapper';
import {getDatasets, getAvailableDatasets} from '../datasetManifest';
+import {useAppDispatch, useAppSelector} from '../hooks';
import I18n from '../i18n';
import {parseJSON} from '../jsonReaderWrapper';
-import type {RootState} from '../redux';
import {
- setSelectedName,
- setSelectedCSV,
- setSelectedJSON,
- resetState,
+ setSelectedName as setSelectedNameAction,
+ setSelectedCSV as setSelectedCSVAction,
+ setSelectedJSON as setSelectedJSONAction,
+ resetState as resetStateAction,
getSpecifiedDatasets,
- setHighlightDataset,
+ setHighlightDataset as setHighlightDatasetAction,
} from '../redux';
import ScrollableContent from './ScrollableContent';
-interface SelectDatasetProps {
- setSelectedName: (name: string) => void;
- setSelectedCSV: (csvfilePath: string | File) => void;
- setSelectedJSON: (jsonfilePath: string) => void;
- setHighlightDataset: (id: string | undefined) => void;
- csvfile: string | object | undefined;
- jsonfile: string | object | undefined;
- resetState: () => void;
- specifiedDatasets: string[] | undefined;
- name: string | undefined;
- highlightDataset: string | undefined;
- invalidData: string | undefined;
-}
-
-const SelectDataset = ({
- setSelectedName,
- setSelectedCSV,
- setSelectedJSON,
- setHighlightDataset,
- resetState,
- specifiedDatasets,
- name,
- highlightDataset,
- invalidData,
-}: SelectDatasetProps) => {
+const SelectDataset = () => {
+ const dispatch = useAppDispatch();
+ const specifiedDatasets = useAppSelector(getSpecifiedDatasets);
+ const name = useAppSelector(state => state.name);
+ const highlightDataset = useAppSelector(state => state.highlightDataset);
+ const invalidData = useAppSelector(state => state.invalidData);
+
+ const resetState = () => dispatch(resetStateAction());
+ const setSelectedName = (datasetName: string) =>
+ dispatch(setSelectedNameAction(datasetName));
+ const setSelectedCSV = (csvfilePath: string | File) =>
+ dispatch(setSelectedCSVAction(csvfilePath as string));
+ const setSelectedJSON = (jsonfilePath: string) =>
+ dispatch(setSelectedJSONAction(jsonfilePath));
+ const setHighlightDataset = (id: string | undefined) =>
+ dispatch(setHighlightDatasetAction(id as string));
+
const [, setDownload] = useState(false);
const handleDatasetClick = (id: string) => {
@@ -161,30 +151,4 @@ const SelectDataset = ({
);
};
-export default connect(
- (state: RootState) => ({
- csvfile: state.csvfile,
- jsonfile: state.jsonfile,
- specifiedDatasets: getSpecifiedDatasets(state),
- name: state.name,
- highlightDataset: state.highlightDataset,
- invalidData: state.invalidData,
- }),
- (dispatch: Dispatch) => ({
- resetState() {
- dispatch(resetState());
- },
- setSelectedName(name: string) {
- dispatch(setSelectedName(name));
- },
- setSelectedCSV(csvfilePath: string | File) {
- dispatch(setSelectedCSV(csvfilePath as string));
- },
- setSelectedJSON(jsonfilePath: string) {
- dispatch(setSelectedJSON(jsonfilePath));
- },
- setHighlightDataset(id: string | undefined) {
- dispatch(setHighlightDataset(id as string));
- },
- }),
-)(SelectDataset);
+export default SelectDataset;
diff --git a/frontend/packages/labs/ailab/src/UIComponents/SelectLabelButton.tsx b/frontend/packages/labs/ailab/src/UIComponents/SelectLabelButton.tsx
index 3862b6b07f7f8..00e53b9bcd4d2 100644
--- a/frontend/packages/labs/ailab/src/UIComponents/SelectLabelButton.tsx
+++ b/frontend/packages/labs/ailab/src/UIComponents/SelectLabelButton.tsx
@@ -1,21 +1,18 @@
/* React component to handle selecting a column as the label. */
-import {connect} from 'react-redux';
-
import {styles} from '../constants';
+import {useAppDispatch} from '../hooks';
import I18n from '../i18n';
import {setLabelColumn} from '../redux';
interface SelectLabelButtonProps {
column?: string;
- setLabelColumn: (column: string) => void;
}
-const SelectLabelButton = ({
- column,
- setLabelColumn,
-}: SelectLabelButtonProps) => {
+const SelectLabelButton = ({column}: SelectLabelButtonProps) => {
+ const dispatch = useAppDispatch();
+
const setPredictColumn = (event: React.MouseEvent, column: string) => {
- setLabelColumn(column);
+ dispatch(setLabelColumn(column));
event.preventDefault();
};
@@ -31,8 +28,4 @@ const SelectLabelButton = ({
);
};
-export default connect(null, dispatch => ({
- setLabelColumn(column: string) {
- dispatch(setLabelColumn(column));
- },
-}))(SelectLabelButton);
+export default SelectLabelButton;
diff --git a/frontend/packages/labs/ailab/src/UIComponents/Statement.tsx b/frontend/packages/labs/ailab/src/UIComponents/Statement.tsx
index e66383da250a4..0d2410f6e47f9 100644
--- a/frontend/packages/labs/ailab/src/UIComponents/Statement.tsx
+++ b/frontend/packages/labs/ailab/src/UIComponents/Statement.tsx
@@ -1,12 +1,11 @@
/* React component to display a statement about our model. */
import {faTimesCircle} from '@fortawesome/free-solid-svg-icons';
import {FontAwesomeIcon} from '@fortawesome/react-fontawesome';
-import {connect} from 'react-redux';
import {styles} from '../constants';
import {getLocalizedColumnName} from '../helpers/columnDetails';
+import {useAppDispatch, useAppSelector} from '../hooks';
import I18n from '../i18n';
-import type {RootState} from '../redux';
import {setLabelColumn, removeSelectedFeature} from '../redux';
interface StatementProps {
@@ -153,22 +152,32 @@ const Statement = ({
);
};
+// The presentational component, for callers that supply props directly (e.g.
+// the historic-results list in Results).
export const UnconnectedStatement = Statement;
-export default connect(
- (state: RootState) => ({
- shouldShow: state.data.length !== 0,
- currentPanel: state.currentPanel,
- labelColumn: state.labelColumn,
- selectedFeatures: state.selectedFeatures,
- datasetId: state.metadata && state.metadata.name,
- }),
- dispatch => ({
- setLabelColumn(labelColumn: string | null) {
- dispatch(setLabelColumn(labelColumn as string));
- },
- removeSelectedFeature(labelColumn: string) {
- dispatch(removeSelectedFeature(labelColumn));
- },
- }),
-)(Statement);
+// Store-connected container: the default export used in the live panels.
+const ConnectedStatement = () => {
+ const dispatch = useAppDispatch();
+ const shouldShow = useAppSelector(state => state.data.length !== 0);
+ const currentPanel = useAppSelector(state => state.currentPanel);
+ const labelColumn = useAppSelector(state => state.labelColumn);
+ const selectedFeatures = useAppSelector(state => state.selectedFeatures);
+ const datasetId = useAppSelector(
+ state => state.metadata && state.metadata.name,
+ );
+
+ return (
+ dispatch(setLabelColumn(column as string))}
+ removeSelectedFeature={id => dispatch(removeSelectedFeature(id))}
+ />
+ );
+};
+
+export default ConnectedStatement;
diff --git a/frontend/packages/labs/ailab/src/UIComponents/TrainModel.tsx b/frontend/packages/labs/ailab/src/UIComponents/TrainModel.tsx
index e7e7a0e33b7da..a023cf6254d69 100644
--- a/frontend/packages/labs/ailab/src/UIComponents/TrainModel.tsx
+++ b/frontend/packages/labs/ailab/src/UIComponents/TrainModel.tsx
@@ -1,15 +1,13 @@
/* React component to handle training. */
import {useState, useEffect, useRef} from 'react';
-import {connect} from 'react-redux';
import {imageUrl} from '../assetPath';
import {styles, getFadeOpacity} from '../constants';
+import {useAppSelector} from '../hooks';
import I18n from '../i18n';
-import type {RootState} from '../redux';
import {getTableData} from '../redux';
import {store} from '../store';
import train from '../train';
-import type {DataRow} from '../types';
import {TrainingAnimationDescription} from './AnimationDescriptions';
import DataTable from './DataTable';
@@ -17,12 +15,11 @@ import DataTable from './DataTable';
const framesPerCycle = 80;
const maxNumItems = 7;
-interface TrainModelProps {
- data: DataRow[];
- instructionsOverlayActive: boolean;
-}
-
-const TrainModel = ({data, instructionsOverlayActive}: TrainModelProps) => {
+const TrainModel = () => {
+ const data = useAppSelector(state => getTableData(state, false));
+ const instructionsOverlayActive = useAppSelector(
+ state => state.instructionsOverlayActive,
+ );
const [frame, setFrame] = useState(0);
const [headOpen, setHeadOpen] = useState(false);
const [, setFinished] = useState(false);
@@ -183,7 +180,4 @@ const TrainModel = ({data, instructionsOverlayActive}: TrainModelProps) => {
);
};
-export default connect((state: RootState) => ({
- data: getTableData(state, false),
- instructionsOverlayActive: state.instructionsOverlayActive,
-}))(TrainModel);
+export default TrainModel;
diff --git a/frontend/packages/labs/ailab/src/UIComponents/UniqueOptionsWarning.tsx b/frontend/packages/labs/ailab/src/UIComponents/UniqueOptionsWarning.tsx
index 250270c7f8f71..0455461cc2083 100644
--- a/frontend/packages/labs/ailab/src/UIComponents/UniqueOptionsWarning.tsx
+++ b/frontend/packages/labs/ailab/src/UIComponents/UniqueOptionsWarning.tsx
@@ -1,16 +1,12 @@
/* React component to handle showing warning for excessive unique options. */
-import {connect} from 'react-redux';
-
import {styles, UNIQUE_OPTIONS_MAX} from '../constants';
+import {useAppSelector} from '../hooks';
import I18n from '../i18n';
-import type {RootState} from '../redux';
import {hasTooManyUniqueOptions} from '../selectors/currentColumnSelectors';
-interface UniqueOptionsWarningProps {
- showWarning?: boolean;
-}
+const UniqueOptionsWarning = () => {
+ const showWarning = useAppSelector(hasTooManyUniqueOptions);
-const UniqueOptionsWarning = ({showWarning}: UniqueOptionsWarningProps) => {
if (!showWarning) {
return null;
}
@@ -27,6 +23,4 @@ const UniqueOptionsWarning = ({showWarning}: UniqueOptionsWarningProps) => {
);
};
-export default connect((state: RootState) => ({
- showWarning: hasTooManyUniqueOptions(state),
-}))(UniqueOptionsWarning);
+export default UniqueOptionsWarning;
diff --git a/frontend/packages/labs/ailab/src/helpers/datasetDetails.ts b/frontend/packages/labs/ailab/src/helpers/datasetDetails.ts
index a440191804290..4d3c715d82f41 100644
--- a/frontend/packages/labs/ailab/src/helpers/datasetDetails.ts
+++ b/frontend/packages/labs/ailab/src/helpers/datasetDetails.ts
@@ -4,6 +4,10 @@ import type {DatasetDetails} from '../types';
/* Helper functions for getting information about the selected dataset. */
+// Reads the I18n global, so it is intentionally NOT memoized on Redux state
+// (a state-keyed cache would go stale across a locale change). Callers that
+// select it should pass `shallowEqual` to useSelector — the result is a flat
+// object, so that dedupes the reference and avoids unnecessary rerenders.
export function getDatasetDetails(state: RootState): DatasetDetails {
const datasetDetails: DatasetDetails = {
name: state.metadata?.name || '',
diff --git a/frontend/packages/labs/ailab/src/hooks.ts b/frontend/packages/labs/ailab/src/hooks.ts
new file mode 100644
index 0000000000000..5b263c9860d94
--- /dev/null
+++ b/frontend/packages/labs/ailab/src/hooks.ts
@@ -0,0 +1,48 @@
+import {
+ type TypedUseSelectorHook,
+ shallowEqual,
+ useDispatch,
+ useSelector,
+} from 'react-redux';
+
+import type {RootState} from './redux';
+
+// Typed react-redux hooks for use throughout the package, in place of connect().
+export const useAppSelector: TypedUseSelectorHook = useSelector;
+export const useAppDispatch = useDispatch;
+
+// Re-exported for selectors that build a fresh object/array each call (e.g.
+// derived shapes that also read non-Redux globals like I18n and so can't be
+// memoized on state): pass it to useAppSelector to dedupe by value.
+export {shallowEqual};
+
+// Structural equality for useAppSelector, for selectors whose result is nested
+// (arrays of objects, or objects with array fields) and so isn't deduped by
+// shallowEqual. Like shallowEqual it lets a selector that rebuilds an equal
+// value each call avoid spurious rerenders without being memoized on state.
+export function deepEqual(a: unknown, b: unknown): boolean {
+ if (a === b) {
+ return true;
+ }
+ if (
+ typeof a !== 'object' ||
+ typeof b !== 'object' ||
+ a === null ||
+ b === null
+ ) {
+ return false;
+ }
+ const keysA = Object.keys(a);
+ const keysB = Object.keys(b);
+ if (keysA.length !== keysB.length) {
+ return false;
+ }
+ return keysA.every(
+ key =>
+ Object.prototype.hasOwnProperty.call(b, key) &&
+ deepEqual(
+ (a as Record)[key],
+ (b as Record)[key],
+ ),
+ );
+}
diff --git a/frontend/packages/labs/ailab/src/index.tsx b/frontend/packages/labs/ailab/src/index.tsx
index f60a2bfd0db31..896ae657aafcb 100644
--- a/frontend/packages/labs/ailab/src/index.tsx
+++ b/frontend/packages/labs/ailab/src/index.tsx
@@ -18,6 +18,7 @@ import {
setReserveLocation,
setInstructionsDismissed,
setFirehoseMetricsLogger,
+ getTrainedModelDataToSave,
} from './redux';
import {store} from './store';
import type {Mode, ModelDataToSave, SaveResponse} from './types';
@@ -36,7 +37,7 @@ let saveTrainedModel:
| undefined = null;
let onContinue: (() => void) | null | undefined = null;
-interface InitAllOptions {
+export interface InitAllOptions {
i18n?: Record;
mode?: Mode;
onContinue?: () => void;
@@ -123,7 +124,11 @@ const processMode = (mode: Mode | undefined): void => {
};
// Do the asynchronous save of a model.
-const startSaveTrainedModel = (dataToSave: ModelDataToSave): void => {
+// TODO: In the RTK migration, fold this into a thunk action so the payload is
+// composed from getState() in the thunk body and App can simply dispatch it,
+// rather than the store being reached into here.
+const startSaveTrainedModel = (): void => {
+ const dataToSave = getTrainedModelDataToSave(store.getState());
store.dispatch(setSaveStatus('started'));
saveTrainedModel!(dataToSave, (response: SaveResponse) => {
store.dispatch(setSaveStatus(response.status, response.data));
@@ -135,3 +140,6 @@ const startSaveTrainedModel = (dataToSave: ModelDataToSave): void => {
}
});
};
+
+// Export a few types.
+export {type SaveResponse, type ModelDataToSave} from './types';
diff --git a/frontend/packages/labs/ailab/src/redux.ts b/frontend/packages/labs/ailab/src/redux.ts
index 27f0156651942..b739b9d7d036a 100644
--- a/frontend/packages/labs/ailab/src/redux.ts
+++ b/frontend/packages/labs/ailab/src/redux.ts
@@ -820,6 +820,9 @@ export function getPredictAvailable(state: RootState): boolean {
);
}
+// Builds {prev, next} and reads the I18n global for button text, so it is not
+// memoized on Redux state. App selects it with a prev/next-aware equality fn so
+// the new object reference doesn't trigger unnecessary rerenders.
export function getPanelButtons(state: RootState): PrevNextButtons {
return prevNextButtons(state);
}