diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml
index dbc669292..907a1bc3b 100644
--- a/.github/workflows/build.yml
+++ b/.github/workflows/build.yml
@@ -44,12 +44,12 @@ jobs:
- run: node ./bin/print-ci-env.cjs >> $GITHUB_ENV
- run: npm run ci
- name: Run Playwright tests
- if: env.STAGE == 'REVIEW' || env.STAGE == 'STAGING'
+ if: false
uses: docker://mcr.microsoft.com/playwright:v1.45.0-jammy
with:
args: npx playwright test
- name: Store reports
- if: (env.STAGE == 'REVIEW' || env.STAGE == 'STAGING') && failure()
+ if: false
uses: actions/upload-artifact@v4
with:
name: playwright-report
diff --git a/lang/ui.ca.json b/lang/ui.ca.json
index 8ca5503c5..9aceb478a 100644
--- a/lang/ui.ca.json
+++ b/lang/ui.ca.json
@@ -723,7 +723,10 @@
"defaultMessage": "Comencem",
"description": "Get started action"
},
- "go-action": { "defaultMessage": "Ves", "description": "Go action" },
+ "go-action": {
+ "defaultMessage": "Ves",
+ "description": "Go action"
+ },
"graph-color-scheme": {
"defaultMessage": "Esquema de colors del gràfic",
"description": "Graph colour scheme setting label"
@@ -1752,4 +1755,4 @@
"defaultMessage": "desconnecta i torna a connectar el cable USB",
"description": "WebUSB error dialog"
}
-}
+}
\ No newline at end of file
diff --git a/lang/ui.en.json b/lang/ui.en.json
index fa926faf2..9313bd964 100644
--- a/lang/ui.en.json
+++ b/lang/ui.en.json
@@ -723,7 +723,10 @@
"defaultMessage": "Get started",
"description": "Get started action"
},
- "go-action": { "defaultMessage": "Go", "description": "Go action" },
+ "go-action": {
+ "defaultMessage": "Go",
+ "description": "Go action"
+ },
"graph-color-scheme": {
"defaultMessage": "Graph colour scheme",
"description": "Graph colour scheme setting label"
@@ -1125,7 +1128,7 @@
"description": "Aria label for the additional actions menu to the right of the Edit in MakeCode button"
},
"name-action-hint": {
- "defaultMessage": "Name an action you want the micro:bit to recognise",
+ "defaultMessage": "Name an action you want the micro:bit to recognise, e.g. ‘waving’ or ‘jumping’",
"description": "Hint shown when you have an unnamed action"
},
"name-project": {
@@ -1752,4 +1755,4 @@
"defaultMessage": "unplug and replug the USB cable",
"description": "WebUSB error dialog"
}
-}
+}
\ No newline at end of file
diff --git a/lang/ui.es-es.json b/lang/ui.es-es.json
index 86f31e642..9929f8a31 100644
--- a/lang/ui.es-es.json
+++ b/lang/ui.es-es.json
@@ -723,7 +723,10 @@
"defaultMessage": "Empieza",
"description": "Get started action"
},
- "go-action": { "defaultMessage": "Ir", "description": "Go action" },
+ "go-action": {
+ "defaultMessage": "Ir",
+ "description": "Go action"
+ },
"graph-color-scheme": {
"defaultMessage": "Esquema de colores del gráfico",
"description": "Graph colour scheme setting label"
@@ -1752,4 +1755,4 @@
"defaultMessage": "desenchufa y vuelve a enchufar el cable USB",
"description": "WebUSB error dialog"
}
-}
+}
\ No newline at end of file
diff --git a/lang/ui.fr.json b/lang/ui.fr.json
index 4a385ce4c..9d768f6df 100644
--- a/lang/ui.fr.json
+++ b/lang/ui.fr.json
@@ -71,6 +71,10 @@
"defaultMessage": "Modifier les échantillons de données",
"description": "Back button text"
},
+ "blocks-preview-title": {
+ "defaultMessage": "Blocks preview",
+ "description": "Blocks preview heading text"
+ },
"bluetooth-unsupported-advice": {
"defaultMessage": "Nous recommandons Google Chrome ou Microsoft Edge pour que vous puissiez vous connecter directement à votre micro:bit.",
"description": "Note on supported browsers"
@@ -427,6 +431,10 @@
"defaultMessage": "Afficher les fonctionnalités des données",
"description": "Show data features button text"
},
+ "data-preview-title": {
+ "defaultMessage": "Data preview",
+ "description": "Data preview heading text"
+ },
"data-samples-actions-region": {
"defaultMessage": "Barre d'outils d'échantillons de données",
"description": "Region label for data samples actions"
@@ -715,7 +723,10 @@
"defaultMessage": "C'est parti",
"description": "Get started action"
},
- "go-action": { "defaultMessage": "Aller", "description": "Go action" },
+ "go-action": {
+ "defaultMessage": "Aller",
+ "description": "Go action"
+ },
"graph-color-scheme": {
"defaultMessage": "Schéma de couleurs du graphique",
"description": "Graph colour scheme setting label"
@@ -1216,6 +1227,34 @@
"defaultMessage": "Ouvrir le fichier lorsqu’il est déposé",
"description": "Aria label for file drop target"
},
+ "open-project-action": {
+ "defaultMessage": "Open project",
+ "description": "Open project button text"
+ },
+ "open-project-setup-description": {
+ "defaultMessage": "Opening this project will overwrite any existing session. You may want to save your existing session first.",
+ "description": "Open project setup description"
+ },
+ "open-shared-project-blocks-preview-description": {
+ "defaultMessage": "Preview of the MakeCode code contained within this project.",
+ "description": "Blocks preview description text"
+ },
+ "open-shared-project-data-preview-description": {
+ "defaultMessage": "Preview of the actions and data contained within this project.",
+ "description": "Data preview description text"
+ },
+ "open-shared-project-description": {
+ "defaultMessage": "This is a Microsoft MakeCode shared project compatible with CreateAI.",
+ "description": "Import shared CreateAI project description"
+ },
+ "open-shared-project-error-description": {
+ "defaultMessage": "Check the link you followed is correct, and make sure you are connected to the internet before trying again.",
+ "description": "Import shared CreateAI error description"
+ },
+ "open-shared-project-title": {
+ "defaultMessage": "Open shared CreateAI project",
+ "description": "Import shared CreateAI project title"
+ },
"other-tabs-body1": {
"defaultMessage": "Un autre processus est connecté à cet appareil.",
"description": "Connection error dialog"
@@ -1496,6 +1535,10 @@
"defaultMessage": "Test du modèle",
"description": "Testing model page title"
},
+ "third-party-content-description": {
+ "defaultMessage": "This project is provided by a user, and not endorsed by Microsoft or the Micro:bit Educational Foundation. Visit the MakeCode shared project to report abuse to Microsoft MakeCode if you think it's not appropriate.",
+ "description": "Notify user of third party content"
+ },
"tour-action": {
"defaultMessage": "Visite",
"description": "Button label that starts a tour of the app's features"
@@ -1712,4 +1755,4 @@
"defaultMessage": "débrancher et rebrancher le câble USB",
"description": "WebUSB error dialog"
}
-}
+}
\ No newline at end of file
diff --git a/lang/ui.ja.json b/lang/ui.ja.json
index 0c244d9fc..beefe097f 100644
--- a/lang/ui.ja.json
+++ b/lang/ui.ja.json
@@ -723,7 +723,10 @@
"defaultMessage": "はじめよう",
"description": "Get started action"
},
- "go-action": { "defaultMessage": "実行", "description": "Go action" },
+ "go-action": {
+ "defaultMessage": "実行",
+ "description": "Go action"
+ },
"graph-color-scheme": {
"defaultMessage": "グラフの配色設定",
"description": "Graph colour scheme setting label"
@@ -1752,4 +1755,4 @@
"defaultMessage": "USBケーブルを抜いて再接続",
"description": "WebUSB error dialog"
}
-}
+}
\ No newline at end of file
diff --git a/lang/ui.ko.json b/lang/ui.ko.json
index 9fbff517b..7c61c3ad1 100644
--- a/lang/ui.ko.json
+++ b/lang/ui.ko.json
@@ -723,7 +723,10 @@
"defaultMessage": "시작하기",
"description": "Get started action"
},
- "go-action": { "defaultMessage": "이동", "description": "Go action" },
+ "go-action": {
+ "defaultMessage": "이동",
+ "description": "Go action"
+ },
"graph-color-scheme": {
"defaultMessage": "그래프 색 구성표",
"description": "Graph colour scheme setting label"
@@ -1752,4 +1755,4 @@
"defaultMessage": "USB 케이블을 분리했다가 다시 연결하세요.",
"description": "WebUSB error dialog"
}
-}
+}
\ No newline at end of file
diff --git a/lang/ui.lol.json b/lang/ui.lol.json
index cce032947..0b377a4f8 100644
--- a/lang/ui.lol.json
+++ b/lang/ui.lol.json
@@ -1755,4 +1755,4 @@
"defaultMessage": "crwdns363476:0crwdne363476:0",
"description": "WebUSB error dialog"
}
-}
+}
\ No newline at end of file
diff --git a/lang/ui.nl.json b/lang/ui.nl.json
index cde3a6f2c..0381abee9 100644
--- a/lang/ui.nl.json
+++ b/lang/ui.nl.json
@@ -723,7 +723,10 @@
"defaultMessage": "Hoe te beginnen",
"description": "Get started action"
},
- "go-action": { "defaultMessage": "Gaan", "description": "Go action" },
+ "go-action": {
+ "defaultMessage": "Gaan",
+ "description": "Go action"
+ },
"graph-color-scheme": {
"defaultMessage": "Grafiek kleurschema",
"description": "Graph colour scheme setting label"
@@ -1752,4 +1755,4 @@
"defaultMessage": "de USB-kabel ontkoppelen en opnieuw aansluiten",
"description": "WebUSB error dialog"
}
-}
+}
\ No newline at end of file
diff --git a/lang/ui.pl.json b/lang/ui.pl.json
index 5b8ec5216..bf2e3f1f9 100644
--- a/lang/ui.pl.json
+++ b/lang/ui.pl.json
@@ -723,7 +723,10 @@
"defaultMessage": "Rozpocznij",
"description": "Get started action"
},
- "go-action": { "defaultMessage": "Dalej", "description": "Go action" },
+ "go-action": {
+ "defaultMessage": "Dalej",
+ "description": "Go action"
+ },
"graph-color-scheme": {
"defaultMessage": "Schemat kolorów wykresu",
"description": "Graph colour scheme setting label"
@@ -1752,4 +1755,4 @@
"defaultMessage": "odłącz i podłącz kabel USB",
"description": "WebUSB error dialog"
}
-}
+}
\ No newline at end of file
diff --git a/lang/ui.pt-br.json b/lang/ui.pt-br.json
index 76ff5df99..d6cdf001c 100644
--- a/lang/ui.pt-br.json
+++ b/lang/ui.pt-br.json
@@ -723,7 +723,10 @@
"defaultMessage": "Começar",
"description": "Get started action"
},
- "go-action": { "defaultMessage": "Ir", "description": "Go action" },
+ "go-action": {
+ "defaultMessage": "Ir",
+ "description": "Go action"
+ },
"graph-color-scheme": {
"defaultMessage": "Esquema de cores do gráfico",
"description": "Graph colour scheme setting label"
@@ -1752,4 +1755,4 @@
"defaultMessage": "Desconecte e reconecte o cabo USB.",
"description": "WebUSB error dialog"
}
-}
+}
\ No newline at end of file
diff --git a/lang/ui.zh-tw.json b/lang/ui.zh-tw.json
index 0a6b4d146..97eed0597 100644
--- a/lang/ui.zh-tw.json
+++ b/lang/ui.zh-tw.json
@@ -723,7 +723,10 @@
"defaultMessage": "入門指南",
"description": "Get started action"
},
- "go-action": { "defaultMessage": "進行", "description": "Go action" },
+ "go-action": {
+ "defaultMessage": "進行",
+ "description": "Go action"
+ },
"graph-color-scheme": {
"defaultMessage": "圖表顏色方案",
"description": "Graph colour scheme setting label"
@@ -784,7 +787,10 @@
"defaultMessage": "主頁",
"description": "Home button text"
},
- "homepage": { "defaultMessage": "首頁", "description": "Link to home page" },
+ "homepage": {
+ "defaultMessage": "首頁",
+ "description": "Link to home page"
+ },
"homepage-alt": {
"defaultMessage": "顯示代表 micro:bit 加速計數據的 x、y、z 圖線的圖表,並重疊拍手圖示",
"description": "Alt text for image in homepage"
@@ -1085,7 +1091,10 @@
"defaultMessage": "正在載入",
"description": "Aria label for loading spinner"
},
- "main-menu": { "defaultMessage": "主選單", "description": "Main menu label" },
+ "main-menu": {
+ "defaultMessage": "主選單",
+ "description": "Main menu label"
+ },
"makecode-back-alt": {
"defaultMessage": "MakeCode 傳回的左箭頭圖標",
"description": "Testing model page title"
@@ -1746,4 +1755,4 @@
"defaultMessage": "拔下並重新插入 USB 纜線",
"description": "WebUSB error dialog"
}
-}
+}
\ No newline at end of file
diff --git a/src/buffered-data-hooks.tsx b/src/buffered-data-hooks.tsx
index 3f5fa45c9..c36fad360 100644
--- a/src/buffered-data-hooks.tsx
+++ b/src/buffered-data-hooks.tsx
@@ -4,7 +4,10 @@
*
* SPDX-License-Identifier: MIT
*/
-import { AccelerometerDataEvent } from "@microbit/microbit-connection";
+import {
+ AccelerometerData,
+ AccelerometerDataEvent,
+} from "@microbit/microbit-connection";
import {
ReactNode,
createContext,
@@ -12,6 +15,7 @@ import {
useContext,
useEffect,
useRef,
+ useState,
} from "react";
import { BufferedData } from "./buffered-data";
import { useConnectActions } from "./connect-actions-hooks";
@@ -73,3 +77,59 @@ const useBufferedDataInternal = (): BufferedData => {
}, [connection, connectStatus, getBuffer]);
return getBuffer();
};
+
+export const useHasMoved = (): boolean => {
+ const [hasMoved, setHasMoved] = useState(false);
+ const [connectStatus] = useConnectStatus();
+ const connection = useConnectActions();
+ useEffect(() => {
+ if (connectStatus !== ConnectionStatus.Connected) {
+ setHasMoved(false);
+ }
+ let ignore = false;
+ const delta: AccelerometerData = { x: 0, y: 0, z: 0 };
+ let lastSample: AccelerometerData | undefined;
+ const threshold = 40_000;
+ const minDelta = 100;
+ const skipSamples = 10;
+ let skipped = 0;
+ const listener = (e: AccelerometerDataEvent) => {
+ if (skipped < skipSamples) {
+ skipped++;
+ } else if (lastSample) {
+ const deltaX = Math.abs(lastSample.x - e.data.x);
+ if (deltaX > minDelta) {
+ delta.x += deltaX;
+ }
+ const deltaY = Math.abs(lastSample.y - e.data.y);
+ if (deltaY > minDelta) {
+ delta.y += deltaY;
+ }
+ const deltaZ = Math.abs(lastSample.z - e.data.z);
+ if (deltaZ > minDelta) {
+ delta.z += deltaZ;
+ }
+ }
+ lastSample = e.data;
+ if (
+ (delta.x > threshold ? 1 : 0) +
+ (delta.y > threshold ? 1 : 0) +
+ (delta.z > threshold ? 1 : 0) >
+ 1
+ ) {
+ connection.removeAccelerometerListener(listener);
+ if (!ignore) {
+ setHasMoved(true);
+ }
+ }
+ };
+ if (!hasMoved) {
+ connection.addAccelerometerListener(listener);
+ }
+ return () => {
+ ignore = true;
+ connection.removeAccelerometerListener(listener);
+ };
+ }, [connection, connectStatus, hasMoved]);
+ return hasMoved;
+};
diff --git a/src/components/DataSamplesTable.tsx b/src/components/DataSamplesTable.tsx
index 1f6d7ffcf..48cc46315 100644
--- a/src/components/DataSamplesTable.tsx
+++ b/src/components/DataSamplesTable.tsx
@@ -4,42 +4,27 @@
*
* SPDX-License-Identifier: MIT
*/
-import {
- Button,
- Grid,
- GridProps,
- HStack,
- Text,
- VStack,
-} from "@chakra-ui/react";
+import { Grid, GridProps, HStack, Text } from "@chakra-ui/react";
import { ButtonEvent } from "@microbit/microbit-connection";
-import {
- ReactNode,
- useCallback,
- useEffect,
- useMemo,
- useRef,
- useState,
-} from "react";
+import { useCallback, useEffect, useState } from "react";
import { FormattedMessage, useIntl } from "react-intl";
import { useConnectActions } from "../connect-actions-hooks";
import { useConnectionStage } from "../connection-stage-hooks";
-import { ActionData } from "../model";
+import { keyboardShortcuts, useShortcut } from "../keyboard-shortcut-hooks";
+import { ActionData, DataSamplesPageHint } from "../model";
import { useStore } from "../store";
+import { recordButtonId } from "./ActionDataSamplesCard";
+import { actionNameInputId } from "./ActionNameCard";
+import { ConfirmDialog } from "./ConfirmDialog";
import ConnectFirstDialog from "./ConnectFirstDialog";
import DataSamplesMenu from "./DataSamplesMenu";
import DataSamplesTableRow from "./DataSamplesTableRow";
import HeadingGrid, { GridColumnHeadingItemProps } from "./HeadingGrid";
-import LoadProjectInput, { LoadProjectInputRef } from "./LoadProjectInput";
import RecordingDialog, {
RecordingCompleteDetail,
RecordingOptions,
} from "./RecordingDialog";
import ShowGraphsCheckbox from "./ShowGraphsCheckbox";
-import { ConfirmDialog } from "./ConfirmDialog";
-import { actionNameInputId } from "./ActionNameCard";
-import { recordButtonId } from "./ActionDataSamplesCard";
-import { keyboardShortcuts, useShortcut } from "../keyboard-shortcut-hooks";
const gridCommonProps: Partial = {
gridTemplateColumns: "290px 1fr",
@@ -68,22 +53,18 @@ const headings: GridColumnHeadingItemProps[] = [
interface DataSamplesTableProps {
selectedActionIdx: number;
setSelectedActionIdx: (idx: number) => void;
+ hint: DataSamplesPageHint;
}
const DataSamplesTable = ({
selectedActionIdx: selectedActionIdx,
setSelectedActionIdx: setSelectedActionIdx,
+ hint,
}: DataSamplesTableProps) => {
const actions = useStore((s) => s.actions);
// Default to first action being selected if last action is deleted.
const selectedAction: ActionData = actions[selectedActionIdx] ?? actions[0];
- const showHints = useMemo(
- () =>
- actions.length === 0 ||
- (actions.length === 1 && actions[0].recordings.length === 0),
- [actions]
- );
const intl = useIntl();
const isDeleteActionConfirmOpen = useStore((s) => s.isDeleteActionDialogOpen);
const deleteActionConfirmOnOpen = useStore((s) => s.deleteActionDialogOnOpen);
@@ -99,19 +80,13 @@ const DataSamplesTable = ({
const closeDialog = useStore((s) => s.closeDialog);
const connection = useConnectActions();
- const { actions: connActions } = useConnectionStage();
const { isConnected } = useConnectionStage();
- const loadProjectInputRef = useRef(null);
// For adding flashing animation for new recording.
const [newRecordingId, setNewRecordingId] = useState(
undefined
);
- const handleConnect = useCallback(() => {
- connActions.startConnect();
- }, [connActions]);
-
useEffect(() => {
const listener = (e: ButtonEvent) => {
if (!isRecordingDialogOpen && e.state) {
@@ -227,73 +202,30 @@ const DataSamplesTable = ({
{...gridCommonProps}
headings={headings}
/>
- {actions.length === 0 ? (
-
-
-
-
-
- {!isConnected && (
-
- (
-
- ),
- link2: (chunks: ReactNode) => (
-
- ),
- }}
- />
-
- )}
-
- ) : (
-
- {actions.map((action, idx) => (
- setNewRecordingId(undefined)}
- selected={selectedAction.ID === action.ID}
- onSelectRow={() => setSelectedActionIdx(idx)}
- onRecord={handleRecord}
- showHints={showHints}
- onDeleteAction={deleteActionConfirmOnOpen}
- renameShortcutScopeRef={renameActionShortcutScopeRef}
- />
- ))}
-
- )}
+
+ {actions.map((action, idx) => (
+ setNewRecordingId(undefined)}
+ selected={selectedAction.ID === action.ID}
+ onSelectRow={() => setSelectedActionIdx(idx)}
+ onRecord={handleRecord}
+ hint={hint}
+ onDeleteAction={deleteActionConfirmOnOpen}
+ renameShortcutScopeRef={renameActionShortcutScopeRef}
+ />
+ ))}
+
>
);
};
diff --git a/src/components/DataSamplesTableHints.tsx b/src/components/DataSamplesTableHints.tsx
index b95951387..8f41d45c8 100644
--- a/src/components/DataSamplesTableHints.tsx
+++ b/src/components/DataSamplesTableHints.tsx
@@ -3,70 +3,135 @@
*
* SPDX-License-Identifier: MIT
*/
-import { GridItem, HStack, Text, VStack } from "@chakra-ui/react";
+import {
+ AspectRatio,
+ Box,
+ HStack,
+ Image,
+ Stack,
+ Text,
+ VStack,
+} from "@chakra-ui/react";
import { FormattedMessage } from "react-intl";
import { useConnectionStage } from "../connection-stage-hooks";
-import { ActionData } from "../model";
-import ActionDataSamplesCard from "./ActionDataSamplesCard";
-import GreetingEmojiWithArrow from "./GreetingEmojiWithArrow";
-import { RecordingOptions } from "./RecordingDialog";
+import { Action } from "../model";
+import Emoji, { animations } from "./Emoji";
+import EmojiArrow from "./EmojiArrow";
import UpCurveArrow from "./UpCurveArrow";
+import moveMicrobitImage from "../images/move-microbit.svg";
+import microbitOnWrist from "../images/microbit-on-wrist.svg";
-interface DataSamplesTableHintsProps {
- action: ActionData;
- onRecord?: (recordingOptions: RecordingOptions) => void;
-}
+export const NameActionHint = () => {
+ return (
+
+
+
+
+
+
+
+
+
+
+
+ );
+};
-const DataSamplesTableHints = ({
- action,
- onRecord,
-}: DataSamplesTableHintsProps) => {
+export const RecordButtonHint = () => {
const { isConnected } = useConnectionStage();
return (
- <>
- {action.name.length === 0 ? (
-
-
-
-
-
-
-
-
- ) : (
+
+
+ {/* Emoji? Or explain button B visually? */}
+ {isConnected ? (
<>
-
-
-
- {/* Empty grid item to fill first column of grid */}
-
-
-
-
- {isConnected ? (
-
-
-
- ) : (
-
-
-
- )}
-
-
+
+
+
+
>
+ ) : (
+
+
+
)}
- >
+
+ );
+};
+
+export const RecordMoreHint = ({ recorded }: { recorded: number }) => {
+ return (
+
+
+
+
+ {recorded === 1
+ ? "Record at least 2 more data samples"
+ : "Record at least 1 more data sample"}
+
+
+ );
+};
+
+export const AddActionHint = ({ action }: { action: Action }) => {
+ return (
+
+
+
+
+
+
+
+
+ Finished recording for {action.name}?
+ Add another action
+
+
);
};
-export default DataSamplesTableHints;
+export const MoveMicrobitHint = () => {
+ return (
+
+
+ {/* Ratio hides excess whitespace */}
+
+
+
+
+ Try different movements to see how your actions change the graph.
+
+
+ );
+};
diff --git a/src/components/DataSamplesTableRow.tsx b/src/components/DataSamplesTableRow.tsx
index dee9446a5..c51fcdb77 100644
--- a/src/components/DataSamplesTableRow.tsx
+++ b/src/components/DataSamplesTableRow.tsx
@@ -5,13 +5,17 @@
* SPDX-License-Identifier: MIT
*/
import { Box, GridItem } from "@chakra-ui/react";
+import { RefType } from "react-hotkeys-hook/dist/types";
import { useIntl } from "react-intl";
-import { ActionData } from "../model";
+import { ActionData, DataSamplesPageHint } from "../model";
import ActionDataSamplesCard from "./ActionDataSamplesCard";
import ActionNameCard, { ActionCardNameViewMode } from "./ActionNameCard";
-import DataSamplesTableHints from "./DataSamplesTableHints";
+import {
+ NameActionHint,
+ RecordButtonHint,
+ RecordMoreHint,
+} from "./DataSamplesTableHints";
import { RecordingOptions } from "./RecordingDialog";
-import { RefType } from "react-hotkeys-hook/dist/types";
interface DataSamplesTableRowProps {
preview?: boolean;
@@ -19,7 +23,7 @@ interface DataSamplesTableRowProps {
selected: boolean;
onSelectRow?: () => void;
onRecord?: (recordingOptions: RecordingOptions) => void;
- showHints: boolean;
+ hint: DataSamplesPageHint;
newRecordingId?: number;
clearNewRecordingId?: () => void;
onDeleteAction?: () => void;
@@ -32,14 +36,13 @@ const DataSamplesTableRow = ({
onSelectRow,
onRecord,
preview,
- showHints,
+ hint,
newRecordingId,
clearNewRecordingId,
onDeleteAction,
renameShortcutScopeRef,
}: DataSamplesTableRowProps) => {
const intl = useIntl();
-
return (
<>
- {showHints ? (
-
- ) : (
-
- {(action.name.length > 0 || action.recordings.length > 0) && (
-
- )}
+ {hint === "name-action" && (
+
+
)}
+
+ {(action.name.length > 0 || action.recordings.length > 0) && (
+
+ )}
+
+ {(hint === "record" || hint === "record-more") && (
+ <>
+ {/* Skip first column to correctly place hint. */}
+
+
+ {hint === "record" && }
+ {hint === "record-more" && (
+
+ )}
+
+ >
+ )}
>
);
diff --git a/src/components/Emoji.tsx b/src/components/Emoji.tsx
new file mode 100644
index 000000000..ee6f5c07f
--- /dev/null
+++ b/src/components/Emoji.tsx
@@ -0,0 +1,117 @@
+import { Icon, IconProps, keyframes } from "@chakra-ui/react";
+
+export const animations = {
+ wobble: `${keyframes({
+ "0%": {
+ transform: "rotate(15deg)",
+ },
+ "25%": {
+ transform: "rotate(-15deg)",
+ },
+ "50%": {
+ transform: "rotate(15deg)",
+ },
+ "75%": {
+ transform: "rotate(-15deg)",
+ },
+ })} 2s`,
+ tada: `${keyframes({
+ "0%": {
+ transform: "scale(1) rotate(0deg)",
+ },
+ "10%, 20%": {
+ transform: "scale(0.95) rotate(-3deg)",
+ },
+ "30%, 50%, 70%, 90%": {
+ transform: "scale(1.1) rotate(3deg)",
+ },
+ "40%, 60%, 80%": {
+ transform: "scale(1.1) rotate(-3deg)",
+ },
+ "100%": {
+ transform: "scale(1) rotate(0deg)",
+ },
+ })} 1s ease-in-out`,
+ spin: `${keyframes({
+ "0%": {
+ transform: "rotate3d(0, 1, 0, 0deg)",
+ },
+ "100%": {
+ transform: "rotate3d(0, 1, 0, 360deg)",
+ },
+ })} 2s`,
+};
+
+type Eye = "round" | "tick";
+
+type Side = "left" | "right";
+
+interface EmojiProps extends IconProps {
+ leftEye?: Eye;
+ rightEye?: Eye;
+}
+
+const Emoji = ({
+ leftEye = "round",
+ rightEye = "round",
+ boxSize = 16,
+ color = "brand.500",
+ ...props
+}: EmojiProps) => {
+ return (
+
+ {/* Outline */}
+
+
+
+ {/* Smile */}
+
+
+ );
+};
+
+const Eye = ({ type, side }: { type: Eye; side: Side }) => {
+ switch (type) {
+ case "round":
+ return ;
+ case "tick":
+ return ;
+ }
+};
+
+const RoundEye = ({ side }: { side: Side }) => {
+ return (
+
+ );
+};
+
+const TickEye = ({ position }: { position: Side }) => {
+ return (
+
+
+
+
+ );
+};
+
+export default Emoji;
diff --git a/src/components/EmojiArrow.tsx b/src/components/EmojiArrow.tsx
new file mode 100644
index 000000000..eb014af3f
--- /dev/null
+++ b/src/components/EmojiArrow.tsx
@@ -0,0 +1,14 @@
+import { Icon, IconProps } from "@chakra-ui/react";
+
+const EmojiArrow = (props: IconProps) => {
+ return (
+
+
+
+ );
+};
+
+export default EmojiArrow;
diff --git a/src/components/GreetingEmojiWithArrow.tsx b/src/components/GreetingEmojiWithArrow.tsx
deleted file mode 100644
index 71c2921ee..000000000
--- a/src/components/GreetingEmojiWithArrow.tsx
+++ /dev/null
@@ -1,36 +0,0 @@
-/**
- * (c) 2024, Micro:bit Educational Foundation and contributors
- *
- * SPDX-License-Identifier: MIT
- */
-import { Icon } from "@chakra-ui/react";
-
-interface GreetingEmojiWithArrowProps {
- w: string;
- h: string;
- color?: string;
-}
-
-const GreetingEmojiWithArrow = ({
- w,
- h,
- color,
-}: GreetingEmojiWithArrowProps) => {
- return (
-
-
-
-
- );
-};
-
-export default GreetingEmojiWithArrow;
diff --git a/src/components/LiveGraphPanel.tsx b/src/components/LiveGraphPanel.tsx
index 502a1b36f..aee586d09 100644
--- a/src/components/LiveGraphPanel.tsx
+++ b/src/components/LiveGraphPanel.tsx
@@ -29,6 +29,7 @@ import PredictedAction from "./PredictedAction";
interface LiveGraphPanelProps {
showPredictedAction?: boolean;
+ showDisconnectedOverlay?: boolean;
disconnectedTextId: string;
}
@@ -37,6 +38,7 @@ export const predictedActionDisplayWidth = 180;
const LiveGraphPanel = ({
showPredictedAction,
disconnectedTextId,
+ showDisconnectedOverlay = true,
}: LiveGraphPanelProps) => {
const { actions, status, isConnected } = useConnectionStage();
const parentPortalRef = useRef(null);
@@ -87,7 +89,7 @@ const LiveGraphPanel = ({
bgColor="white"
className={tourElClassname.liveGraph}
>
- {isDisconnected && (
+ {isDisconnected && showDisconnectedOverlay && (
, "children"> {
+ onChooseConnect?: () => void;
+}
+
+const WelcomeDialog = ({
+ onClose,
+ onChooseConnect,
+ isOpen,
+ ...rest
+}: WelcomeDialogProps) => {
+ const {
+ actions,
+ status: connStatus,
+ isDialogOpen: isConnectionDialogOpen,
+ } = useConnectionStage();
+ const [isWaiting, setIsWaiting] = useState(false);
+
+ const deleteAllActions = useStore((s) => s.deleteAllActions);
+ const hasActions = useStore((s) => s.actions.length > 0);
+
+ const handleOnClose = useCallback(
+ (connecting: boolean) => {
+ // TODO: hacky way to create the initial action!
+ if (!hasActions && !connecting) {
+ deleteAllActions();
+ }
+ setIsWaiting(false);
+ onClose();
+ },
+ [deleteAllActions, hasActions, onClose]
+ );
+
+ const handleConnect = useCallback(async () => {
+ onClose();
+ onChooseConnect?.();
+ switch (connStatus) {
+ case ConnectionStatus.FailedToConnect:
+ case ConnectionStatus.FailedToReconnectTwice:
+ case ConnectionStatus.FailedToSelectBluetoothDevice:
+ case ConnectionStatus.NotConnected: {
+ // Start connection flow.
+ actions.startConnect({});
+ return handleOnClose(true);
+ }
+ case ConnectionStatus.ConnectionLost:
+ case ConnectionStatus.FailedToReconnect:
+ case ConnectionStatus.Disconnected: {
+ // Reconnect.
+ await actions.reconnect();
+ return handleOnClose(true);
+ }
+ case ConnectionStatus.ReconnectingAutomatically: {
+ // Wait for reconnection to happen.
+ setIsWaiting(true);
+ return;
+ }
+ case ConnectionStatus.Connected: {
+ // Connected whilst dialog is up.
+ return handleOnClose(false);
+ }
+ case ConnectionStatus.ReconnectingExplicitly:
+ case ConnectionStatus.Connecting: {
+ // Impossible cases.
+ return handleOnClose(true);
+ }
+ }
+ }, [onClose, onChooseConnect, connStatus, actions, handleOnClose]);
+
+ useEffect(() => {
+ if (
+ isOpen &&
+ (isConnectionDialogOpen ||
+ (isWaiting && connStatus === ConnectionStatus.Connected))
+ ) {
+ // Close dialog if connection dialog is opened, or
+ // once connected after waiting.
+ handleOnClose(isConnectionDialogOpen);
+ return;
+ }
+ }, [
+ connStatus,
+ handleOnClose,
+ isConnectionDialogOpen,
+ isOpen,
+ isWaiting,
+ onClose,
+ ]);
+
+ return (
+ handleOnClose(false)}
+ isOpen={isOpen}
+ {...rest}
+ >
+
+
+
+ Welcome to micro:bit CreateAI
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ );
+};
+
+export default WelcomeDialog;
diff --git a/src/images/createai-animation.mp4 b/src/images/createai-animation.mp4
new file mode 100644
index 000000000..5b068cdba
Binary files /dev/null and b/src/images/createai-animation.mp4 differ
diff --git a/src/images/microbit-on-wrist.svg b/src/images/microbit-on-wrist.svg
new file mode 100644
index 000000000..f6c0053ea
--- /dev/null
+++ b/src/images/microbit-on-wrist.svg
@@ -0,0 +1,49 @@
+
diff --git a/src/images/move-microbit.svg b/src/images/move-microbit.svg
new file mode 100644
index 000000000..8db1c179b
--- /dev/null
+++ b/src/images/move-microbit.svg
@@ -0,0 +1,190 @@
+
+
diff --git a/src/images/pre-connect-video.mp4 b/src/images/pre-connect-video.mp4
new file mode 100644
index 000000000..8fc97a9d0
Binary files /dev/null and b/src/images/pre-connect-video.mp4 differ
diff --git a/src/messages/ui.en.json b/src/messages/ui.en.json
index 5a77bd78f..24b5295f2 100644
--- a/src/messages/ui.en.json
+++ b/src/messages/ui.en.json
@@ -1924,7 +1924,7 @@
"name-action-hint": [
{
"type": 0,
- "value": "Name an action you want the micro:bit to recognise"
+ "value": "Name an action you want the micro:bit to recognise, e.g. ‘waving’ or ‘jumping’"
}
],
"name-project": [
diff --git a/src/messages/ui.fr.json b/src/messages/ui.fr.json
index 11fc7c28f..78f2037dd 100644
--- a/src/messages/ui.fr.json
+++ b/src/messages/ui.fr.json
@@ -141,6 +141,12 @@
"value": "Modifier les échantillons de données"
}
],
+ "blocks-preview-title": [
+ {
+ "type": 0,
+ "value": "Blocks preview"
+ }
+ ],
"bluetooth-unsupported-advice": [
{
"type": 0,
@@ -719,6 +725,12 @@
"value": "Afficher les fonctionnalités des données"
}
],
+ "data-preview-title": [
+ {
+ "type": 0,
+ "value": "Data preview"
+ }
+ ],
"data-samples-actions-region": [
{
"type": 0,
@@ -1876,7 +1888,7 @@
"makecode-block-show-icon": [
{
"type": 0,
- "value": "afficher l'icône"
+ "value": "montrer l'icône"
}
],
"makecode-load-error-dialog-body": [
@@ -2109,6 +2121,62 @@
"value": "Ouvrir le fichier lorsqu’il est déposé"
}
],
+ "open-project-action": [
+ {
+ "type": 0,
+ "value": "Open project"
+ }
+ ],
+ "open-project-setup-description": [
+ {
+ "type": 0,
+ "value": "Opening this project will overwrite any existing session. You may want to "
+ },
+ {
+ "children": [
+ {
+ "type": 0,
+ "value": "save your existing session"
+ }
+ ],
+ "type": 8,
+ "value": "link"
+ },
+ {
+ "type": 0,
+ "value": " first."
+ }
+ ],
+ "open-shared-project-blocks-preview-description": [
+ {
+ "type": 0,
+ "value": "Preview of the MakeCode code contained within this project."
+ }
+ ],
+ "open-shared-project-data-preview-description": [
+ {
+ "type": 0,
+ "value": "Preview of the actions and data contained within this project."
+ }
+ ],
+ "open-shared-project-description": [
+ {
+ "type": 0,
+ "value": "This is a Microsoft MakeCode shared project compatible with CreateAI."
+ }
+ ],
+ "open-shared-project-error-description": [
+ {
+ "type": 0,
+ "value": "Check the link you followed is correct, and make sure you are connected to the internet before trying again."
+ }
+ ],
+ "open-shared-project-title": [
+ {
+ "type": 0,
+ "value": "Open shared CreateAI project"
+ }
+ ],
"other-tabs-body1": [
{
"type": 0,
@@ -2647,6 +2715,26 @@
"value": "Test du modèle"
}
],
+ "third-party-content-description": [
+ {
+ "type": 0,
+ "value": "This project is provided by a user, and not endorsed by Microsoft or the Micro:bit Educational Foundation. Visit the "
+ },
+ {
+ "children": [
+ {
+ "type": 0,
+ "value": "MakeCode shared project"
+ }
+ ],
+ "type": 8,
+ "value": "link"
+ },
+ {
+ "type": 0,
+ "value": " to report abuse to Microsoft MakeCode if you think it's not appropriate."
+ }
+ ],
"tour-action": [
{
"type": 0,
@@ -2834,7 +2922,7 @@
"tour-trainModel-makeCodeBlocks-content": [
{
"type": 0,
- "value": "Ces blocs MakeCode afficheront des icônes pour chaque action détectée lorsque vous transférez votre code et votre modèle sur un micro:bit."
+ "value": "Ces blocs MakeCode montreront des icônes pour chaque action détectée lorsque vous transférez votre code et votre modèle sur un micro:bit."
}
],
"tour-trainModel-makeCodeBlocks-title": [
diff --git a/src/model.ts b/src/model.ts
index 3e05c4828..01c3f2d3a 100644
--- a/src/model.ts
+++ b/src/model.ts
@@ -223,6 +223,14 @@ export enum DataSamplesView {
GraphAndDataFeatures = "graph and data features",
}
+export type DataSamplesPageHint =
+ | null
+ | "move-microbit"
+ | "name-action"
+ | "record"
+ | "record-more"
+ | "add-action";
+
export enum PostImportDialogState {
None = "none",
Error = "error",
diff --git a/src/pages/DataSamplesPage.tsx b/src/pages/DataSamplesPage.tsx
index 584aa3221..7272c8c61 100644
--- a/src/pages/DataSamplesPage.tsx
+++ b/src/pages/DataSamplesPage.tsx
@@ -4,23 +4,31 @@
*
* SPDX-License-Identifier: MIT
*/
-import { Button, Flex, HStack, VStack } from "@chakra-ui/react";
+import { Button, Flex, HStack, useDisclosure, VStack } from "@chakra-ui/react";
import { useCallback, useEffect, useRef, useState } from "react";
import { RiAddLine, RiArrowRightLine } from "react-icons/ri";
import { FormattedMessage, useIntl } from "react-intl";
import { useNavigate } from "react-router";
+import { useHasMoved } from "../buffered-data-hooks";
import DataSamplesTable from "../components/DataSamplesTable";
+import {
+ AddActionHint,
+ MoveMicrobitHint,
+} from "../components/DataSamplesTableHints";
import DefaultPageLayout, {
ProjectMenuItems,
ProjectToolbarItems,
} from "../components/DefaultPageLayout";
import LiveGraphPanel from "../components/LiveGraphPanel";
import TrainModelDialogs from "../components/TrainModelFlowDialogs";
+import WelcomeDialog from "../components/WelcomeDialog";
import { useConnectionStage } from "../connection-stage-hooks";
import { keyboardShortcuts, useShortcut } from "../keyboard-shortcut-hooks";
+import { ActionData, DataSamplesPageHint } from "../model";
import { useHasSufficientDataForTraining, useStore } from "../store";
import { tourElClassname } from "../tours";
import { createTestingModelPageUrl } from "../urls";
+import { animations } from "../components/Emoji";
const DataSamplesPage = () => {
const actions = useStore((s) => s.actions);
@@ -32,7 +40,8 @@ const DataSamplesPage = () => {
const trainModelFlowStart = useStore((s) => s.trainModelFlowStart);
const tourStart = useStore((s) => s.tourStart);
- const { isConnected } = useConnectionStage();
+ const { isConnected, isDialogOpen: isConnectionDialogOpen } =
+ useConnectionStage();
useEffect(() => {
// If a user first connects on "Testing model" this can result in the tour when they return to the "Data samples" page.
if (isConnected) {
@@ -56,8 +65,29 @@ const DataSamplesPage = () => {
enabled: !isAddNewActionDisabled,
});
const intl = useIntl();
+ const welcomeDialogDisclosure = useDisclosure({
+ defaultIsOpen: !isConnected,
+ });
+ const hasMoved = useHasMoved();
+ const tourInProgress = useStore((s) => !!s.tourState);
+ const isRecordingDialogOpen = useStore((s) => !!s.isRecordingDialogOpen);
+ const isDialogOpen =
+ welcomeDialogDisclosure.isOpen ||
+ isConnectionDialogOpen ||
+ tourInProgress ||
+ isRecordingDialogOpen;
+ const hint: DataSamplesPageHint = isDialogOpen
+ ? null
+ : activeHintForActions(actions, isConnected, hasMoved);
+
return (
<>
+ {welcomeDialogDisclosure.isOpen && (
+
+ )}
{
menuItems={}
toolbarItemsRight={}
>
-
+
@@ -85,6 +123,7 @@ const DataSamplesPage = () => {
borderTopWidth={3}
borderColor="gray.200"
alignItems="center"
+ position="relative"
>
)}
+ {hint === "move-microbit" && }
+ {hint === "add-action" && }
-
+
+
>
);
};
+const activeHintForActions = (
+ actions: ActionData[],
+ isConnected: boolean,
+ hasMoved: boolean
+): DataSamplesPageHint => {
+ if (isConnected && !hasMoved) {
+ return "move-microbit";
+ }
+
+ // We don't let you have zero. If you have > 1 you've seen it all before.
+ if (actions.length !== 1) {
+ return null;
+ }
+ const action = actions[0];
+ if (action.name.length === 0) {
+ if (action.recordings.length === 0) {
+ return "name-action";
+ } else {
+ // No space for hint with actions already recorded.
+ return null;
+ }
+ }
+
+ if (action.recordings.length === 0) {
+ return "record";
+ }
+ if (action.recordings.length < 3) {
+ return "record-more";
+ }
+ return "add-action";
+};
+
export default DataSamplesPage;
diff --git a/src/pages/OpenSharedProjectPage.tsx b/src/pages/OpenSharedProjectPage.tsx
index 33195128c..eadcd6a1c 100644
--- a/src/pages/OpenSharedProjectPage.tsx
+++ b/src/pages/OpenSharedProjectPage.tsx
@@ -296,7 +296,7 @@ const PreviewData = ({ dataset }: PreviewDataProps) => {
key={action.ID}
action={action}
selected={false}
- showHints={false}
+ hint={null}
/>
))}
diff --git a/src/store.ts b/src/store.ts
index f8a120af3..cbc0786cf 100644
--- a/src/store.ts
+++ b/src/store.ts
@@ -298,7 +298,7 @@ const createMlStore = (logging: Logging) => {
persist(
(set, get) => ({
timestamp: undefined,
- actions: [],
+ actions: [createFirstAction()],
dataWindow: currentDataWindow,
isRecording: false,
project: createUntitledProject(),
@@ -389,7 +389,7 @@ const createMlStore = (logging: Logging) => {
const untitledProject = createUntitledProject();
set(
{
- actions: [],
+ actions: [createFirstAction()],
dataWindow: currentDataWindow,
model: undefined,
project: projectName