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