Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ import ImportPage from "./pages/ImportPage";
import NewPage from "./pages/NewPage";
import TestingModelPage from "./pages/TestingModelPage";
import OpenSharedProjectPage from "./pages/OpenSharedProjectPage";
import { loadProjectFromStorage, useStore } from "./store";
import { getAllProjects, loadProjectFromStorage, useStore } from "./store";
import {
createCodePageUrl,
createDataSamplesPageUrl,
Expand Down Expand Up @@ -185,6 +185,10 @@ const createRouter = () => {
{
path: createNewPageUrl(),
element: <NewPage />,
loader: () => {
const allProjectData = getAllProjects();
return defer({ allProjectData });
},
},
{ path: createImportPageUrl(), element: <ImportPage /> },
{
Expand Down
2 changes: 1 addition & 1 deletion src/e2e/new-page.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ test.describe("new page", () => {
await newPage.startNewSession();
await dataSamplesPage.navbar.home();
await homePage.getStarted();
await newPage.expectResumeButtonToShowProjectName("Untitled");
// await newPage.expectResumeButtonToShowProjectName("Untitled");
await newPage.resumeSession();
await dataSamplesPage.expectOnPage();
});
Expand Down
15 changes: 11 additions & 4 deletions src/ml.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,18 +19,25 @@ import {
import actionDataBadLabels from "./test-fixtures/shake-still-circle-legacy-bad-labels.json";
import actionData from "./test-fixtures/shake-still-circle-data-samples-legacy.json";
import testData from "./test-fixtures/shake-still-circle-legacy-test-data.json";
import { currentDataWindow, migrateLegacyActionData } from "./project-utils";
import {
currentDataWindow,
migrateLegacyActionDataAndAssignNewIds,
} from "./project-utils";

const fixUpTestData = (data: Partial<OldActionData>[]): OldActionData[] => {
data.forEach((action) => (action.icon = "Heart"));
return data as OldActionData[];
};

const migratedActionData = migrateLegacyActionData(fixUpTestData(actionData));
const migratedActionDataBadLabels = migrateLegacyActionData(
const migratedActionData = migrateLegacyActionDataAndAssignNewIds(
fixUpTestData(actionData)
);
const migratedActionDataBadLabels = migrateLegacyActionDataAndAssignNewIds(
fixUpTestData(actionDataBadLabels)
);
const migratedTestData = migrateLegacyActionData(fixUpTestData(testData));
const migratedTestData = migrateLegacyActionDataAndAssignNewIds(
fixUpTestData(testData)
);

let trainingResult: TrainingResult;
beforeAll(async () => {
Expand Down
5 changes: 5 additions & 0 deletions src/pages/DataSamplesPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -26,8 +26,13 @@ const DataSamplesPage = () => {
const actions = useStore((s) => s.actions);
const addNewAction = useStore((s) => s.addNewAction);
const model = useStore((s) => s.model);
const updateOrCreateProject = useStore((s) => s.updateOrCreateProject);
const [selectedActionIdx, setSelectedActionIdx] = useState<number>(0);

useEffect(() => {
void updateOrCreateProject();
}, [updateOrCreateProject]);

const navigate = useNavigate();
const trainModelFlowStart = useStore((s) => s.trainModelFlowStart);

Expand Down
126 changes: 123 additions & 3 deletions src/pages/NewPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,18 +6,23 @@
*/
import {
Box,
Card,
CardBody,
Container,
Grid,
GridItem,
Heading,
HStack,
Icon,
IconButton,
Stack,
Text,
VStack,
} from "@chakra-ui/react";
import { ReactNode, useCallback, useRef } from "react";
import { ReactNode, Suspense, useCallback, useRef, useState } from "react";
import { RiAddLine, RiFolderOpenLine, RiRestartLine } from "react-icons/ri";
import { FormattedMessage, useIntl } from "react-intl";
import { useNavigate } from "react-router";
import { Await, useAsyncValue, useLoaderData, useNavigate } from "react-router";
import DefaultPageLayout, {
HomeMenuItem,
HomeToolbarItem,
Expand All @@ -27,16 +32,22 @@ import LoadProjectInput, {
} from "../components/LoadProjectInput";
import NewPageChoice from "../components/NewPageChoice";
import { useLogging } from "../logging/logging-hooks";
import { useStore } from "../store";
import { loadProjectFromStorage, useStore } from "../store";
import { createDataSamplesPageUrl } from "../urls";
import { useProjectName } from "../hooks/project-hooks";
import LoadingAnimation from "../components/LoadingAnimation";
import { ProjectData, ProjectDataWithActions, StoreAction } from "../storage";
import { DeleteIcon } from "@chakra-ui/icons";

const NewPage = () => {
const existingSessionTimestamp = useStore((s) => s.timestamp);
const projectName = useProjectName();
const newSession = useStore((s) => s.newSession);
const navigate = useNavigate();
const logging = useLogging();
const { allProjectData } = useLoaderData() as {
allProjectData: ProjectData[];
};

const handleOpenLastSession = useCallback(() => {
logging.event({
Expand Down Expand Up @@ -165,11 +176,120 @@ const NewPage = () => {
</NewPageChoice>
<Box flex="1" />
</HStack>
<Suspense fallback={<LoadingAnimation />}>
<Await resolve={allProjectData}>
<ProjectsList />
</Await>
</Suspense>
</VStack>
</Container>
</VStack>
</DefaultPageLayout>
);
};

const ProjectsList = () => {
const data = useAsyncValue() as ProjectDataWithActions[];
const [projects, setProjects] = useState(data);
const deleteProject = useStore((s) => s.deleteProject);

const handleDeleteProject = useCallback(
async (id: string) => {
setProjects((prev) => prev.filter((p) => p.id !== id));
await deleteProject(id);
},
[deleteProject]
);

return (
<>
<Heading as="h2" fontSize="2xl" mt={8}>
Projects
</Heading>
<Grid mt={3} gap={3} templateColumns="repeat(5, 1fr)">
{projects.map((projectData) => (
<GridItem key={projectData.id} display="flex">
<ProjectCard
id={projectData.id}
name={projectData.name}
actions={projectData.actions}
updatedAt={projectData.updatedAt}
onDeleteProject={handleDeleteProject}
/>
</GridItem>
))}
</Grid>
</>
);
};

interface ProjectCard {
id: string;
name: string;
actions: StoreAction[];
updatedAt: number;
onDeleteProject: (id: string) => Promise<void>;
}

const ProjectCard = ({
id,
name,
actions,
updatedAt,
onDeleteProject,
}: ProjectCard) => {
const navigate = useNavigate();

const handleLoadProject = useCallback(
async (_e: React.MouseEvent) => {
await loadProjectFromStorage(id);
navigate(createDataSamplesPageUrl());
},
[id, navigate]
);

const handleDeleteProject = useCallback(
async (e: React.MouseEvent) => {
e.stopPropagation();
await onDeleteProject(id);
},
[id, onDeleteProject]
);

return (
<Card onClick={handleLoadProject} cursor="pointer" flexGrow={1}>
<IconButton
aria-label="Delete project"
onClick={handleDeleteProject}
icon={<DeleteIcon />}
position="absolute"
right={1}
top={1}
borderRadius="sm"
border="none"
/>
<CardBody display="flex">
<Stack h="100%">
<Heading as="h3" fontSize="xl">
{name}
</Heading>
<Text mb="auto">
Actions:{" "}
{actions.length > 0
? actions.map((a) => a.name).join(", ")
: "none"}
</Text>
<Text>{`Last edited: ${new Intl.DateTimeFormat(undefined, {
dateStyle: "medium",
timeStyle: "medium",
}).format(updatedAt)}`}</Text>
<Text fontSize="xs" color="gray.600">
id: {id}
</Text>
</Stack>
</CardBody>
</Card>
);
};

export default NewPage;
13 changes: 11 additions & 2 deletions src/project-utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@ export const createUntitledProject = (): MakeCodeProject => ({
),
});

export const migrateLegacyActionData = (
export const migrateLegacyActionDataAndAssignNewIds = (
actions: OldActionData[] | ActionData[]
): ActionData[] => {
return actions.map((a) => {
Expand All @@ -85,6 +85,15 @@ export const migrateLegacyActionData = (
createdAt: (a as OldActionData).ID,
};
}
return a as ActionData;
// Assign new unique ids to actions and recordings.
// This is required if the user imports the same dataset / project twice.
return {
...a,
id: uuid(),
recordings: a.recordings.map((r) => ({
...r,
id: uuid(),
})),
} as ActionData;
});
};
Loading