diff --git a/index.html b/index.html
index ede552fbb..4d21ebbaf 100644
--- a/index.html
+++ b/index.html
@@ -16,11 +16,11 @@
diff --git a/lang/ui.en.json b/lang/ui.en.json
index ddf02b5a2..3cbd0be48 100644
--- a/lang/ui.en.json
+++ b/lang/ui.en.json
@@ -679,13 +679,9 @@
"defaultMessage": "Live graph",
"description": ""
},
- "footer.resume": {
- "defaultMessage": "Resume session",
- "description": ""
- },
- "footer.start": {
- "defaultMessage": "Start new session",
- "description": ""
+ "get-started-action": {
+ "defaultMessage": "Get started",
+ "description": "Get started action"
},
"help-label": {
"defaultMessage": "Help",
@@ -699,9 +695,33 @@
"defaultMessage": "Home",
"description": "Home button text"
},
+ "homepage-description": {
+ "defaultMessage": "Train a machine learning model on your own movement data and run it on your micro:bit.",
+ "description": "Home page description"
+ },
+ "homepage-how-it-works": {
+ "defaultMessage": "How it works",
+ "description": "Home page section heading"
+ },
+ "homepage-projects": {
+ "defaultMessage": "Projects",
+ "description": "Home page section heading"
+ },
+ "homepage-step-by-step": {
+ "defaultMessage": "Step by step",
+ "description": "Home page section heading"
+ },
"homepage-subtitle": {
- "defaultMessage": "Introduce students to machine learning concepts through physical movement and data",
- "description": "Subtitle of the home page"
+ "defaultMessage": "Create AI on your BBC micro:bit using movement and machine learning.",
+ "description": "Home page subtitle"
+ },
+ "homepage-video-alt": {
+ "defaultMessage": "Introductory video",
+ "description": "Home page video alt text"
+ },
+ "homepage-video-prompt": {
+ "defaultMessage": "Watch the video to learn how to use {appNameFull}.",
+ "description": "Prompt to watch the video on the home page"
},
"homepage.Link": {
"defaultMessage": "Home page",
@@ -743,6 +763,82 @@
"defaultMessage": "More edit in MakeCode options",
"description": "Aria label for the additional actions menu to the right of the Edit in MakeCode button"
},
+ "newpage-browse-lessons": {
+ "defaultMessage": "Browse lessons",
+ "description": "Browse lessons button text"
+ },
+ "newpage-browse-projects": {
+ "defaultMessage": "Browse projects",
+ "description": "Browse projects button text"
+ },
+ "newpage-choose-subtitle": {
+ "defaultMessage": "Find a project or lesson from our teacher resources and open it in micro:bit classroom",
+ "description": "Choose a project or lesson subtitle"
+ },
+ "newpage-choose-title": {
+ "defaultMessage": "Choose a project or lesson",
+ "description": "Choose a project or lesson title"
+ },
+ "newpage-continue-session-instruction": {
+ "defaultMessage": "To continue a saved session from a file please select a file.",
+ "description": "Instruction to users to continue a saved session from a file"
+ },
+ "newpage-continue-session-subtitle": {
+ "defaultMessage": "Use a hex file or data samples file you saved to your computer to continue a session.",
+ "description": "Continue session subtitle"
+ },
+ "newpage-continue-session-title": {
+ "defaultMessage": "Continue a saved session",
+ "description": "Continue session title"
+ },
+ "newpage-heading-subtitle": {
+ "defaultMessage": "Run whole class sessions, easily share code with students and save progress",
+ "description": "Homepage heading subtitle"
+ },
+ "newpage-heading-title": {
+ "defaultMessage": "Welcome to micro:bit classroom",
+ "description": "Homepage heading title"
+ },
+ "newpage-last-session-date": {
+ "defaultMessage": "Date: {date}",
+ "description": "Last session date label"
+ },
+ "newpage-last-session-name": {
+ "defaultMessage": "Name: {name}",
+ "description": "Last session name label"
+ },
+ "newpage-last-session-none": {
+ "defaultMessage": "No session found",
+ "description": "Last session not found text"
+ },
+ "newpage-last-session-title": {
+ "defaultMessage": "Open last session",
+ "description": "Open last session title"
+ },
+ "newpage-new-session-subtitle": {
+ "defaultMessage": "Connect your micro:bit and collect movement data to build a machine learning model",
+ "description": "Start new session subtitle"
+ },
+ "newpage-new-session-title": {
+ "defaultMessage": "New session",
+ "description": "Start new session title"
+ },
+ "newpage-section-one-title": {
+ "defaultMessage": "Pick up where you left off",
+ "description": "Homepage section one title"
+ },
+ "newpage-section-two-title": {
+ "defaultMessage": "Start something new",
+ "description": "Homepage section two title"
+ },
+ "newpage-subtitle": {
+ "defaultMessage": "Introduce students to machine learning concepts through physical movement and data",
+ "description": "Subtitle of micro:bit AI creator home page"
+ },
+ "newpage-title": {
+ "defaultMessage": "New session",
+ "description": "New page title"
+ },
"no-data-samples": {
"defaultMessage": "No data samples",
"description": "Empty data samples page status text"
@@ -867,6 +963,26 @@
"defaultMessage": "Start training",
"description": "Start training button text"
},
+ "steps-code": {
+ "defaultMessage": "Code",
+ "description": "Step in home page diagram"
+ },
+ "steps-collect-data": {
+ "defaultMessage": "Collect data",
+ "description": "Step in home page diagram"
+ },
+ "steps-improve": {
+ "defaultMessage": "Improve",
+ "description": "Step in home page diagram"
+ },
+ "steps-test-model": {
+ "defaultMessage": "Test model",
+ "description": "Step in home page diagram"
+ },
+ "steps-train": {
+ "defaultMessage": "Train",
+ "description": "Step in home page diagram"
+ },
"support-request": {
"defaultMessage": "Please consider raising a support request.",
"description": "Support request link text"
@@ -975,4 +1091,4 @@
"defaultMessage": "unknown",
"description": "Label for unknown ML event"
}
-}
\ No newline at end of file
+}
diff --git a/src/App.tsx b/src/App.tsx
index 5349d84d7..3560be472 100644
--- a/src/App.tsx
+++ b/src/App.tsx
@@ -21,9 +21,14 @@ import { deployment, useDeployment } from "./deployment";
import { ProjectProvider } from "./hooks/project-hooks";
import { LoggingProvider } from "./logging/logging-hooks";
import TranslationProvider from "./messages/TranslationProvider";
-import HomePage from "./pages/HomePage";
-import { createHomePageUrl, createSessionPageUrl } from "./urls";
import { sessionPageConfigs } from "./pages-config";
+import HomePage from "./pages/HomePage";
+import NewPage from "./pages/NewPage";
+import {
+ createHomePageUrl,
+ createNewPageUrl,
+ createSessionPageUrl,
+} from "./urls";
export interface ProviderLayoutProps {
children: ReactNode;
@@ -90,6 +95,10 @@ const createRouter = () => {
path: createHomePageUrl(),
element: ,
},
+ {
+ path: createNewPageUrl(),
+ element: ,
+ },
...sessionPageConfigs.map((config) => {
return {
path: createSessionPageUrl(config.id),
diff --git a/src/components/DefaultPageLayout.tsx b/src/components/DefaultPageLayout.tsx
index ff0d067f6..d4c07b45b 100644
--- a/src/components/DefaultPageLayout.tsx
+++ b/src/components/DefaultPageLayout.tsx
@@ -11,15 +11,16 @@ import {
VStack,
} from "@chakra-ui/react";
import { ReactNode, useCallback, useEffect } from "react";
-import { RiDownload2Line, RiFolderOpenLine, RiHome2Line } from "react-icons/ri";
+import { RiDownload2Line, RiHome2Line } from "react-icons/ri";
import { FormattedMessage, useIntl } from "react-intl";
import { useNavigate } from "react-router";
+import { useDeployment } from "../deployment";
import { flags } from "../flags";
import { useProject } from "../hooks/project-hooks";
-import { SaveStep, TrainModelDialogStage } from "../model";
+import { TrainModelDialogStage } from "../model";
import { SessionPageId } from "../pages-config";
import Tour from "../pages/Tour";
-import { useSettings, useStore } from "../store";
+import { useStore } from "../store";
import { createHomePageUrl, createSessionPageUrl } from "../urls";
import ActionBar from "./ActionBar";
import AppLogo from "./AppLogo";
@@ -27,41 +28,33 @@ import ConnectionDialogs from "./ConnectionFlowDialogs";
import DownloadDialogs from "./DownloadDialogs";
import HelpMenu from "./HelpMenu";
import LanguageMenuItem from "./LanguageMenuItem";
-import LoadProjectMenuItem from "./LoadProjectMenuItem";
-import OpenButton from "./OpenButton";
import PreReleaseNotice from "./PreReleaseNotice";
import SaveDialogs from "./SaveDialogs";
import SettingsMenu from "./SettingsMenu";
import ToolbarMenu from "./ToolbarMenu";
import TrainModelDialogs from "./TrainModelFlowDialogs";
-import { useDeployment } from "../deployment";
interface DefaultPageLayoutProps {
titleId?: string;
children: ReactNode;
toolbarItemsLeft?: ReactNode;
+ toolbarItemsRight?: ReactNode;
+ menuItems?: ReactNode;
showPageTitle?: boolean;
- showHomeButton?: boolean;
- showSaveButton?: boolean;
- showOpenButton?: boolean;
}
const DefaultPageLayout = ({
titleId,
children,
+ menuItems,
toolbarItemsLeft,
+ toolbarItemsRight,
showPageTitle = false,
- showHomeButton = false,
- showSaveButton = false,
- showOpenButton = false,
}: DefaultPageLayoutProps) => {
const intl = useIntl();
const navigate = useNavigate();
const isEditorOpen = useStore((s) => s.isEditorOpen);
const stage = useStore((s) => s.trainModelDialogStage);
-
- const { saveHex } = useProject();
- const [settings] = useSettings();
const toast = useToast();
const { appNameFull } = useDeployment();
@@ -91,19 +84,6 @@ const DefaultPageLayout = ({
);
}, [intl, navigate, toast]);
- const handleHomeClick = useCallback(() => {
- navigate(createHomePageUrl());
- }, [navigate]);
-
- const setSave = useStore((s) => s.setSave);
- const handleSave = useCallback(() => {
- if (settings.showPreSaveHelp) {
- setSave({ step: SaveStep.PreSaveHelp });
- } else {
- void saveHex();
- }
- }, [saveHex, setSave, settings.showPreSaveHelp]);
-
return (
<>
{/* Suppress dialogs to prevent overlapping dialogs */}
@@ -138,26 +118,7 @@ const DefaultPageLayout = ({
itemsRight={
<>
- {showOpenButton && }
- {showSaveButton && (
- }
- onClick={handleSave}
- >
-
-
- )}
- {showHomeButton && (
- }
- aria-label={intl.formatMessage({ id: "homepage.Link" })}
- variant="plain"
- size="lg"
- fontSize="xl"
- />
- )}
+ {toolbarItemsRight}
@@ -166,31 +127,7 @@ const DefaultPageLayout = ({
variant="plain"
label={intl.formatMessage({ id: "main-menu" })}
>
- {showOpenButton && (
- }
- accept=".hex"
- >
-
-
- )}
- {showSaveButton && (
- }
- >
-
-
- )}
-
- {showHomeButton && (
- }
- >
-
-
- )}
+ {menuItems}
>
@@ -205,4 +142,77 @@ const DefaultPageLayout = ({
);
};
+export const ProjectToolbarItems = () => {
+ const { saveHex } = useProject();
+ const handleSave = useCallback(() => {
+ void saveHex();
+ }, [saveHex]);
+
+ return (
+ <>
+ }
+ onClick={handleSave}
+ >
+
+
+
+ >
+ );
+};
+
+export const HomeToolbarItem = () => {
+ const intl = useIntl();
+ const navigate = useNavigate();
+ const handleHomeClick = useCallback(() => {
+ navigate(createHomePageUrl());
+ }, [navigate]);
+ return (
+ }
+ aria-label={intl.formatMessage({ id: "homepage.Link" })}
+ variant="plain"
+ size="lg"
+ fontSize="xl"
+ />
+ );
+};
+
+export const ProjectMenuItems = () => {
+ const { saveHex } = useProject();
+ const handleSave = useCallback(() => {
+ void saveHex();
+ }, [saveHex]);
+
+ return (
+ <>
+ }
+ >
+
+
+
+
+ >
+ );
+};
+
+export const HomeMenuItem = () => {
+ const navigate = useNavigate();
+ const handleHomeClick = useCallback(() => {
+ navigate(createHomePageUrl());
+ }, [navigate]);
+ return (
+ }
+ >
+
+
+ );
+};
+
export default DefaultPageLayout;
diff --git a/src/components/LoadProjectInput.tsx b/src/components/LoadProjectInput.tsx
index 1cd9e190b..fc78b1f17 100644
--- a/src/components/LoadProjectInput.tsx
+++ b/src/components/LoadProjectInput.tsx
@@ -8,7 +8,7 @@ export interface LoadProjectInputProps {
* File input tag accept attribute.
* A project can be opened from .json or .hex file.
*/
- accept?: ".json" | ".hex";
+ accept: ".json" | ".hex" | ".json,.hex";
}
export interface LoadProjectInputRef {
diff --git a/src/components/LoadProjectMenuItem.tsx b/src/components/LoadProjectMenuItem.tsx
index a26756780..a33f0c07d 100644
--- a/src/components/LoadProjectMenuItem.tsx
+++ b/src/components/LoadProjectMenuItem.tsx
@@ -13,7 +13,7 @@ interface LoadProjectMenuItemProps
* File input tag accept attribute.
* A project can be opened from .json or .hex file.
*/
- accept?: ".json" | ".hex";
+ accept: ".json" | ".hex" | ".json,.hex";
}
const LoadProjectMenuItem = ({
diff --git a/src/components/NewPageChoice.tsx b/src/components/NewPageChoice.tsx
new file mode 100644
index 000000000..5d1ac20af
--- /dev/null
+++ b/src/components/NewPageChoice.tsx
@@ -0,0 +1,77 @@
+import {
+ Box,
+ BoxProps,
+ Heading,
+ HStack,
+ IconButton,
+ Stack,
+} from "@chakra-ui/react";
+import { ReactElement, ReactNode } from "react";
+
+interface GetStartedChoiceProps extends BoxProps {
+ children: ReactNode;
+ onClick: () => void;
+ icon: ReactElement;
+ disabled?: boolean;
+ label: string;
+}
+
+const NewPageChoice = ({
+ disabled,
+ children,
+ onClick,
+ icon,
+ label,
+ ...props
+}: GetStartedChoiceProps) => {
+ return (
+
+
+
+ {label}
+
+ {children}
+
+
+
+
+
+ );
+};
+
+export default NewPageChoice;
diff --git a/src/components/OpenButton.tsx b/src/components/OpenButton.tsx
index 5f7db6aad..c11c74f3c 100644
--- a/src/components/OpenButton.tsx
+++ b/src/components/OpenButton.tsx
@@ -15,7 +15,7 @@ const OpenButton = () => {
>
-
+
>
);
};
diff --git a/src/components/PercentageDisplay.tsx b/src/components/PercentageDisplay.tsx
index 67e5c1a67..5ce826756 100644
--- a/src/components/PercentageDisplay.tsx
+++ b/src/components/PercentageDisplay.tsx
@@ -1,6 +1,6 @@
-import { StackProps, Text } from "@chakra-ui/react";
+import { BoxProps, Text } from "@chakra-ui/react";
-interface PercentageDisplayProps extends StackProps {
+interface PercentageDisplayProps extends BoxProps {
value: number;
colorScheme?: string;
}
@@ -8,6 +8,7 @@ interface PercentageDisplayProps extends StackProps {
const PercentageDisplay = ({
value,
colorScheme = "gray.600",
+ ...rest
}: PercentageDisplayProps) => {
return (
{`${Math.round(value * 100)}%`}
);
};
diff --git a/src/components/RecordingGraph.tsx b/src/components/RecordingGraph.tsx
index 1326b6c65..29d3e4281 100644
--- a/src/components/RecordingGraph.tsx
+++ b/src/components/RecordingGraph.tsx
@@ -1,4 +1,4 @@
-import { Box } from "@chakra-ui/react";
+import { Box, BoxProps } from "@chakra-ui/react";
import {
Chart,
LineController,
@@ -11,11 +11,11 @@ import { useEffect, useRef } from "react";
import { XYZData } from "../model";
import { getConfig as getRecordingChartConfig } from "../recording-graph";
-interface RecordingGraphProps {
+interface RecordingGraphProps extends BoxProps {
data: XYZData;
}
-const RecordingGraph = ({ data }: RecordingGraphProps) => {
+const RecordingGraph = ({ data, ...rest }: RecordingGraphProps) => {
const canvasRef = useRef(null);
useEffect(() => {
@@ -40,6 +40,7 @@ const RecordingGraph = ({ data }: RecordingGraphProps) => {
borderColor="gray.200"
width="100%"
height="100%"
+ {...rest}
>
diff --git a/src/components/ResourceCard.tsx b/src/components/ResourceCard.tsx
new file mode 100644
index 000000000..b69017952
--- /dev/null
+++ b/src/components/ResourceCard.tsx
@@ -0,0 +1,46 @@
+import {
+ AspectRatio,
+ HStack,
+ Heading,
+ Image,
+ LinkBox,
+ LinkOverlay,
+ VStack,
+} from "@chakra-ui/react";
+import { ReactNode } from "react";
+import Link from "./Link";
+
+interface ResourceCardProps {
+ url: string;
+ imgSrc: string;
+ title: ReactNode;
+}
+
+const ResourceCard = ({ imgSrc, url, title }: ResourceCardProps) => {
+ return (
+
+
+
+
+
+
+
+
+ {title}
+
+
+
+
+
+ );
+};
+
+export default ResourceCard;
diff --git a/src/components/ResourceCardPlaceholder.tsx b/src/components/ResourceCardPlaceholder.tsx
new file mode 100644
index 000000000..47f5cf93b
--- /dev/null
+++ b/src/components/ResourceCardPlaceholder.tsx
@@ -0,0 +1,34 @@
+import { AspectRatio, Box, Text, VStack } from "@chakra-ui/react";
+
+const ResourceCardPlaceholder = () => {
+ return (
+
+
+
+
+
+
+ Coming soon
+
+
+ );
+};
+
+export default ResourceCardPlaceholder;
diff --git a/src/components/StartOverWarningDialog.tsx b/src/components/StartOverWarningDialog.tsx
deleted file mode 100644
index 9ddaafe75..000000000
--- a/src/components/StartOverWarningDialog.tsx
+++ /dev/null
@@ -1,84 +0,0 @@
-import {
- Button,
- Heading,
- Link,
- Modal,
- ModalBody,
- ModalCloseButton,
- ModalContent,
- ModalFooter,
- ModalOverlay,
- Text,
- VStack,
-} from "@chakra-ui/react";
-import { ReactNode, useCallback } from "react";
-import { FormattedMessage } from "react-intl";
-import { useProject } from "../hooks/project-hooks";
-
-interface StartOverWardningDialogProps {
- isOpen: boolean;
- onClose: () => void;
- onStart: () => void;
-}
-
-const StartOverWarningDialog = ({
- isOpen,
- onClose,
- onStart,
-}: StartOverWardningDialogProps) => {
- const { saveHex } = useProject();
- const handleSaveHex = useCallback(async () => {
- await saveHex();
- }, [saveHex]);
- return (
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- (
-
- {chunks}
-
- ),
- }}
- />
-
-
-
-
-
-
-
-
-
-
- );
-};
-
-export default StartOverWarningDialog;
diff --git a/src/components/StartResumeActions.tsx b/src/components/StartResumeActions.tsx
deleted file mode 100644
index 24150d775..000000000
--- a/src/components/StartResumeActions.tsx
+++ /dev/null
@@ -1,71 +0,0 @@
-import { Button, HStack, StackProps, useDisclosure } from "@chakra-ui/react";
-import { useCallback } from "react";
-import { FormattedMessage } from "react-intl";
-import { useNavigate } from "react-router";
-import { useConnectionStage } from "../connection-stage-hooks";
-import { SessionPageId } from "../pages-config";
-import { useHasGestures, useStore } from "../store";
-import { createSessionPageUrl } from "../urls";
-import StartOverWarningDialog from "./StartOverWarningDialog";
-
-const StartResumeActions = ({ ...props }: Partial) => {
- const newSession = useStore((s) => s.newSession);
- const hasExistingSession = useHasGestures();
- const startOverWarningDialogDisclosure = useDisclosure();
- const navigate = useNavigate();
- const { actions: connStageActions } = useConnectionStage();
-
- const handleNavigateToAddData = useCallback(() => {
- navigate(createSessionPageUrl(SessionPageId.DataSamples));
- }, [navigate]);
-
- const handleStartNewSession = useCallback(() => {
- startOverWarningDialogDisclosure.onClose();
- newSession();
- handleNavigateToAddData();
- connStageActions.startConnect();
- }, [
- startOverWarningDialogDisclosure,
- newSession,
- handleNavigateToAddData,
- connStageActions,
- ]);
-
- const onClickStartNewSession = useCallback(() => {
- if (hasExistingSession) {
- startOverWarningDialogDisclosure.onOpen();
- } else {
- handleStartNewSession();
- }
- }, [
- handleStartNewSession,
- hasExistingSession,
- startOverWarningDialogDisclosure,
- ]);
-
- return (
- <>
-
-
- {hasExistingSession && (
-
- )}
-
-
- >
- );
-};
-
-export default StartResumeActions;
diff --git a/src/components/YoutubeVideoEmbed.tsx b/src/components/YoutubeVideoEmbed.tsx
new file mode 100644
index 000000000..8df16fb07
--- /dev/null
+++ b/src/components/YoutubeVideoEmbed.tsx
@@ -0,0 +1,34 @@
+import { AspectRatio, Box } from "@chakra-ui/react";
+
+export interface YoutubeVideo {
+ alt: string;
+ youtubeId: string;
+}
+
+interface YoutubeVideoProps {
+ alt: string;
+ youtubeId: string;
+}
+
+const YoutubeVideoEmbed = ({ alt, youtubeId }: YoutubeVideoProps) => {
+ return (
+
+
+
+
+
+ );
+};
+
+export default YoutubeVideoEmbed;
diff --git a/src/hooks/project-hooks.tsx b/src/hooks/project-hooks.tsx
index c6a732ce1..1b5fb1100 100644
--- a/src/hooks/project-hooks.tsx
+++ b/src/hooks/project-hooks.tsx
@@ -132,29 +132,14 @@ export const ProjectProvider = ({
const saveHex = useCallback(
async (hex?: HexData): Promise => {
const { step } = save;
- if (hex) {
- if (settings.showPreSaveHelp && step === SaveStep.None) {
- // All we do is trigger the help and remember the project.
- setSave({
- step: SaveStep.PreSaveHelp,
- hex: hex,
- });
- } else {
- // We can just go ahead and download. Either the project came from
- // the editor or via the dialog flow.
- downloadHex(hex);
- setSave({
- step: SaveStep.None,
- });
- toast({
- id: "save-complete",
- position: "top",
- duration: 5_000,
- title: intl.formatMessage({ id: "saving-toast-title" }),
- status: "info",
- });
- }
- } else {
+ if (settings.showPreSaveHelp && step === SaveStep.None) {
+ // All we do is trigger the help and remember the project (if any).
+ // We'll be invoked again
+ setSave({
+ step: SaveStep.PreSaveHelp,
+ hex: hex,
+ });
+ } else if (!hex) {
// We need to request something to save.
setSave({
step: SaveStep.SaveProgress,
@@ -163,6 +148,20 @@ export const ProjectProvider = ({
saveNextDownloadRef.current = true;
await driverRef.current!.compile();
});
+ } else {
+ // We can just go ahead and download. Either the project came from
+ // the editor or via the dialog flow.
+ downloadHex(hex);
+ setSave({
+ step: SaveStep.None,
+ });
+ toast({
+ id: "save-complete",
+ position: "top",
+ duration: 5_000,
+ title: intl.formatMessage({ id: "saving-toast-title" }),
+ status: "info",
+ });
}
},
[
diff --git a/src/images/block.png b/src/images/block.png
new file mode 100644
index 000000000..a541d6c91
Binary files /dev/null and b/src/images/block.png differ
diff --git a/src/images/model_blue.svg b/src/images/model_blue.svg
deleted file mode 100644
index 63087d0df..000000000
--- a/src/images/model_blue.svg
+++ /dev/null
@@ -1,4 +0,0 @@
-
-
diff --git a/src/images/xyz-graph.png b/src/images/xyz-graph.png
new file mode 100644
index 000000000..5477008cf
Binary files /dev/null and b/src/images/xyz-graph.png differ
diff --git a/src/messages/ui.en.json b/src/messages/ui.en.json
index cf5710e42..d91c6362d 100644
--- a/src/messages/ui.en.json
+++ b/src/messages/ui.en.json
@@ -1215,16 +1215,10 @@
"value": "Live graph"
}
],
- "footer.resume": [
+ "get-started-action": [
{
"type": 0,
- "value": "Resume session"
- }
- ],
- "footer.start": [
- {
- "type": 0,
- "value": "Start new session"
+ "value": "Get started"
}
],
"help-label": [
@@ -1245,10 +1239,54 @@
"value": "Home"
}
],
+ "homepage-description": [
+ {
+ "type": 0,
+ "value": "Train a machine learning model on your own movement data and run it on your micro:bit."
+ }
+ ],
+ "homepage-how-it-works": [
+ {
+ "type": 0,
+ "value": "How it works"
+ }
+ ],
+ "homepage-projects": [
+ {
+ "type": 0,
+ "value": "Projects"
+ }
+ ],
+ "homepage-step-by-step": [
+ {
+ "type": 0,
+ "value": "Step by step"
+ }
+ ],
"homepage-subtitle": [
{
"type": 0,
- "value": "Introduce students to machine learning concepts through physical movement and data"
+ "value": "Create AI on your BBC micro:bit using movement and machine learning."
+ }
+ ],
+ "homepage-video-alt": [
+ {
+ "type": 0,
+ "value": "Introductory video"
+ }
+ ],
+ "homepage-video-prompt": [
+ {
+ "type": 0,
+ "value": "Watch the video to learn how to use "
+ },
+ {
+ "type": 1,
+ "value": "appNameFull"
+ },
+ {
+ "type": 0,
+ "value": "."
}
],
"homepage.Link": [
@@ -1311,6 +1349,148 @@
"value": "More edit in MakeCode options"
}
],
+ "newpage-browse-lessons": [
+ {
+ "type": 0,
+ "value": "Browse lessons"
+ }
+ ],
+ "newpage-browse-projects": [
+ {
+ "type": 0,
+ "value": "Browse projects"
+ }
+ ],
+ "newpage-choose-subtitle": [
+ {
+ "type": 0,
+ "value": "Find a project or lesson from our teacher resources and open it in micro:bit classroom"
+ }
+ ],
+ "newpage-choose-title": [
+ {
+ "type": 0,
+ "value": "Choose a project or lesson"
+ }
+ ],
+ "newpage-continue-session-instruction": [
+ {
+ "type": 0,
+ "value": "To continue a saved session from a file please select a file."
+ }
+ ],
+ "newpage-continue-session-subtitle": [
+ {
+ "type": 0,
+ "value": "Use a hex file or data samples file you saved to your computer to continue a session."
+ }
+ ],
+ "newpage-continue-session-title": [
+ {
+ "type": 0,
+ "value": "Continue a saved session"
+ }
+ ],
+ "newpage-heading-subtitle": [
+ {
+ "type": 0,
+ "value": "Run whole class sessions, easily share code with students and save progress"
+ }
+ ],
+ "newpage-heading-title": [
+ {
+ "type": 0,
+ "value": "Welcome to micro:bit classroom"
+ }
+ ],
+ "newpage-last-session-date": [
+ {
+ "children": [
+ {
+ "type": 0,
+ "value": "Date:"
+ }
+ ],
+ "type": 8,
+ "value": "strong"
+ },
+ {
+ "type": 0,
+ "value": " "
+ },
+ {
+ "type": 1,
+ "value": "date"
+ }
+ ],
+ "newpage-last-session-name": [
+ {
+ "children": [
+ {
+ "type": 0,
+ "value": "Name:"
+ }
+ ],
+ "type": 8,
+ "value": "strong"
+ },
+ {
+ "type": 0,
+ "value": " "
+ },
+ {
+ "type": 1,
+ "value": "name"
+ }
+ ],
+ "newpage-last-session-none": [
+ {
+ "type": 0,
+ "value": "No session found"
+ }
+ ],
+ "newpage-last-session-title": [
+ {
+ "type": 0,
+ "value": "Open last session"
+ }
+ ],
+ "newpage-new-session-subtitle": [
+ {
+ "type": 0,
+ "value": "Connect your micro:bit and collect movement data to build a machine learning model"
+ }
+ ],
+ "newpage-new-session-title": [
+ {
+ "type": 0,
+ "value": "New session"
+ }
+ ],
+ "newpage-section-one-title": [
+ {
+ "type": 0,
+ "value": "Pick up where you left off"
+ }
+ ],
+ "newpage-section-two-title": [
+ {
+ "type": 0,
+ "value": "Start something new"
+ }
+ ],
+ "newpage-subtitle": [
+ {
+ "type": 0,
+ "value": "Introduce students to machine learning concepts through physical movement and data"
+ }
+ ],
+ "newpage-title": [
+ {
+ "type": 0,
+ "value": "New session"
+ }
+ ],
"no-data-samples": [
{
"type": 0,
@@ -1509,6 +1689,36 @@
"value": "Start training"
}
],
+ "steps-code": [
+ {
+ "type": 0,
+ "value": "Code"
+ }
+ ],
+ "steps-collect-data": [
+ {
+ "type": 0,
+ "value": "Collect data"
+ }
+ ],
+ "steps-improve": [
+ {
+ "type": 0,
+ "value": "Improve"
+ }
+ ],
+ "steps-test-model": [
+ {
+ "type": 0,
+ "value": "Test model"
+ }
+ ],
+ "steps-train": [
+ {
+ "type": 0,
+ "value": "Train"
+ }
+ ],
"support-request": [
{
"type": 0,
diff --git a/src/pages/DataSamplesPage.tsx b/src/pages/DataSamplesPage.tsx
index 003642186..54b9d74b4 100644
--- a/src/pages/DataSamplesPage.tsx
+++ b/src/pages/DataSamplesPage.tsx
@@ -4,7 +4,10 @@ import { RiAddLine, RiArrowRightLine } from "react-icons/ri";
import { FormattedMessage } from "react-intl";
import { useNavigate } from "react-router";
import DataSampleGridView from "../components/DataSampleGridView";
-import DefaultPageLayout from "../components/DefaultPageLayout";
+import DefaultPageLayout, {
+ ProjectMenuItems,
+ ProjectToolbarItems,
+} from "../components/DefaultPageLayout";
import LiveGraphPanel from "../components/LiveGraphPanel";
import { SessionPageId } from "../pages-config";
import { useHasSufficientDataForTraining, useStore } from "../store";
@@ -30,8 +33,8 @@ const DataSamplesPage = () => {
}
+ toolbarItemsRight={}
>
diff --git a/src/pages/HomePage.tsx b/src/pages/HomePage.tsx
index 91e8ac368..6028e76a8 100644
--- a/src/pages/HomePage.tsx
+++ b/src/pages/HomePage.tsx
@@ -1,105 +1,301 @@
import {
- AspectRatio,
+ Box,
+ BoxProps,
+ Button,
+ Container,
Heading,
+ HStack,
Image,
- Stack,
Text,
+ useInterval,
VStack,
} from "@chakra-ui/react";
+import { ReactNode, useCallback, useState } from "react";
import { FormattedMessage, useIntl } from "react-intl";
+import { useNavigate } from "react-router";
import DefaultPageLayout from "../components/DefaultPageLayout";
-import StartResumeActions from "../components/StartResumeActions";
-import addDataImage from "../images/add_data.svg";
-import testModelImage from "../images/test_model_blue.svg";
-import trainModelImage from "../images/train_model_blue.svg";
+import PercentageDisplay from "../components/PercentageDisplay";
+import PercentageMeter from "../components/PercentageMeter";
+import RecordingGraph from "../components/RecordingGraph";
+import ResourceCard from "../components/ResourceCard";
+import ResourceCardPlaceholder from "../components/ResourceCardPlaceholder";
+import YoutubeVideoEmbed from "../components/YoutubeVideoEmbed";
import { useDeployment } from "../deployment";
+import blockImage from "../images/block.png";
+import xyzGraph from "../images/xyz-graph.png";
+import { createNewPageUrl } from "../urls";
-type StepId = "add-data" | "train-model" | "test-model";
-
-interface StepConfig {
- id: StepId;
- imgSrc: string;
-}
-
-const stepsConfig: StepConfig[] = [
- {
- id: "add-data",
- imgSrc: addDataImage,
- },
- {
- id: "train-model",
- imgSrc: trainModelImage,
- },
- {
- id: "test-model",
- imgSrc: testModelImage,
- },
-];
+const graphData = {
+ x: [
+ 0.4, 0.152, -0.008, 0.056, -0.324, 1.604, 2.04, 0.92, 1.844, 1.872, 2.04,
+ 2.04, 2.04, 2.04, 0.196, -1.004, 0.928, 0.624, -0.052, -0.38, 0.08, -0.136,
+ 0.156, 0.316, -0.264, -0.28, 2.04, 2.04, 1.896, 2.04, 2.04, 2.04, 2.04,
+ 2.04, 1.484, -0.408, -0.424, 0.988, 0.412, -0.528, -0.568, 0.428, 0.44,
+ 0.364, 0.004, -0.028, -0.304, 2.04, 2.004, 0.98, 2.04, 2.04, 1.368, 2.04,
+ 2.04, 1.472, -1.328, 0.4, 0.904, -0.392, -0.2, 0.06, -0.044, -0.172, 0.076,
+ -0.044, -0.316, 0.692, 2.04, 2.04, 2.04, 1.148, 1.1, 2.04, 2.04, 2.04,
+ 0.952, -0.68, 0.032, 0.48, -0.08, -0.068, 0.024, 0.336, 0.204, -0.1, -0.244,
+ 0.04, 1.168,
+ ],
+ y: [
+ -0.752, -1.192, -1.212, -1.456, -0.972, -0.42, 0.292, 1.68, 0.544, -0.66,
+ -1.148, -0.224, -0.784, -1.196, -0.888, 0.472, 1.172, -0.776, -0.86, 0.476,
+ 0.324, 0.228, 0.576, 0.68, 0.904, 0.708, -0.9, 0.02, 0.752, 0.424, -1.012,
+ -0.44, -0.144, -0.032, -0.072, 0.764, 1.228, 0.872, 0.436, 0.616, 0.324,
+ 0.26, -0.416, -1.748, -2.04, -1.036, 0.596, 0.788, 0.46, 0.02, 0.676,
+ -1.332, -1.112, -0.48, -0.548, -1.16, 0.516, 0.264, -0.72, -0.8, 0.488,
+ 0.868, 0.836, 0.404, 0.36, 0.276, 0.244, 0.348, 0.88, 1.468, -0.596, -1.248,
+ -0.864, 0.068, -0.512, 0.092, 0.592, -0.024, 1.008, 0.432, 0.508, 0.76,
+ 0.568, 0.14, -0.348, -1.192, -1.528, -1.496, 0.38,
+ ],
+ z: [
+ -0.408, -0.152, 0.06, -0.044, 2.04, -0.7, -1.548, 0.58, 0.62, 0.932, 0.764,
+ 0.408, 0.132, -1.004, 0.324, 2.04, 1.156, 0.156, 0.076, -0.232, -0.244,
+ 0.036, 0.056, 0.024, 1.044, 2.04, -2.012, 0.644, -0.44, 0.42, 0.672, 0.604,
+ 0.132, -0.432, 0, 1.176, 2.04, 0.152, 0.04, -0.544, 0.072, 0.116, 0.208,
+ 0.868, 0.816, 0.324, 2.04, -2.04, 1.536, -0.044, 0.444, 0.552, 0.784,
+ -0.004, -0.604, -0.008, 2.04, 1.764, 0.044, 0.068, -0.02, -0.052, -0.052,
+ 0.024, -0.196, 0.28, 2.04, 0.704, -0.576, 0.432, 0.788, 0.88, 0.872, 0.22,
+ 0.288, -0.516, 0.348, 2.04, 1.892, 0.12, -0.04, -0.464, -0.104, -0.088,
+ -0.084, 0.22, 0.74, 2.04, -0.096,
+ ],
+};
const HomePage = () => {
+ const navigate = useNavigate();
+ const handleGetStarted = useCallback(() => {
+ navigate(createNewPageUrl());
+ }, [navigate]);
const intl = useIntl();
const { appNameFull } = useDeployment();
return (
-
-
-
-
- {appNameFull}
+
+
+
+ }
+ >
+
+
+
+
+ {appNameFull}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
-
+
+
+
+ Video coming soon
+
+
+
+
-
-
- {stepsConfig.map(({ id, imgSrc }, idx) => (
-
- ))}
-
+
+
+
+
+
+ }
+ />
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ }
+ />
+ }
+ />
+
+
+
+
+
+
+
+
+
+
-
-
+
);
};
interface StepProps {
- title: string;
- imgSrc: string;
- description: string;
+ title: ReactNode;
+ image: ReactNode;
}
-const Step = ({ title, imgSrc, description }: StepProps) => (
-
-
+const Step = ({ title, image }: StepProps) => (
+
+ {image}
+
{title}
-
-
-
- {description}
);
+const CollectDataIllustration = () => {
+ const props = {
+ data: graphData,
+ bgColor: "white",
+ w: "158px",
+ };
+ return (
+
+
+
+
+
+ );
+};
+
+const TestModelStepIllustration = () => {
+ const [value, setValue] = useState(0.75);
+ const colorScheme = Math.round(value * 100) >= 80 ? "brand2.500" : undefined;
+ useInterval(() => {
+ setValue((value) => 0.8 * value + 0.2 * Math.min(1, 2.5 * Math.random()));
+ }, 1500);
+ return (
+
+
+
+
+ );
+};
+
+const CodeIllustration = () => {
+ return (
+
+ );
+};
+
+const Arrow = (props: BoxProps) => {
+ return (
+
+
+
+ );
+};
+
export default HomePage;
diff --git a/src/pages/NewPage.tsx b/src/pages/NewPage.tsx
new file mode 100644
index 000000000..ee2024885
--- /dev/null
+++ b/src/pages/NewPage.tsx
@@ -0,0 +1,164 @@
+import {
+ Box,
+ Container,
+ Heading,
+ HStack,
+ Icon,
+ Stack,
+ Text,
+ VStack,
+} from "@chakra-ui/react";
+import { ReactNode, useCallback, useRef } from "react";
+import { RiAddLine, RiFolderOpenLine, RiRestartLine } from "react-icons/ri";
+import { FormattedMessage, useIntl } from "react-intl";
+import { useNavigate } from "react-router";
+import DefaultPageLayout, {
+ HomeMenuItem,
+ HomeToolbarItem,
+} from "../components/DefaultPageLayout";
+import LoadProjectInput, {
+ LoadProjectInputRef,
+} from "../components/LoadProjectInput";
+import NewPageChoice from "../components/NewPageChoice";
+import { useConnectionStage } from "../connection-stage-hooks";
+import { SessionPageId } from "../pages-config";
+import { useStore } from "../store";
+import { createSessionPageUrl } from "../urls";
+
+const NewPage = () => {
+ const existingSessionTimestamp = useStore((s) => s.timestamp);
+ const projectName = useStore((s) => s.project.header?.name ?? "Untitled");
+ const newSession = useStore((s) => s.newSession);
+ const navigate = useNavigate();
+ const { actions: connStageActions } = useConnectionStage();
+
+ const handleOpenLastSession = useCallback(() => {
+ navigate(createSessionPageUrl(SessionPageId.DataSamples));
+ }, [navigate]);
+
+ const loadProjectRef = useRef(null);
+ const handleContinueSessionFromFile = useCallback(() => {
+ loadProjectRef.current?.chooseFile();
+ }, []);
+
+ const handleStartNewSession = useCallback(() => {
+ newSession();
+ navigate(createSessionPageUrl(SessionPageId.DataSamples));
+ connStageActions.startConnect();
+ }, [newSession, navigate, connStageActions]);
+
+ const intl = useIntl();
+ const lastSessionTitle = intl.formatMessage({
+ id: "newpage-last-session-title",
+ });
+ const continueSessionTitle = intl.formatMessage({
+ id: "newpage-continue-session-title",
+ });
+ const newSessionTitle = intl.formatMessage({
+ id: "newpage-new-session-title",
+ });
+
+ return (
+ }
+ menuItems={}
+ >
+
+
+
+
+
+
+
+
+
+
+
+ }
+ >
+ {existingSessionTimestamp ? (
+
+
+ (
+
+ {chunks}
+
+ ),
+ name: projectName,
+ }}
+ />
+
+
+ (
+
+ {chunks}
+
+ ),
+ date: new Intl.DateTimeFormat(undefined, {
+ dateStyle: "medium",
+ }).format(existingSessionTimestamp),
+ }}
+ />
+
+
+ ) : (
+
+
+
+ )}
+
+ }
+ >
+
+
+
+
+
+
+
+
+
+ }
+ >
+
+
+
+
+
+
+
+
+
+
+ );
+};
+
+export default NewPage;
diff --git a/src/pages/TestingModelPage.tsx b/src/pages/TestingModelPage.tsx
index 9312234d1..5eeab624a 100644
--- a/src/pages/TestingModelPage.tsx
+++ b/src/pages/TestingModelPage.tsx
@@ -3,7 +3,10 @@ import { useCallback, useEffect } from "react";
import { FormattedMessage } from "react-intl";
import { useNavigate } from "react-router";
import BackArrow from "../components/BackArrow";
-import DefaultPageLayout from "../components/DefaultPageLayout";
+import DefaultPageLayout, {
+ ProjectMenuItems,
+ ProjectToolbarItems,
+} from "../components/DefaultPageLayout";
import TestingModelGridView from "../components/TestingModelGridView";
import { SessionPageId } from "../pages-config";
import { useStore } from "../store";
@@ -27,8 +30,8 @@ const TestingModelPage = () => {
}
+ toolbarItemsRight={}
toolbarItemsLeft={