docs: improved webpage

This commit is contained in:
Morten Olsen
2023-09-26 21:14:31 +02:00
parent c00b073d37
commit 5d8cba0ddd
54 changed files with 1358 additions and 693 deletions

1
.npmrc
View File

@@ -1,4 +1,3 @@
node-linker=hoisted
public-hoist-pattern[]=*@nextui-org/* public-hoist-pattern[]=*@nextui-org/*
store-dir=.pnpm-store store-dir=.pnpm-store

View File

@@ -5,4 +5,4 @@ export {
type CalulationResult, type CalulationResult,
type PlanableWithPlugins, type PlanableWithPlugins,
} from './algorithm/calulation'; } from './algorithm/calulation';
export { plugins } from './plugins/index'; export { plugins, type AllPlugins } from './plugins';

View File

@@ -7,4 +7,9 @@ const plugins = {
capabilities, capabilities,
} satisfies Record<string, (...args: any[]) => Plugin>; } satisfies Record<string, (...args: any[]) => Plugin>;
type AllPlugins = {
[K in keyof typeof plugins]: ReturnType<(typeof plugins)[K]>;
};
export type { AllPlugins };
export { plugins }; export { plugins };

View File

@@ -1,3 +1,4 @@
import { Expand, UnionToIntersection } from '../types/utils';
import { GraphNode } from './node'; import { GraphNode } from './node';
import { Planable } from './planable'; import { Planable } from './planable';
@@ -20,16 +21,20 @@ type Plugin<TAttributes = any, TContext = any> = {
type Plugins = Record<string, Plugin>; type Plugins = Record<string, Plugin>;
type PluginAttributes<TPlugins extends Plugins> = { type PluginAttributes<TPlugins extends Plugins> = MergeRecords<{
[K in keyof TPlugins]: TPlugins[K] extends Plugin<infer TAttributes, any> [K in keyof TPlugins]: TPlugins[K] extends Plugin<infer TAttributes, any>
? TAttributes ? TAttributes
: never; : never;
}[keyof TPlugins]; }>;
type PluginContext<TPlugins extends Plugins> = { type MergeRecords<T extends Record<string, any>> = Expand<
UnionToIntersection<T[keyof T]>
>;
type PluginContext<TPlugins extends Plugins> = MergeRecords<{
[K in keyof TPlugins]: TPlugins[K] extends Plugin<any, infer TContext> [K in keyof TPlugins]: TPlugins[K] extends Plugin<any, infer TContext>
? TContext ? TContext
: never; : never;
}[keyof TPlugins]; }>;
export type { Plugin, Plugins, PluginAttributes, PluginContext }; export type { Plugin, Plugins, PluginAttributes, PluginContext };

View File

@@ -0,0 +1,17 @@
// expands object types one level deep
type Expand<T> = T extends infer O ? { [K in keyof O]: O[K] } : never;
// expands object types recursively
type ExpandRecursively<T> = T extends object
? T extends infer O
? { [K in keyof O]: ExpandRecursively<O[K]> }
: never
: T;
type UnionToIntersection<U> = (U extends any ? (k: U) => void : never) extends (
k: infer I,
) => void
? I
: never;
export type { Expand, ExpandRecursively, UnionToIntersection };

View File

@@ -3,9 +3,10 @@
<head> <head>
<meta charset="UTF-8" /> <meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" />
<link href="/src/index.css" rel="stylesheet" />
<title>Vite + React + TS</title> <title>Vite + React + TS</title>
</head> </head>
<body> <body class="dark text-foreground bg-background">
<div id="root"></div> <div id="root"></div>
<script type="module" src="/src/main.tsx"></script> <script type="module" src="/src/main.tsx"></script>
</body> </body>

View File

@@ -16,6 +16,8 @@
"framer-motion": "^10.16.4", "framer-motion": "^10.16.4",
"react": "^18.2.0", "react": "^18.2.0",
"react-dom": "^18.2.0", "react-dom": "^18.2.0",
"react-json-tree": "^0.18.0",
"react-router-dom": "^6.16.0",
"tailwindcss": "^3.3.3" "tailwindcss": "^3.3.3"
}, },
"devDependencies": { "devDependencies": {

View File

@@ -1,11 +1,13 @@
import { NextUIProvider } from '@nextui-org/react'; import { NextUIProvider } from '@nextui-org/react';
import { Page } from './containers/experiment'; import { Router } from './router';
const App: React.FC = () => { const App: React.FC = () => {
return ( return (
<div className="dark text-foreground bg-background">
<NextUIProvider> <NextUIProvider>
<Page slug=".capabilities" /> <Router />
</NextUIProvider> </NextUIProvider>
</div>
); );
}; };

View File

@@ -0,0 +1,13 @@
type ContentProps = {
children: React.ReactNode;
};
const Content: React.FC<ContentProps> = ({ children }) => {
return (
<div className="max-w-3xl mx-auto w-full">
<div className="flex-1 p-8">{children}</div>
</div>
);
};
export { Content };

View File

@@ -0,0 +1,41 @@
import {
Navbar,
NavbarBrand,
NavbarContent,
NavbarItem,
Link,
} from '@nextui-org/react';
import { Link as RouterLink } from 'react-router-dom';
type FrameProps = {
children: React.ReactNode;
};
const Frame = ({ children }: FrameProps) => {
return (
<div className="h-screen flex flex-col">
<Navbar shouldHideOnScroll isBordered isBlurred>
<NavbarBrand>
<RouterLink to="/">
<p className="font-bold text-inherit">Bob</p>
</RouterLink>
</NavbarBrand>
<NavbarContent className="hidden sm:flex gap-4" justify="center">
<NavbarItem>
<RouterLink to="/">
<Link color="foreground">Home</Link>
</RouterLink>
</NavbarItem>
<NavbarItem>
<RouterLink to="/experiments">
<Link color="foreground">Experiments</Link>
</RouterLink>
</NavbarItem>
</NavbarContent>
</Navbar>
<div className="flex flex-col flex-1">{children}</div>
</div>
);
};
export { Frame };

View File

@@ -0,0 +1,97 @@
import {
Button,
Modal,
ModalBody,
ModalContent,
ModalHeader,
useDisclosure,
Badge,
Checkbox,
Input,
} from '@nextui-org/react';
import { useMemo, useState } from 'react';
import { useExperimentResult } from '../../../features/experiment';
import { useSelectNode } from '../../../features/experiment/hooks';
import { GraphNode } from '@bob-the-algorithm/core';
const FindNodeView = () => {
const data = useExperimentResult();
const { isOpen, onOpen, onOpenChange } = useDisclosure();
const [completed, setCompleted] = useState(true);
const [text, setText] = useState('');
const selectNode = useSelectNode();
const nodes = useMemo(() => {
if (!data) {
return [];
}
let result = [...data.nodes];
if (completed) {
result = result.filter((node) => node.completed);
}
if (text) {
result = result.filter((node) => node.id === text);
}
return result.sort((a, b) => b.score - a.score).slice(0, 10);
}, [data, completed, text]);
const getColor = (node: GraphNode) => {
if (node.completed) {
return 'success';
}
if (node.deadEnd) {
return 'danger';
}
return 'primary';
};
return (
<>
<Button onPress={onOpen}>Find node</Button>
<Modal
isOpen={isOpen}
scrollBehavior="inside"
onOpenChange={onOpenChange}
>
<ModalContent>
{(close) => (
<>
<ModalHeader className="flex flex-col gap-1">Nodes</ModalHeader>
<ModalBody>
<Input
placeholder="Node ID"
value={text}
onValueChange={setText}
/>
<Checkbox isSelected={completed} onValueChange={setCompleted}>
Completed
</Checkbox>
<div className="flex gap-4 flex-wrap">
{nodes.map((node) => (
<Badge
content={node.score}
color={getColor(node)}
key={node.id}
>
<Button
onClick={() => {
selectNode(node);
close();
}}
key={node.id}
>
{node.id}
</Button>
</Badge>
))}
</div>
</ModalBody>
</>
)}
</ModalContent>
</Modal>
</>
);
};
export { FindNodeView };

View File

@@ -0,0 +1,32 @@
import {
Modal,
ModalContent,
ModalHeader,
ModalBody,
Button,
useDisclosure,
} from '@nextui-org/react';
import { Graph } from '../../presenters/graph';
const GraphView = () => {
const { isOpen, onOpen, onOpenChange } = useDisclosure();
return (
<>
<Button onPress={onOpen}>Show graph</Button>
<Modal isOpen={isOpen} onOpenChange={onOpenChange} size="full">
<ModalContent>
{() => (
<>
<ModalHeader className="flex flex-col gap-1">Graph</ModalHeader>
<ModalBody>
<Graph />
</ModalBody>
</>
)}
</ModalContent>
</Modal>
</>
);
};
export { GraphView };

View File

@@ -1,18 +1,18 @@
import { useEffect, useState } from 'react'; import { useEffect, useState } from 'react';
import { experiments } from '../../utils/experiments';
import { ExperimentProvider } from '../../features/experiment/context'; import { ExperimentProvider } from '../../features/experiment/context';
import { ExperimentView } from './view'; import { ExperimentView } from './view';
import { ExperimentInfo } from '../../features/experiment/types';
type PageProps = { type PageProps = {
slug: string; content: () => Promise<{ default: Experiment }>;
}; };
type Experiment = { type Experiment = {
worker: () => Worker; info: ExperimentInfo;
view: React.ReactElement; view: React.ComponentType;
}; };
const Page: React.FC<PageProps> = ({ slug }) => { const ExperimentPage: React.FC<PageProps> = ({ content }) => {
const [loading, setLoading] = useState(true); const [loading, setLoading] = useState(true);
const [error, setError] = useState<unknown>(); const [error, setError] = useState<unknown>();
const [experiment, setExperiment] = useState<Experiment>(); const [experiment, setExperiment] = useState<Experiment>();
@@ -22,11 +22,9 @@ const Page: React.FC<PageProps> = ({ slug }) => {
setError(undefined); setError(undefined);
const load = async () => { const load = async () => {
try { try {
const page = experiments.find((page) => page.slug === slug); const { default: next } = (await content()) as {
if (!page) { default: Experiment;
throw new Error(`Page not found: ${slug}`); };
}
const next = (await page.loader()) as Experiment;
console.log('n', next); console.log('n', next);
setExperiment(next); setExperiment(next);
} catch (err) { } catch (err) {
@@ -37,7 +35,7 @@ const Page: React.FC<PageProps> = ({ slug }) => {
}; };
load(); load();
}, [slug]); }, [content]);
if (error) { if (error) {
return <div>Error: {error.toString()}</div>; return <div>Error: {error.toString()}</div>;
@@ -48,10 +46,22 @@ const Page: React.FC<PageProps> = ({ slug }) => {
} }
return ( return (
<ExperimentProvider worker={experiment.worker}> <ExperimentProvider experimentInfo={experiment.info}>
<ExperimentView>{experiment.view}</ExperimentView> <ExperimentView>
<experiment.view />
</ExperimentView>
</ExperimentProvider> </ExperimentProvider>
); );
}; };
export { Page }; const pageImports = import.meta.glob('../../experiments/*/index.tsx');
const experiments: {
path: string;
element: JSX.Element;
}[] = Object.entries(pageImports).map(([path, page]) => ({
path: path.replace('../../experiments/', '').replace('/index.tsx', ''),
element: <ExperimentPage content={page as any} />,
}));
export { experiments };

View File

@@ -0,0 +1,116 @@
import {
Modal,
ModalContent,
ModalHeader,
ModalBody,
Button,
useDisclosure,
Card,
CardBody,
CardHeader,
} from '@nextui-org/react';
import { useExperimentResult } from '../../../features/experiment';
import { formatTime } from '../../../utils/time';
const InputView = () => {
const data = useExperimentResult();
const { isOpen, onOpen, onOpenChange } = useDisclosure();
if (!data) {
return null;
}
return (
<>
<Button onPress={onOpen}>Show input</Button>
<Modal
isOpen={isOpen}
onOpenChange={onOpenChange}
scrollBehavior="inside"
>
<ModalContent>
<ModalHeader className="flex flex-col gap-1">Input</ModalHeader>
<ModalBody>
<div className="flex flex-col gap-4">
{data.planables.map((item) => {
const description: [string, string][] = [];
if (item.start) {
if (item.start.min === item.start.max) {
description.push(['start', formatTime(item.start.min)]);
} else {
const min = formatTime(item.start.min);
const max = formatTime(item.start.max);
description.push(['start', `${min} - ${max}`]);
}
}
if (item.count || 0 > 1) {
description.push(['count', item.count?.toString() || '']);
}
if (item.attributes.locations) {
description.push([
'locations',
item.attributes.locations.join(', '),
]);
}
if (item.attributes.capabilities?.requires) {
description.push([
'requires',
item.attributes.capabilities.requires.join(', '),
]);
}
if (item.attributes.capabilities?.perhibits) {
description.push([
'prohibits',
item.attributes.capabilities.perhibits.join(', '),
]);
}
if (item.attributes.capabilities?.provides) {
description.push([
'provides',
item.attributes.capabilities.provides.join(', '),
]);
}
if (item.attributes.capabilities?.consumes) {
description.push([
'consumes',
item.attributes.capabilities.consumes.join(', '),
]);
}
description.push([
'duration',
`${item.duration / 1000 / 60} minutes`,
]);
return (
<Card>
<CardHeader>{item.id}</CardHeader>
<CardBody>
<div>
{description.map((d) => (
<div className="flex justify-between">
<div>
<span className="font-bold">{d[0]}: </span>
</div>
<div>
<span>{d[1]}</span>
</div>
</div>
))}
</div>
</CardBody>
</Card>
);
})}
</div>
</ModalBody>
</ModalContent>
</Modal>
</>
);
};
export { InputView };

View File

@@ -1,26 +0,0 @@
import { Accordion, AccordionItem } from '@nextui-org/react';
import { useSelectedNode } from '../../features/experiment/hooks';
import { Plan } from '../../presenters/plan';
const NodeView = () => {
const node = useSelectedNode();
if (!node) {
return null;
}
return (
<div>
<h1>{node.id}</h1>
<Accordion>
<AccordionItem title="Plan" key="plan">
<Plan node={node} />
</AccordionItem>
<AccordionItem title="Context" key="context">
<pre>{JSON.stringify(node.context, null, 2)}</pre>
</AccordionItem>
</Accordion>
</div>
);
};
export { NodeView };

View File

@@ -0,0 +1,46 @@
import { GraphNode } from '@bob-the-algorithm/core';
import { Modal, ModalBody, ModalContent, ModalHeader } from '@nextui-org/react';
import { format } from 'date-fns';
import { JSONTree } from 'react-json-tree';
import { useExperimentResult } from '../../../features/experiment';
import { useMemo } from 'react';
type NodeDetailsViewProps = {
node?: GraphNode;
onClose: () => void;
};
const NodeDetailsInfo = ({ node }: { node: GraphNode }) => {
const data = useExperimentResult();
const start = format(new Date(node.time), 'HH:mm');
const planable = useMemo(() => {
if (!data) {
return null;
}
return node.planable
? data.planables.find((n) => n.id === node.planable)
: null;
}, [node, data]);
return (
<div>
<h1>{node.id}</h1>
<p>{start}</p>
<pre></pre>
<JSONTree data={node.context} />
{planable && <JSONTree data={planable} />}
</div>
);
};
const NodeDetailsView = ({ node, onClose }: NodeDetailsViewProps) => {
return (
<Modal size="3xl" isOpen={!!node} onClose={onClose}>
<ModalContent>
<ModalHeader>Item details</ModalHeader>
<ModalBody>{node && <NodeDetailsInfo node={node} />}</ModalBody>
</ModalContent>
</Modal>
);
};
export { NodeDetailsView };

View File

@@ -0,0 +1,44 @@
import { Modal, ModalBody, ModalContent, ModalHeader } from '@nextui-org/react';
import { GraphNode } from '@bob-the-algorithm/core';
import {
useSelectNode,
useSelectedNode,
} from '../../../features/experiment/hooks';
import { Plan } from '../../../presenters/plan';
import { useState } from 'react';
import { NodeDetailsView } from './details';
import { HorizontalPlan } from '../../../presenters/horizontal-plan';
const NodeView = () => {
const node = useSelectedNode();
const selectNode = useSelectNode();
const [selectedItem, setSelectedItem] = useState<GraphNode<any>>();
return (
<>
<Modal
isOpen={!!node}
scrollBehavior="inside"
onClose={() => selectNode(undefined)}
>
<ModalContent>
<ModalHeader>Node</ModalHeader>
<ModalBody>
{node && (
<>
<HorizontalPlan node={node} />
<Plan node={node} onSelect={setSelectedItem} />
</>
)}
</ModalBody>
</ModalContent>
</Modal>
<NodeDetailsView
node={selectedItem}
onClose={() => setSelectedItem(undefined)}
/>
</>
);
};
export { NodeView };

View File

@@ -1,23 +0,0 @@
import { useExperimentResult } from '../../features/experiment';
import { useSelectNode } from '../../features/experiment/hooks';
const NodesView = () => {
const data = useExperimentResult();
const selectNode = useSelectNode();
if (!data) {
return null;
}
return (
<div>
{data.completed.map((node) => (
<div onClick={() => selectNode(node)} key={node.id}>
{node.id} {node.score}
</div>
))}
</div>
);
};
export { NodesView };

View File

@@ -0,0 +1,17 @@
import { useExperimentResult } from '../../../features/experiment';
import { useExperimentDuration } from '../../../features/experiment/hooks';
const Stats = () => {
const data = useExperimentResult();
const duration = useExperimentDuration();
return (
<div className="flex gap-2">
<div>Nodes: {data?.nodes.length}</div>
<div>Completed: {data?.completed.length}</div>
<div>Duration: {duration}ms</div>
</div>
);
};
export { Stats };

View File

@@ -1,6 +1,10 @@
import { Graph } from '../../presenters/graph'; import { Content } from '../../components/content';
import { Frame } from '../../components/frame';
import { FindNodeView } from './find-node';
import { GraphView } from './graph';
import { InputView } from './input';
import { NodeView } from './node'; import { NodeView } from './node';
import { NodesView } from './nodes'; import { Stats } from './stats';
type ExperimentViewProps = { type ExperimentViewProps = {
children: React.ReactNode; children: React.ReactNode;
@@ -8,11 +12,23 @@ type ExperimentViewProps = {
const ExperimentView: React.FC<ExperimentViewProps> = ({ children }) => { const ExperimentView: React.FC<ExperimentViewProps> = ({ children }) => {
return ( return (
<> <Frame>
<Graph /> <div className="flex flex-row h-full">
<NodesView /> <div className="flex flex-col flex-1 h-full">
<div className="flex-1">
<Content>{children}</Content>
</div>
<div className="flex-initial p-2 flex gap-2 items-center">
<GraphView />
<InputView />
<FindNodeView />
<div className="flex-1" />
<Stats />
</div>
</div>
<NodeView /> <NodeView />
</> </div>
</Frame>
); );
}; };

View File

@@ -0,0 +1,15 @@
import { Page } from './page';
const pageImports = import.meta.glob('../../pages/**/*.tsx');
const pages: any = Object.entries(pageImports).map(([path, page]) => ({
path: path
.replace('../../pages/', '')
.replace('index.tsx', '')
.replace('.tsx', ''),
element: <Page content={page as any} />,
}));
console.log('pages', pages);
export { pages };

View File

@@ -0,0 +1,38 @@
import { useEffect, useState } from 'react';
import { Frame } from '../../components/frame';
import { Skeleton } from '@nextui-org/react';
import { Content } from '../../components/content';
type Props = {
content: () => Promise<{ Page: (props: any) => JSX.Element }>;
};
const Page = ({ content }: Props) => {
const [Component, setComponent] = useState<React.ComponentType>();
useEffect(() => {
const run = async () => {
const component = await content();
setComponent(() => component.Page);
};
run();
}, [content]);
return (
<Frame>
<Content>
{!!Component ? (
<article className="my-10">
<Component />
</article>
) : (
<div className="w-full flex flex-col gap-2">
<Skeleton className="h-3 w-3/5 rounded-lg" />
<Skeleton className="h-3 w-4/5 rounded-lg" />
</div>
)}
</Content>
</Frame>
);
};
export { Page };

View File

@@ -0,0 +1,163 @@
import { ExperimentInfo } from '../../features/experiment/types';
const MIN = 1000 * 60;
const HOUR = 1000 * 60 * 60;
const info: ExperimentInfo = {
start: 0,
planables: [
{
id: `Brush teeth`,
duration: 2 * MIN,
start: {
min: 7 * HOUR,
max: 8 * HOUR,
},
attributes: {
locations: ['home'],
},
score: 1,
},
{
id: 'Drop off kids',
duration: 30 * MIN,
attributes: {
locations: ['daycare'],
capabilities: {
consumes: ['kids'],
requires: ['kids'],
},
},
score: 1,
start: {
min: 7 * HOUR,
max: 9 * HOUR,
},
},
{
id: 'visit zoo',
duration: 1 * HOUR,
attributes: {
locations: ['zoo'],
capabilities: {
requires: ['kids'],
},
},
score: 1,
start: {
min: 10 * HOUR,
max: 14 * HOUR,
},
},
{
id: 'Pickup the kids',
duration: 30 * MIN,
attributes: {
locations: ['daycare'],
capabilities: {
provides: ['kids'],
},
},
score: 1,
start: {
min: 10 * HOUR,
max: 15.5 * HOUR,
},
},
{
id: 'Do work',
duration: 1 * HOUR,
count: 5,
attributes: {
locations: ['work'],
capabilities: {
perhibits: ['kids'],
},
},
score: 10,
start: {
min: 8 * HOUR,
max: 18 * HOUR,
},
},
{
id: 'put kids to bed',
duration: 30 * MIN,
attributes: {
locations: ['home'],
capabilities: {
consumes: ['kids'],
requires: ['kids'],
},
},
score: 1,
},
{
id: 'Read book',
duration: 0.5 * HOUR,
attributes: {
locations: ['home', 'work'],
capabilities: {
perhibits: ['kids'],
},
},
score: 3,
count: 2,
start: {
min: 8 * HOUR,
max: 22 * HOUR,
},
},
{
id: 'Meditate',
duration: 10 * MIN,
score: 1,
attributes: {
locations: ['home', 'work'],
capabilities: {
perhibits: ['kids'],
},
},
start: {
min: 8 * HOUR,
max: 22 * HOUR,
},
},
{
id: 'Meeting 1',
duration: 1 * HOUR,
attributes: {
locations: ['work', 'work'],
capabilities: {
perhibits: ['kids'],
},
},
score: 10,
start: {
min: 10 * HOUR,
max: 10 * HOUR,
},
},
{
id: 'Play playstation',
duration: 1 * HOUR,
attributes: {
locations: ['home'],
capabilities: {
perhibits: ['kids'],
},
},
score: 10,
start: {
min: 16 * HOUR,
max: 24 * HOUR,
},
},
],
context: {
location: 'home',
capabilities: ['kids'],
},
};
export { info };

View File

@@ -1,12 +1,6 @@
const Foo = () => { import { info } from './experiment';
return <div></div>;
export default {
info,
view: () => <>Hello</>,
}; };
const worker = () =>
new Worker(new URL('./script.ts', import.meta.url), {
type: 'module',
});
const view = <Foo />;
export { worker, view };

View File

@@ -1,181 +0,0 @@
import { Bob, plugins } from '@bob-the-algorithm/core';
import { createWorker } from '../../features/experiment/worker';
const MIN = 1000 * 60;
const HOUR = 1000 * 60 * 60;
const getTravelTime = async () => 30 * MIN;
const transport = plugins.transport({
getTravelTime,
});
const realistic = async () => {
try {
const bob = new Bob({
plugins: { transport, capabilities: plugins.capabilities() },
});
const result = await bob.run({
context: {
location: 'home',
capabilities: ['kids'],
},
start: 0,
heuristic: ({ completed }) => completed.length >= 30,
planables: [
{
id: `Brush teeth`,
duration: 2 * MIN,
start: {
min: 7 * HOUR,
max: 8 * HOUR,
},
attributes: {
locations: ['home'],
},
score: 1,
},
{
id: 'Drop off kids',
duration: 30 * MIN,
attributes: {
locations: ['daycare'],
capabilities: {
consumes: ['kids'],
requires: ['kids'],
},
},
score: 1,
start: {
min: 7 * HOUR,
max: 9 * HOUR,
},
},
{
id: 'visit zoo',
duration: 1 * HOUR,
attributes: {
locations: ['zoo'],
capabilities: {
requires: ['kids'],
},
},
score: 1,
start: {
min: 10 * HOUR,
max: 14 * HOUR,
},
},
{
id: 'Pickup the kids',
duration: 30 * MIN,
attributes: {
locations: ['daycare'],
capabilities: {
provides: ['kids'],
},
},
score: 1,
start: {
min: 10 * HOUR,
max: 15.5 * HOUR,
},
},
{
id: 'Do work',
duration: 1 * HOUR,
count: 5,
attributes: {
locations: ['work'],
capabilities: {
perhibits: ['kids'],
},
},
score: 10,
start: {
min: 8 * HOUR,
max: 18 * HOUR,
},
},
{
id: 'put kids to bed',
duration: 30 * MIN,
attributes: {
locations: ['home'],
capabilities: {
consumes: ['kids'],
requires: ['kids'],
},
},
score: 1,
},
{
id: 'Read book',
duration: 0.5 * HOUR,
attributes: {
locations: ['home', 'work'],
capabilities: {
perhibits: ['kids'],
},
},
score: 3,
count: 2,
start: {
min: 8 * HOUR,
max: 22 * HOUR,
},
},
{
id: 'Meditate',
duration: 10 * MIN,
score: 1,
attributes: {
locations: ['home', 'work'],
capabilities: {
perhibits: ['kids'],
},
},
start: {
min: 8 * HOUR,
max: 22 * HOUR,
},
},
{
id: 'Meeting 1',
duration: 1 * HOUR,
attributes: {
locations: ['work', 'work'],
capabilities: {
perhibits: ['kids'],
},
},
score: 10,
start: {
min: 10 * HOUR,
max: 10 * HOUR,
},
},
{
id: 'Play playstation',
duration: 1 * HOUR,
attributes: {
locations: ['home'],
capabilities: {
perhibits: ['kids'],
},
},
score: 10,
start: {
min: 16 * HOUR,
max: 24 * HOUR,
},
},
],
});
return result;
} catch (e) {
console.error(e);
throw e;
}
};
createWorker(realistic);

View File

@@ -0,0 +1,3 @@
# Hello
world

View File

@@ -1,12 +0,0 @@
const Foo = () => {
return <div></div>;
};
const worker = () =>
new Worker(new URL('./script.ts', import.meta.url), {
type: 'module',
});
const view = <Foo />;
export { worker, view };

View File

@@ -1,153 +0,0 @@
import { Bob, plugins } from '@bob-the-algorithm/core';
import { createWorker } from '../../features/experiment/worker';
const MIN = 1000 * 60;
const HOUR = 1000 * 60 * 60;
const getTravelTime = async () => 30 * MIN;
const transport = plugins.transport({
getTravelTime,
});
const realistic = async () => {
try {
const bob = new Bob({
plugins: { transport, capabilities: plugins.capabilities() },
});
const result = await bob.run({
context: {
location: 'home',
},
start: 0,
heuristic: ({ completed }) => completed.length >= 3,
planables: [
{
id: `Brush teeth`,
duration: 2 * MIN,
start: {
min: 7 * HOUR,
max: 8 * HOUR,
},
attributes: {
locations: ['home'],
},
score: 1,
},
{
id: 'Drop off kids',
duration: 30 * MIN,
attributes: {
locations: ['daycare'],
},
score: 1,
start: {
min: 7 * HOUR,
max: 9 * HOUR,
},
},
{
id: 'Pickup the kids',
duration: 30 * MIN,
attributes: {
locations: ['daycare'],
},
score: 1,
start: {
min: 15 * HOUR,
max: 15.5 * HOUR,
},
},
{
id: `Eat breakfast`,
duration: 15 * MIN,
start: {
min: 7 * HOUR,
max: 9 * HOUR,
},
attributes: {
locations: ['home'],
},
score: 1,
},
{
id: 'Do work',
duration: 1 * HOUR,
count: 5,
attributes: {
locations: ['work'],
},
score: 10,
start: {
min: 8 * HOUR,
max: 18 * HOUR,
},
},
{
id: 'Read book',
duration: 0.5 * HOUR,
attributes: {
locations: ['home', 'work'],
},
score: 3,
count: 2,
start: {
min: 8 * HOUR,
max: 22 * HOUR,
},
},
{
id: 'Meditate',
duration: 10 * MIN,
score: 1,
attributes: {},
start: {
min: 8 * HOUR,
max: 22 * HOUR,
},
},
{
id: 'Meeting 1',
duration: 1 * HOUR,
attributes: {
locations: ['work', 'work'],
},
score: 10,
start: {
min: 10 * HOUR,
max: 10 * HOUR,
},
},
{
id: 'Meeting 2',
duration: 1 * HOUR,
attributes: {
locations: ['work', 'work'],
},
score: 10,
start: {
min: 12 * HOUR,
max: 12 * HOUR,
},
},
{
id: 'Play playstation',
duration: 1 * HOUR,
attributes: {
locations: ['home'],
},
score: 10,
start: {
min: 16 * HOUR,
max: 24 * HOUR,
},
},
],
});
return result;
} catch (e) {
console.error(e);
throw e;
}
};
createWorker(realistic);

View File

@@ -0,0 +1,204 @@
import { ExperimentInfo } from '../../features/experiment/types';
const MIN = 1000 * 60;
const HOUR = 1000 * 60 * 60;
const info: ExperimentInfo = {
context: {
location: 'home',
capabilities: ['kids'],
},
start: 0,
planables: [
{
id: `Brush teeth`,
duration: 2 * MIN,
start: {
min: 7 * HOUR,
max: 8 * HOUR,
},
attributes: {
locations: ['home'],
},
score: 1,
},
{
id: 'Drop off kids',
duration: 30 * MIN,
attributes: {
locations: ['daycare'],
capabilities: {
requires: ['kids'],
consumes: ['kids'],
},
},
score: 1,
start: {
min: 7 * HOUR,
max: 9 * HOUR,
},
},
{
id: 'put kids to bed',
duration: 30 * MIN,
attributes: {
locations: ['home'],
capabilities: {
consumes: ['kids'],
requires: ['kids'],
},
},
start: {
min: 18.5 * HOUR,
max: 19.5 * HOUR,
},
score: 1,
},
{
id: 'Pickup the kids',
duration: 30 * MIN,
attributes: {
locations: ['daycare'],
capabilities: {
provides: ['kids'],
},
},
score: 1,
start: {
min: 15 * HOUR,
max: 16 * HOUR,
},
},
{
id: `Eat breakfast`,
duration: 15 * MIN,
start: {
min: 7 * HOUR,
max: 9 * HOUR,
},
attributes: {
locations: ['home'],
},
score: 1,
},
{
id: 'Eat dinner',
duration: 60 * MIN,
attributes: {
locations: ['home'],
capabilities: {
requires: ['kids'],
},
},
score: 1,
start: {
min: 17 * HOUR,
max: 22 * HOUR,
},
},
{
id: 'Have lunch',
duration: 30 * MIN,
attributes: {},
score: 1,
start: {
min: 11 * HOUR,
max: 13.5 * HOUR,
},
},
{
id: 'Do work',
duration: 1 * HOUR,
count: 5,
attributes: {
locations: ['work'],
capabilities: {
perhibits: ['kids'],
},
},
score: 10,
start: {
min: 8 * HOUR,
max: 22 * HOUR,
},
},
{
id: 'Read book',
duration: 0.5 * HOUR,
attributes: {
locations: ['home', 'work'],
capabilities: {
perhibits: ['kids'],
},
},
score: 3,
count: 2,
start: {
min: 8 * HOUR,
max: 22 * HOUR,
},
},
{
id: 'Meditate',
duration: 10 * MIN,
score: 1,
attributes: {
locations: ['home', 'work'],
capabilities: {
perhibits: ['kids'],
},
},
start: {
min: 8 * HOUR,
max: 22 * HOUR,
},
},
{
id: 'Meeting 1',
duration: 1 * HOUR,
attributes: {
locations: ['work'],
capabilities: {
perhibits: ['kids'],
},
},
score: 10,
start: {
min: 10 * HOUR,
max: 10 * HOUR,
},
},
{
id: 'Meeting 2',
duration: 1 * HOUR,
attributes: {
locations: ['work'],
capabilities: {
perhibits: ['kids'],
},
},
score: 10,
start: {
min: 12 * HOUR,
max: 12 * HOUR,
},
},
{
id: 'Play playstation',
duration: 1 * HOUR,
attributes: {
locations: ['home'],
capabilities: {
perhibits: ['kids'],
},
},
score: 10,
start: {
min: 16 * HOUR,
max: 24 * HOUR,
},
},
],
};
export { info };

View File

@@ -0,0 +1,9 @@
import { info } from './experiment';
const Foo = () => {
return <div></div>;
};
export default {
info,
view: Foo,
};

View File

@@ -1,4 +1,8 @@
import { CalulationResult, GraphNode } from '@bob-the-algorithm/core'; import {
AllPlugins,
CalulationResult,
GraphNode,
} from '@bob-the-algorithm/core';
import { import {
ReactNode, ReactNode,
createContext, createContext,
@@ -7,9 +11,11 @@ import {
useMemo, useMemo,
useState, useState,
} from 'react'; } from 'react';
import { ExperimentInfo } from './types';
type ExperimentResult = { type ExperimentResult = {
payload: CalulationResult<any>; payload: CalulationResult<AllPlugins>;
duration: number;
}; };
type ExperimentContextValue = { type ExperimentContextValue = {
@@ -22,9 +28,14 @@ type ExperimentContextValue = {
type ExperimentProviderProps = { type ExperimentProviderProps = {
children: ReactNode; children: ReactNode;
worker: () => Worker; experimentInfo: ExperimentInfo;
}; };
const createWorker = () =>
new Worker(new URL('./worker.ts', import.meta.url), {
type: 'module',
});
const ExperimentContext = createContext<ExperimentContextValue>({ const ExperimentContext = createContext<ExperimentContextValue>({
loading: false, loading: false,
selectNode: () => { }, selectNode: () => { },
@@ -32,7 +43,7 @@ const ExperimentContext = createContext<ExperimentContextValue>({
const ExperimentProvider: React.FC<ExperimentProviderProps> = ({ const ExperimentProvider: React.FC<ExperimentProviderProps> = ({
children, children,
worker, experimentInfo,
}) => { }) => {
const [result, setResult] = useState<ExperimentResult>(); const [result, setResult] = useState<ExperimentResult>();
const [error, setError] = useState<any>(); const [error, setError] = useState<any>();
@@ -49,7 +60,7 @@ const ExperimentProvider: React.FC<ExperimentProviderProps> = ({
setLoading(true); setLoading(true);
setError(undefined); setError(undefined);
setResult(undefined); setResult(undefined);
const workerInstance = worker(); const workerInstance = createWorker();
workerInstance.onmessage = (e) => { workerInstance.onmessage = (e) => {
switch (e.data.type) { switch (e.data.type) {
case 'error': { case 'error': {
@@ -69,13 +80,14 @@ const ExperimentProvider: React.FC<ExperimentProviderProps> = ({
}; };
workerInstance.postMessage({ workerInstance.postMessage({
type: 'run', type: 'run',
payload: experimentInfo,
}); });
}; };
run(); run();
return () => { return () => {
workerInstance?.terminate(); workerInstance?.terminate();
}; };
}, [worker]); }, [experimentInfo]);
const value = useMemo( const value = useMemo(
() => ({ () => ({

View File

@@ -1,15 +0,0 @@
import { ReactNode } from 'react';
import { ExperimentProvider } from './context';
type CreateExperimentInput = {
worker: () => Worker;
view: ReactNode;
};
const createExperiment = (input: CreateExperimentInput) => {
return (
<ExperimentProvider worker={input.worker}>{input.view}</ExperimentProvider>
);
};
export { createExperiment };

View File

@@ -6,6 +6,11 @@ const useExperimentResult = () => {
return result?.payload; return result?.payload;
}; };
const useExperimentDuration = () => {
const { result } = useContext(ExperimentContext);
return result?.duration;
};
const useSelectNode = () => { const useSelectNode = () => {
const { selectNode } = useContext(ExperimentContext); const { selectNode } = useContext(ExperimentContext);
return selectNode; return selectNode;
@@ -16,4 +21,9 @@ const useSelectedNode = () => {
return selectedNode; return selectedNode;
}; };
export { useExperimentResult, useSelectNode, useSelectedNode }; export {
useExperimentResult,
useSelectNode,
useSelectedNode,
useExperimentDuration,
};

View File

@@ -1,4 +1,2 @@
export { createWorker } from './worker';
export { ExperimentProvider as Experminent } from './context'; export { ExperimentProvider as Experminent } from './context';
export { createExperiment } from './create';
export { useExperimentResult } from './hooks'; export { useExperimentResult } from './hooks';

View File

@@ -0,0 +1,9 @@
import { AllPlugins, PlanableWithPlugins } from '@bob-the-algorithm/core';
type ExperimentInfo = {
planables: PlanableWithPlugins<AllPlugins>[];
context: any;
start: number;
};
export type { ExperimentInfo };

View File

@@ -1,23 +1,38 @@
type WorkerFn = (...args: any[]) => any; import { Bob, plugins } from '@bob-the-algorithm/core';
import { ExperimentInfo } from './types';
const createWorker = (fn: WorkerFn) => { const MIN = 1000 * 60;
const run = async () => {
const getTravelTime = async () => 30 * MIN;
const transport = plugins.transport({
getTravelTime,
});
const run = async (payload: ExperimentInfo) => {
const startTime = performance.now(); const startTime = performance.now();
try { try {
const result = await fn(); const bob = new Bob({
plugins: { transport, capabilities: plugins.capabilities() },
});
const result = await bob.run({
planables: payload.planables,
context: payload.context,
start: payload.start,
heuristic: ({ completed }) => completed.length >= 3,
});
const endTime = performance.now(); const endTime = performance.now();
const duration = endTime - startTime; const duration = endTime - startTime;
self.postMessage({ type: 'output', payload: result, duration }); self.postMessage({ type: 'output', payload: result, duration });
} catch (error) { } catch (error) {
self.postMessage({ type: 'error', payload: error }); self.postMessage({ type: 'error', payload: error });
console.error(error);
} }
};
self.addEventListener('message', (event) => {
const { type } = event.data;
if (type === 'run') {
run();
}
});
}; };
export { createWorker }; self.addEventListener('message', (event) => {
const { type, payload } = event.data;
if (type === 'run') {
run(payload);
}
});

View File

@@ -1,3 +1,10 @@
@tailwind base; @tailwind base;
@tailwind components; @tailwind components;
@tailwind utilities; @tailwind utilities;
html,
body,
#root,
#root > div {
height: 100%;
}

View File

@@ -1,4 +1,3 @@
import './main.css';
import React from 'react'; import React from 'react';
import ReactDOM from 'react-dom/client'; import ReactDOM from 'react-dom/client';
import { App } from './app.tsx'; import { App } from './app.tsx';

View File

@@ -0,0 +1,53 @@
import { useNavigate } from 'react-router-dom';
import { Card, CardBody, Button } from '@nextui-org/react';
type Experiment = {
title: string;
slug: string;
description: string;
};
const experiments: Experiment[] = [
{
title: 'Capabilities',
slug: 'capabilities',
description: 'Explore the capabilities of Bob.',
},
{
title: 'Packed day',
slug: 'realistic',
description: 'Explore the capabilities of Bob.',
},
];
const Experiments = () => {
const navigate = useNavigate();
return (
<div className="flex flex-col gap-4">
<h1 className="text-2xl font-bold">Experiments</h1>
{experiments.map(({ title, slug, description }) => (
<Card>
<CardBody>
<div className="flex justify-between items-center">
<div className="flex flex-col gap-2">
<p className="text-md">{title}</p>
<p className="text-sm">{description}</p>
</div>
<Button
color="primary"
radius="full"
size="sm"
variant="solid"
onPress={() => navigate(`/experiments/${slug}`)}
>
Show
</Button>
</div>
</CardBody>
</Card>
))}
</div>
);
};
export { Experiments as Page };

View File

@@ -0,0 +1,5 @@
const Experiments = () => {
return <div>Welcome</div>;
};
export { Experiments as Page };

View File

@@ -1,4 +1,4 @@
import { useMemo, useState } from 'react'; import { useMemo } from 'react';
import { GraphCanvas } from 'reagraph'; import { GraphCanvas } from 'reagraph';
import { useExperimentResult } from '../../features/experiment'; import { useExperimentResult } from '../../features/experiment';
import { convertResult } from '../../utils/graph'; import { convertResult } from '../../utils/graph';
@@ -17,7 +17,6 @@ const Graph: React.FC = () => {
} }
return convertResult(data); return convertResult(data);
}, [data]); }, [data]);
const [visualize, setVisualize] = useState(false);
const selectedPath = useMemo(() => { const selectedPath = useMemo(() => {
if (!selectedNode) { if (!selectedNode) {
return []; return [];
@@ -45,10 +44,6 @@ const Graph: React.FC = () => {
return ( return (
<> <>
Nodes count: {output.nodes.length} Nodes count: {output.nodes.length}
<button onClick={() => setVisualize(!visualize)}>
{visualize ? 'Hide' : 'Show'} Visualize
</button>
{visualize && (
<div style={{ position: 'relative', height: '70vh' }}> <div style={{ position: 'relative', height: '70vh' }}>
<GraphCanvas <GraphCanvas
{...output} {...output}
@@ -90,7 +85,6 @@ const Graph: React.FC = () => {
}} }}
/> />
</div> </div>
)}
</> </>
); );
}; };

View File

@@ -1,76 +0,0 @@
import { GraphNode } from '@bob-the-algorithm/core';
import { useMemo } from 'react';
import { ConvertedResult } from '../../utils/graph';
import { format } from 'date-fns';
type PlanProps = {
id: string;
output: ConvertedResult;
};
type NodeProps = {
node: GraphNode;
output: ConvertedResult;
};
const Node = ({ node, output }: NodeProps) => {
const planable = useMemo(() => {
return node.planable
? output.result.planables.find((n) => n.id === node.planable)
: null;
}, [node, output]);
const time = useMemo(() => {
const start = new Date(node.time);
const end = new Date(start.getTime() + node.duration);
return (
<span>
{format(start, 'HH:mm')} - {format(end, 'HH:mm')}
</span>
);
}, [node.duration, node.time]);
if (planable) {
return (
<div>
{time} Planable: {planable!.id}
</div>
);
}
if (node.type === 'travel') {
return (
<div>
{time} Travel: {node.context.location}
</div>
);
}
return null;
};
const Plan: React.FC<PlanProps> = ({ id, output }) => {
const nodes = useMemo(() => {
const result: GraphNode[] = [];
let current = output.result.nodes.find((n) => n.id === id);
while (current) {
result.push(current);
if (!current.parent) {
break;
}
current = output.result.nodes.find((n) => n.id === current?.parent);
}
return result;
}, [id, output]);
return (
<>
{nodes.map((n) => (
<Node key={n.id} node={n} output={output} />
))}
</>
);
};
export { Plan };

View File

@@ -0,0 +1,90 @@
import { GraphNode } from '@bob-the-algorithm/core';
import { Popover, PopoverTrigger, PopoverContent } from '@nextui-org/react';
import { useExperimentResult } from '../../features/experiment';
import { useMemo } from 'react';
import { formatTime } from '../../utils/time';
type PlanProps = {
node: GraphNode;
};
const timespan = 24 * 60 * 60 * 1000;
const randomColor = () => Math.floor(Math.random() * 16777215).toString(16);
const HorizontalPlan: React.FC<PlanProps> = ({ node }) => {
const data = useExperimentResult();
const nodes = useMemo(() => {
if (!data) {
return [];
}
const result: GraphNode[] = [];
let current = node;
if (!current) {
return [];
}
while (current) {
result.push(current);
if (!current.parent) {
break;
}
current = data.nodes.find((n) => n.id === current?.parent)!;
}
return result;
}, [data, node]);
if (!data) {
return null;
}
return (
<div className="w-full min-h-unit-6 rounded-lg bg-gray-900 relative">
{nodes.map((node) => {
const time = (
<span>
{formatTime(node.time)} - {formatTime(node.time + node.duration)}
</span>
);
let title = '';
if (node.planable) {
const planable = data!.planables.find((n) => n.id === node.planable);
title = `Planable: ${planable?.id}`;
}
if (node.type === 'travel') {
title = `Travel: ${node.context.location}`;
}
return (
<div
className="absolute top-0 bottom-0 h-full left-0 right-0 p-[2px] flex items-stretch justify-stretch"
style={{
left: `${(node.time / timespan) * 100}%`,
width: `${(node.duration / timespan) * 100}%`,
}}
>
<Popover placement="bottom" showArrow>
<PopoverTrigger>
<div
className="flex-1 rounded-[2px]"
style={{
left: `${(node.time / timespan) * 100}%`,
width: `${(node.duration / timespan) * 100}%`,
backgroundColor: `#${randomColor()}`,
}}
/>
</PopoverTrigger>
<PopoverContent>
<div className="px-1 py-2">
<div className="text-small font-bold">{title}</div>
<div className="text-tiny">{time}</div>
</div>
</PopoverContent>
</Popover>
</div>
);
})}
</div>
);
};
export { HorizontalPlan };

View File

@@ -1,60 +1,18 @@
import { GraphNode } from '@bob-the-algorithm/core'; import { GraphNode } from '@bob-the-algorithm/core';
import { useMemo } from 'react'; import { useMemo } from 'react';
import { convertResult } from '../../utils/graph'; import { Listbox, ListboxItem, cn } from '@nextui-org/react';
import { format } from 'date-fns';
import { useExperimentResult } from '../../features/experiment'; import { useExperimentResult } from '../../features/experiment';
import { useSelectNode } from '../../features/experiment/hooks'; import { formatTime } from '../../utils/time';
type PlanProps = { type PlanProps = {
node: GraphNode; node: GraphNode;
onSelect: (node: GraphNode) => void;
}; };
type NodeProps = { const Plan: React.FC<PlanProps> = ({ node, onSelect }) => {
node: GraphNode;
};
const Node = ({ node }: NodeProps) => {
const selectNode = useSelectNode();
const data = useExperimentResult(); const data = useExperimentResult();
const planable = useMemo(() => {
return node.planable
? data?.planables.find((n) => n.id === node.planable)
: null;
}, [node, data]);
const time = useMemo(() => {
const start = new Date(node.time);
const end = new Date(start.getTime() + node.duration);
return (
<span>
{format(start, 'HH:mm')} - {format(end, 'HH:mm')}
</span>
);
}, [node.duration, node.time]);
if (planable) {
return (
<div onClick={() => selectNode(node)}>
{time} Planable: {planable!.id}
</div>
);
}
if (node.type === 'travel') {
return (
<div onClick={() => selectNode(node)}>
{time} Travel: {node.context.location}
</div>
);
}
return null;
};
const Plan: React.FC<PlanProps> = ({ node }) => {
const data = useExperimentResult();
const output = useMemo(() => (data ? convertResult(data) : null), [data]);
const nodes = useMemo(() => { const nodes = useMemo(() => {
if (!output) { if (!data) {
return []; return [];
} }
const result: GraphNode[] = []; const result: GraphNode[] = [];
@@ -68,22 +26,48 @@ const Plan: React.FC<PlanProps> = ({ node }) => {
if (!current.parent) { if (!current.parent) {
break; break;
} }
current = output.result.nodes.find((n) => n.id === current?.parent)!; current = data.nodes.find((n) => n.id === current?.parent)!;
} }
return result; return result.reverse();
}, [output, node]); }, [data, node]);
if (!output) { if (!data) {
return null; return null;
} }
return ( return (
<> <Listbox
{nodes.map((n) => ( variant="flat"
<Node key={n.id} node={n} /> aria-label="Listbox menu with descriptions"
))} items={nodes}
</> >
{(node) => {
const time = (
<span>
{formatTime(node.time)} - {formatTime(node.time + node.duration)}
</span>
);
let title = '';
if (node.planable) {
const planable = data!.planables.find((n) => n.id === node.planable);
title = `Planable: ${planable?.id}`;
}
if (node.type === 'travel') {
title = `Travel: ${node.context.location}`;
}
return (
<ListboxItem
key={node.id}
className={cn({ selected: node.id === node.id })}
onClick={() => onSelect(node)}
description={time}
>
{title}
</ListboxItem>
);
}}
</Listbox>
); );
}; };

View File

@@ -0,0 +1,18 @@
import { createHashRouter, RouterProvider } from 'react-router-dom';
import { pages } from '../containers/page';
import { experiments } from '../containers/experiment';
const router = createHashRouter([
{
path: '/',
children: pages,
},
{
path: '/experiments',
children: experiments,
},
]);
const Router = () => <RouterProvider router={router} />;
export { Router };

View File

@@ -1,8 +0,0 @@
const imports = import.meta.glob('../experiments/*/index.tsx');
const experiments = Object.entries(imports).map(([path, loader]) => {
const slug = path.replace('./experiments/', '').replace('/index.tsx', '');
return { slug, loader };
});
export { experiments };

View File

@@ -1,4 +1,4 @@
import { CalulationResult } from '@bob-the-algorithm/core'; import { AllPlugins, CalulationResult } from '@bob-the-algorithm/core';
function msToHMS(ms: number) { function msToHMS(ms: number) {
// 1- Convert to seconds: // 1- Convert to seconds:
@@ -12,13 +12,13 @@ function msToHMS(ms: number) {
seconds = seconds % 60; seconds = seconds % 60;
return hours + ':' + minutes + ':' + seconds; return hours + ':' + minutes + ':' + seconds;
} }
const convertResult = (result: CalulationResult<any>) => { const convertResult = (result: CalulationResult<AllPlugins>) => {
const nodes = result.nodes.map((node) => { const nodes = result.nodes.map((node) => {
let label = `root (${node.location})`; let label = `root (${node.context.location})`;
if (node.type === 'planable') { if (node.type === 'planable') {
label = `task: ${node.planable!.toString()}`; label = `task: ${node.planable!.toString()}`;
} else if (node.type === 'travel') { } else if (node.type === 'travel') {
label = `travel->${node.location}`; label = `travel->${node.context.location}`;
} }
return { return {
id: node.id, id: node.id,

View File

@@ -0,0 +1,6 @@
const formatTime = (time: number) => {
const toUtc = new Date(time).toUTCString();
return toUtc.slice(17, 22);
};
export { formatTime };

View File

@@ -1,11 +1,15 @@
/** @type {import('tailwindcss').Config} */ /** @type {import('tailwindcss').Config} */
const path = require('path');
const { nextui } = require('@nextui-org/react'); const { nextui } = require('@nextui-org/react');
const themePath = require.resolve('@nextui-org/theme/package.json');
const themeDir = path.dirname(themePath);
export default { export default {
content: [ content: [
'./index.html', './index.html',
'./src/**/*.{js,ts,jsx,tsx}', './src/**/*.{js,ts,jsx,tsx}',
'./node_modules/@nextui-org/theme/dist/**/*.{js,ts,jsx,tsx}', `${themeDir}/dist/**/*.{js,ts,jsx,tsx}`,
], ],
theme: { theme: {
extend: {}, extend: {},

File diff suppressed because one or more lines are too long

View File

@@ -1,2 +0,0 @@
declare const _default: import("vite").UserConfig;
export default _default;

View File

@@ -1,7 +1,9 @@
import { defineConfig } from 'vite'; import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react-swc'; import react from '@vitejs/plugin-react-swc';
import mdx from '@mdx-js/rollup'; import mdx from '@mdx-js/rollup';
var ASSET_URL = process.env.ASSET_URL || '';
const ASSET_URL = process.env.ASSET_URL || '';
// https://vitejs.dev/config/ // https://vitejs.dev/config/
export default defineConfig({ export default defineConfig({
base: ASSET_URL, base: ASSET_URL,

View File

@@ -1,11 +0,0 @@
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react-swc';
import mdx from '@mdx-js/rollup';
const ASSET_URL = process.env.ASSET_URL || '';
// https://vitejs.dev/config/
export default defineConfig({
base: ASSET_URL,
plugins: [mdx(), react()],
});

78
pnpm-lock.yaml generated
View File

@@ -69,6 +69,12 @@ importers:
react-dom: react-dom:
specifier: ^18.2.0 specifier: ^18.2.0
version: 18.2.0(react@18.2.0) version: 18.2.0(react@18.2.0)
react-json-tree:
specifier: ^0.18.0
version: 0.18.0(@types/react@18.2.15)(react@18.2.0)
react-router-dom:
specifier: ^6.16.0
version: 6.16.0(react-dom@18.2.0)(react@18.2.0)
tailwindcss: tailwindcss:
specifier: ^3.3.3 specifier: ^3.3.3
version: 3.3.3(ts-node@10.9.1) version: 3.3.3(ts-node@10.9.1)
@@ -3168,6 +3174,11 @@ packages:
react: 18.2.0 react: 18.2.0
dev: false dev: false
/@remix-run/router@1.9.0:
resolution: {integrity: sha512-bV63itrKBC0zdT27qYm6SDZHlkXwFL1xMBuhkn+X7l0+IIhNaH5wuuvZKp6eKhCD4KFhujhfhCT1YxXW6esUIA==}
engines: {node: '>=14.0.0'}
dev: false
/@rollup/pluginutils@5.0.4(rollup@3.28.1): /@rollup/pluginutils@5.0.4(rollup@3.28.1):
resolution: {integrity: sha512-0KJnIoRI8A+a1dqOYLxH8vBf8bphDmty5QvIm2hqm7oFCFYKCAZWWd2hXgMibaPsNDhI0AtpYfQZJG47pt/k4g==} resolution: {integrity: sha512-0KJnIoRI8A+a1dqOYLxH8vBf8bphDmty5QvIm2hqm7oFCFYKCAZWWd2hXgMibaPsNDhI0AtpYfQZJG47pt/k4g==}
engines: {node: '>=14.0.0'} engines: {node: '>=14.0.0'}
@@ -3350,6 +3361,10 @@ packages:
'@types/estree': 1.0.1 '@types/estree': 1.0.1
dev: true dev: true
/@types/base16@1.0.3:
resolution: {integrity: sha512-rjrIWFr73ylMjEQuU1OQjkoIDcLR2/dIwiopZe2S5ASo5eoSYBxaAnGtwTUhWc5oWefQXxHRFmGDelYR5yMcgA==}
dev: false
/@types/color-convert@2.0.1: /@types/color-convert@2.0.1:
resolution: {integrity: sha512-GwXanrvq/tBHJtudbl1lSy9Ybt7KS9+rA+YY3bcuIIM+d6jSHUr+5yjO83gtiRpuaPiBccwFjSnAK2qSrIPA7w==} resolution: {integrity: sha512-GwXanrvq/tBHJtudbl1lSy9Ybt7KS9+rA+YY3bcuIIM+d6jSHUr+5yjO83gtiRpuaPiBccwFjSnAK2qSrIPA7w==}
dependencies: dependencies:
@@ -4018,6 +4033,10 @@ packages:
/balanced-match@1.0.2: /balanced-match@1.0.2:
resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==}
/base16@1.0.0:
resolution: {integrity: sha512-pNdYkNPiJUnEhnfXV56+sQy8+AaPcG3POZAUnwr4EeqCUZFz4u2PePbo3e5Gj4ziYPCWGUZT9RHisvJKnwFuBQ==}
dev: false
/better-path-resolve@1.0.0: /better-path-resolve@1.0.0:
resolution: {integrity: sha512-pbnl5XzGBdrFU/wT4jqmJVPn2B6UHPBOhzMQkY/SPUPB6QtUXtmBHBIwCbXJol93mOpGMnQyP/+BB19q04xj7g==} resolution: {integrity: sha512-pbnl5XzGBdrFU/wT4jqmJVPn2B6UHPBOhzMQkY/SPUPB6QtUXtmBHBIwCbXJol93mOpGMnQyP/+BB19q04xj7g==}
engines: {node: '>=4'} engines: {node: '>=4'}
@@ -4240,6 +4259,13 @@ packages:
resolution: {integrity: sha512-kJhwH5nAwb34tmyuqq/lgjEKzlFXn1U99NlnB6Ws4qVaERcRUYeYP1cBw6BJ4vxaWStAUEef4WMr7WjOCnBt8w==} resolution: {integrity: sha512-kJhwH5nAwb34tmyuqq/lgjEKzlFXn1U99NlnB6Ws4qVaERcRUYeYP1cBw6BJ4vxaWStAUEef4WMr7WjOCnBt8w==}
dev: false dev: false
/color@3.2.1:
resolution: {integrity: sha512-aBl7dZI9ENN6fUGC7mWpMTPNHmWUSNan9tuWN6ahh5ZLNk9baLJOnSMlrQkHcrfFgz2/RigjUVAjdx36VcemKA==}
dependencies:
color-convert: 1.9.3
color-string: 1.9.1
dev: false
/color@4.2.3: /color@4.2.3:
resolution: {integrity: sha512-1rXeuUUiGGrykh+CeBdu5Ie7OJwinCgQY0bc7GCRxy5xVHy+moaqkpL/jqQq0MtQOeYcrqEz4abc5f0KtU7W4A==} resolution: {integrity: sha512-1rXeuUUiGGrykh+CeBdu5Ie7OJwinCgQY0bc7GCRxy5xVHy+moaqkpL/jqQq0MtQOeYcrqEz4abc5f0KtU7W4A==}
engines: {node: '>=12.5.0'} engines: {node: '>=12.5.0'}
@@ -5856,6 +5882,10 @@ packages:
resolution: {integrity: sha512-HvzRFWjtcguTW7yd8NJBshuNaCa8aqNFtnswdT7f/cMd/1YKy5Zzoq4W/Oxvnx9l7aeY258uSdDfM793+eLsVg==} resolution: {integrity: sha512-HvzRFWjtcguTW7yd8NJBshuNaCa8aqNFtnswdT7f/cMd/1YKy5Zzoq4W/Oxvnx9l7aeY258uSdDfM793+eLsVg==}
dev: true dev: true
/lodash.curry@4.1.1:
resolution: {integrity: sha512-/u14pXGviLaweY5JI0IUzgzF2J6Ne8INyzAZjImcryjgkZ+ebruBxy2/JaOOkTqScddcYtakjhSaeemV8lR0tA==}
dev: false
/lodash.foreach@4.5.0: /lodash.foreach@4.5.0:
resolution: {integrity: sha512-aEXTF4d+m05rVOAUG3z4vZZ4xVexLKZGF0lIxuHZ1Hplpk/3B6Z1+/ICICYRLm7c41Z2xiejbkCkJoTlypoXhQ==} resolution: {integrity: sha512-aEXTF4d+m05rVOAUG3z4vZZ4xVexLKZGF0lIxuHZ1Hplpk/3B6Z1+/ICICYRLm7c41Z2xiejbkCkJoTlypoXhQ==}
dev: false dev: false
@@ -6814,6 +6844,18 @@ packages:
engines: {node: '>=8'} engines: {node: '>=8'}
dev: false dev: false
/react-base16-styling@0.9.1:
resolution: {integrity: sha512-1s0CY1zRBOQ5M3T61wetEpvQmsYSNtWEcdYzyZNxKa8t7oDvaOn9d21xrGezGAHFWLM7SHcktPuPTrvoqxSfKw==}
dependencies:
'@babel/runtime': 7.22.15
'@types/base16': 1.0.3
'@types/lodash': 4.14.199
base16: 1.0.0
color: 3.2.1
csstype: 3.1.2
lodash.curry: 4.1.1
dev: false
/react-composer@5.0.3(react@18.2.0): /react-composer@5.0.3(react@18.2.0):
resolution: {integrity: sha512-1uWd07EME6XZvMfapwZmc7NgCZqDemcvicRi3wMJzXsQLvZ3L7fTHVyPy1bZdnWXM4iPjYuNE+uJ41MLKeTtnA==} resolution: {integrity: sha512-1uWd07EME6XZvMfapwZmc7NgCZqDemcvicRi3wMJzXsQLvZ3L7fTHVyPy1bZdnWXM4iPjYuNE+uJ41MLKeTtnA==}
peerDependencies: peerDependencies:
@@ -6835,6 +6877,19 @@ packages:
/react-is@16.13.1: /react-is@16.13.1:
resolution: {integrity: sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==} resolution: {integrity: sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==}
/react-json-tree@0.18.0(@types/react@18.2.15)(react@18.2.0):
resolution: {integrity: sha512-Qe6HKSXrr++n9Y31nkRJ3XvQMATISpqigH1vEKhLwB56+nk5thTP0ITThpjxY6ZG/ubpVq/aEHIcyLP/OPHxeA==}
peerDependencies:
'@types/react': ^16.8.0 || ^17.0.0 || ^18.0.0
react: ^16.8.0 || ^17.0.0 || ^18.0.0
dependencies:
'@babel/runtime': 7.22.15
'@types/lodash': 4.14.199
'@types/react': 18.2.15
react: 18.2.0
react-base16-styling: 0.9.1
dev: false
/react-merge-refs@1.1.0: /react-merge-refs@1.1.0:
resolution: {integrity: sha512-alTKsjEL0dKH/ru1Iyn7vliS2QRcBp9zZPGoWxUOvRGWPUYgjo+V01is7p04It6KhgrzhJGnIj9GgX8W4bZoCQ==} resolution: {integrity: sha512-alTKsjEL0dKH/ru1Iyn7vliS2QRcBp9zZPGoWxUOvRGWPUYgjo+V01is7p04It6KhgrzhJGnIj9GgX8W4bZoCQ==}
dev: true dev: true
@@ -6885,6 +6940,29 @@ packages:
use-sidecar: 1.1.2(@types/react@18.2.15)(react@18.2.0) use-sidecar: 1.1.2(@types/react@18.2.15)(react@18.2.0)
dev: false dev: false
/react-router-dom@6.16.0(react-dom@18.2.0)(react@18.2.0):
resolution: {integrity: sha512-aTfBLv3mk/gaKLxgRDUPbPw+s4Y/O+ma3rEN1u8EgEpLpPe6gNjIsWt9rxushMHHMb7mSwxRGdGlGdvmFsyPIg==}
engines: {node: '>=14.0.0'}
peerDependencies:
react: '>=16.8'
react-dom: '>=16.8'
dependencies:
'@remix-run/router': 1.9.0
react: 18.2.0
react-dom: 18.2.0(react@18.2.0)
react-router: 6.16.0(react@18.2.0)
dev: false
/react-router@6.16.0(react@18.2.0):
resolution: {integrity: sha512-VT4Mmc4jj5YyjpOi5jOf0I+TYzGpvzERy4ckNSvSh2RArv8LLoCxlsZ2D+tc7zgjxcY34oTz2hZaeX5RVprKqA==}
engines: {node: '>=14.0.0'}
peerDependencies:
react: '>=16.8'
dependencies:
'@remix-run/router': 1.9.0
react: 18.2.0
dev: false
/react-style-singleton@2.2.1(@types/react@18.2.15)(react@18.2.0): /react-style-singleton@2.2.1(@types/react@18.2.15)(react@18.2.0):
resolution: {integrity: sha512-ZWj0fHEMyWkHzKYUr2Bs/4zU6XLmq9HsgBURm7g5pAVfyn49DgUiNgY2d4lXRlYSiCif9YBGpQleewkcqddc7g==} resolution: {integrity: sha512-ZWj0fHEMyWkHzKYUr2Bs/4zU6XLmq9HsgBURm7g5pAVfyn49DgUiNgY2d4lXRlYSiCif9YBGpQleewkcqddc7g==}
engines: {node: '>=10'} engines: {node: '>=10'}