mirror of
https://github.com/morten-olsen/bob.git
synced 2026-02-08 01:46:29 +01:00
feat: add capabilities
This commit is contained in:
1
.npmrc
1
.npmrc
@@ -1,3 +1,4 @@
|
|||||||
node-linker=hoisted
|
node-linker=hoisted
|
||||||
|
public-hoist-pattern[]=*@nextui-org/*
|
||||||
store-dir=.pnpm-store
|
store-dir=.pnpm-store
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
|
|
||||||
> bob-the-algorithm@ build /Users/alice/work/private/bob/packages/algorithm
|
> @bob-the-algorithm/core@0.1.8 build /home/alice/Git/bob/packages/algorithm
|
||||||
> tsc --build configs/tsconfig.libs.json
|
> tsc --build configs/tsconfig.libs.json
|
||||||
|
|
||||||
|
|||||||
@@ -21,6 +21,10 @@ type RunOptions<TPlugins extends Plugins> = {
|
|||||||
heuristic?: (result: any) => boolean;
|
heuristic?: (result: any) => boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
type PlanableWithPlugins<TPlugins extends Plugins> = Planable<
|
||||||
|
PluginAttributes<TPlugins>
|
||||||
|
>;
|
||||||
|
|
||||||
class Bob<TPlugins extends Plugins> {
|
class Bob<TPlugins extends Plugins> {
|
||||||
#options: CalulationOptions<TPlugins>;
|
#options: CalulationOptions<TPlugins>;
|
||||||
#id: number = 0;
|
#id: number = 0;
|
||||||
@@ -99,5 +103,5 @@ class Bob<TPlugins extends Plugins> {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export type { CalulationOptions, CalulationResult };
|
export type { CalulationOptions, CalulationResult, PlanableWithPlugins };
|
||||||
export { Bob };
|
export { Bob };
|
||||||
|
|||||||
@@ -70,7 +70,7 @@ const expandNode = async <
|
|||||||
node.time + node.duration,
|
node.time + node.duration,
|
||||||
planable.start?.min || 0,
|
planable.start?.min || 0,
|
||||||
);
|
);
|
||||||
return {
|
const nextNode = {
|
||||||
...node,
|
...node,
|
||||||
type: 'planable',
|
type: 'planable',
|
||||||
exploreId: 0,
|
exploreId: 0,
|
||||||
@@ -83,6 +83,14 @@ const expandNode = async <
|
|||||||
completed: remaining.length === 0,
|
completed: remaining.length === 0,
|
||||||
parent: node.id,
|
parent: node.id,
|
||||||
};
|
};
|
||||||
|
return Object.values(plugins).reduce(
|
||||||
|
// TODO: remove any
|
||||||
|
(acc, plugin) =>
|
||||||
|
(plugin.mutateNode
|
||||||
|
? plugin.mutateNode(acc as any, planable)
|
||||||
|
: acc) as any,
|
||||||
|
nextNode,
|
||||||
|
) as any;
|
||||||
});
|
});
|
||||||
|
|
||||||
return [...planableNodes, ...metaNodes.flat()];
|
return [...planableNodes, ...metaNodes.flat()];
|
||||||
|
|||||||
@@ -1,5 +1,8 @@
|
|||||||
export type { GraphNode } from './types/node';
|
export type { GraphNode } from './types/node';
|
||||||
export type { Planable } from './types/planable';
|
export type { Planable } from './types/planable';
|
||||||
export { expandNode } from './algorithm/expand-node';
|
export {
|
||||||
export { Bob, type CalulationResult } from './algorithm/calulation';
|
Bob,
|
||||||
|
type CalulationResult,
|
||||||
|
type PlanableWithPlugins,
|
||||||
|
} from './algorithm/calulation';
|
||||||
export { plugins } from './plugins/index';
|
export { plugins } from './plugins/index';
|
||||||
|
|||||||
57
packages/algorithm/src/plugins/capabilities.ts
Normal file
57
packages/algorithm/src/plugins/capabilities.ts
Normal file
@@ -0,0 +1,57 @@
|
|||||||
|
import { Type } from '@sinclair/typebox';
|
||||||
|
import { createPlugin } from './create';
|
||||||
|
|
||||||
|
const capabilities = () =>
|
||||||
|
createPlugin(
|
||||||
|
Type.Object({
|
||||||
|
capabilities: Type.Optional(
|
||||||
|
Type.Object({
|
||||||
|
provides: Type.Optional(Type.Array(Type.String())),
|
||||||
|
consumes: Type.Optional(Type.Array(Type.String())),
|
||||||
|
requires: Type.Optional(Type.Array(Type.String())),
|
||||||
|
perhibits: Type.Optional(Type.Array(Type.String())),
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
}),
|
||||||
|
Type.Object({
|
||||||
|
capabilities: Type.Array(Type.String()),
|
||||||
|
}),
|
||||||
|
{
|
||||||
|
isPlanable: (node, planable) => {
|
||||||
|
const { requires = [], perhibits = [] } =
|
||||||
|
planable.attributes?.capabilities || {};
|
||||||
|
const capabilities = node.context.capabilities;
|
||||||
|
|
||||||
|
if (requires.length === 0 && perhibits.length === 0) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
const satisfiesRequire = requires.every((c) =>
|
||||||
|
capabilities.includes(c),
|
||||||
|
);
|
||||||
|
const satisfiesPerhibit = !perhibits.some((c) =>
|
||||||
|
capabilities.includes(c),
|
||||||
|
);
|
||||||
|
return satisfiesRequire && satisfiesPerhibit;
|
||||||
|
},
|
||||||
|
mutateNode: (node, planable) => {
|
||||||
|
const { provides = [], consumes = [] } =
|
||||||
|
planable.attributes?.capabilities || {};
|
||||||
|
const capabilities = node.context.capabilities || [];
|
||||||
|
|
||||||
|
const newCapabilities = [
|
||||||
|
...capabilities.filter((c) => !consumes.includes(c)),
|
||||||
|
...provides,
|
||||||
|
];
|
||||||
|
|
||||||
|
return {
|
||||||
|
...node,
|
||||||
|
context: {
|
||||||
|
...node.context,
|
||||||
|
capabilities: newCapabilities,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
},
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
export { capabilities };
|
||||||
@@ -1,8 +1,10 @@
|
|||||||
import { Plugin } from '../types/plugin';
|
import { Plugin } from '../types/plugin';
|
||||||
import { transport } from './transport';
|
import { transport } from './transport';
|
||||||
|
import { capabilities } from './capabilities';
|
||||||
|
|
||||||
const plugins = {
|
const plugins = {
|
||||||
transport,
|
transport,
|
||||||
|
capabilities,
|
||||||
} satisfies Record<string, (...args: any[]) => Plugin>;
|
} satisfies Record<string, (...args: any[]) => Plugin>;
|
||||||
|
|
||||||
export { plugins };
|
export { plugins };
|
||||||
|
|||||||
@@ -47,7 +47,7 @@ const transport = ({ getTravelTime }: TransportOptions) =>
|
|||||||
planable: undefined,
|
planable: undefined,
|
||||||
location,
|
location,
|
||||||
exploreId: 0,
|
exploreId: 0,
|
||||||
score: node.score - 10,
|
score: node.score - 5,
|
||||||
time: node.time + node.duration,
|
time: node.time + node.duration,
|
||||||
duration: travelTime,
|
duration: travelTime,
|
||||||
parent: node.id,
|
parent: node.id,
|
||||||
|
|||||||
@@ -12,6 +12,10 @@ type Plugin<TAttributes = any, TContext = any> = {
|
|||||||
node: GraphNode<TAttributes, TContext>,
|
node: GraphNode<TAttributes, TContext>,
|
||||||
planable: Planable<TAttributes>,
|
planable: Planable<TAttributes>,
|
||||||
) => boolean;
|
) => boolean;
|
||||||
|
mutateNode?: (
|
||||||
|
node: GraphNode<TAttributes, TContext>,
|
||||||
|
planable: Planable<TAttributes>,
|
||||||
|
) => GraphNode<TAttributes, TContext>;
|
||||||
};
|
};
|
||||||
|
|
||||||
type Plugins = Record<string, Plugin>;
|
type Plugins = Record<string, Plugin>;
|
||||||
|
|||||||
@@ -11,9 +11,12 @@
|
|||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@bob-the-algorithm/core": "workspace:^",
|
"@bob-the-algorithm/core": "workspace:^",
|
||||||
|
"@nextui-org/react": "^2.1.13",
|
||||||
"date-fns": "^2.30.0",
|
"date-fns": "^2.30.0",
|
||||||
|
"framer-motion": "^10.16.4",
|
||||||
"react": "^18.2.0",
|
"react": "^18.2.0",
|
||||||
"react-dom": "^18.2.0"
|
"react-dom": "^18.2.0",
|
||||||
|
"tailwindcss": "^3.3.3"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@mdx-js/rollup": "^2.3.0",
|
"@mdx-js/rollup": "^2.3.0",
|
||||||
@@ -23,9 +26,11 @@
|
|||||||
"@typescript-eslint/eslint-plugin": "^6.0.0",
|
"@typescript-eslint/eslint-plugin": "^6.0.0",
|
||||||
"@typescript-eslint/parser": "^6.0.0",
|
"@typescript-eslint/parser": "^6.0.0",
|
||||||
"@vitejs/plugin-react-swc": "^3.3.2",
|
"@vitejs/plugin-react-swc": "^3.3.2",
|
||||||
|
"autoprefixer": "^10.4.16",
|
||||||
"eslint": "^8.45.0",
|
"eslint": "^8.45.0",
|
||||||
"eslint-plugin-react-hooks": "^4.6.0",
|
"eslint-plugin-react-hooks": "^4.6.0",
|
||||||
"eslint-plugin-react-refresh": "^0.4.3",
|
"eslint-plugin-react-refresh": "^0.4.3",
|
||||||
|
"postcss": "^8.4.30",
|
||||||
"reagraph": "^4.13.0",
|
"reagraph": "^4.13.0",
|
||||||
"typescript": "^5.0.2",
|
"typescript": "^5.0.2",
|
||||||
"vite": "^4.4.5"
|
"vite": "^4.4.5"
|
||||||
|
|||||||
6
packages/playground/postcss.config.js
Normal file
6
packages/playground/postcss.config.js
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
export default {
|
||||||
|
plugins: {
|
||||||
|
tailwindcss: {},
|
||||||
|
autoprefixer: {},
|
||||||
|
},
|
||||||
|
}
|
||||||
@@ -1,7 +1,12 @@
|
|||||||
import { Page } from './containers/page';
|
import { NextUIProvider } from '@nextui-org/react';
|
||||||
|
import { Page } from './containers/experiment';
|
||||||
|
|
||||||
const App: React.FC = () => {
|
const App: React.FC = () => {
|
||||||
return <Page slug=".hello" />;
|
return (
|
||||||
|
<NextUIProvider>
|
||||||
|
<Page slug=".capabilities" />
|
||||||
|
</NextUIProvider>
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export { App };
|
export { App };
|
||||||
|
|||||||
@@ -1,28 +1,34 @@
|
|||||||
import { useEffect, useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
import { pages } from '../utils/pages';
|
import { experiments } from '../../utils/experiments';
|
||||||
import { RunnerProvider } from '../features/runner';
|
import { ExperimentProvider } from '../../features/experiment/context';
|
||||||
|
import { ExperimentView } from './view';
|
||||||
|
|
||||||
type PageProps = {
|
type PageProps = {
|
||||||
slug: string;
|
slug: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
type Experiment = {
|
||||||
|
worker: () => Worker;
|
||||||
|
view: React.ReactElement;
|
||||||
|
};
|
||||||
|
|
||||||
const Page: React.FC<PageProps> = ({ slug }) => {
|
const Page: React.FC<PageProps> = ({ slug }) => {
|
||||||
const [Component, setComponent] = useState<React.FC>();
|
|
||||||
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>();
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
setError(undefined);
|
setError(undefined);
|
||||||
const load = async () => {
|
const load = async () => {
|
||||||
try {
|
try {
|
||||||
const page = pages.find((page) => page.slug === slug);
|
const page = experiments.find((page) => page.slug === slug);
|
||||||
if (!page) {
|
if (!page) {
|
||||||
throw new Error(`Page not found: ${slug}`);
|
throw new Error(`Page not found: ${slug}`);
|
||||||
}
|
}
|
||||||
const { default: Component } = (await page.loader()) as {
|
const next = (await page.loader()) as Experiment;
|
||||||
default: React.FC;
|
console.log('n', next);
|
||||||
};
|
setExperiment(next);
|
||||||
setComponent(() => Component);
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
setError(err);
|
setError(err);
|
||||||
} finally {
|
} finally {
|
||||||
@@ -37,14 +43,14 @@ const Page: React.FC<PageProps> = ({ slug }) => {
|
|||||||
return <div>Error: {error.toString()}</div>;
|
return <div>Error: {error.toString()}</div>;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (loading || !Component) {
|
if (loading || !experiment) {
|
||||||
return <div>Loading...</div>;
|
return <div>Loading...</div>;
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<RunnerProvider>
|
<ExperimentProvider worker={experiment.worker}>
|
||||||
<Component />
|
<ExperimentView>{experiment.view}</ExperimentView>
|
||||||
</RunnerProvider>
|
</ExperimentProvider>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
26
packages/playground/src/containers/experiment/node.tsx
Normal file
26
packages/playground/src/containers/experiment/node.tsx
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
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 };
|
||||||
23
packages/playground/src/containers/experiment/nodes.tsx
Normal file
23
packages/playground/src/containers/experiment/nodes.tsx
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
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 };
|
||||||
19
packages/playground/src/containers/experiment/view.tsx
Normal file
19
packages/playground/src/containers/experiment/view.tsx
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
import { Graph } from '../../presenters/graph';
|
||||||
|
import { NodeView } from './node';
|
||||||
|
import { NodesView } from './nodes';
|
||||||
|
|
||||||
|
type ExperimentViewProps = {
|
||||||
|
children: React.ReactNode;
|
||||||
|
};
|
||||||
|
|
||||||
|
const ExperimentView: React.FC<ExperimentViewProps> = ({ children }) => {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Graph />
|
||||||
|
<NodesView />
|
||||||
|
<NodeView />
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export { ExperimentView };
|
||||||
12
packages/playground/src/experiments/capabilities/index.tsx
Normal file
12
packages/playground/src/experiments/capabilities/index.tsx
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
const Foo = () => {
|
||||||
|
return <div></div>;
|
||||||
|
};
|
||||||
|
|
||||||
|
const worker = () =>
|
||||||
|
new Worker(new URL('./script.ts', import.meta.url), {
|
||||||
|
type: 'module',
|
||||||
|
});
|
||||||
|
|
||||||
|
const view = <Foo />;
|
||||||
|
|
||||||
|
export { worker, view };
|
||||||
181
packages/playground/src/experiments/capabilities/script.ts
Normal file
181
packages/playground/src/experiments/capabilities/script.ts
Normal file
@@ -0,0 +1,181 @@
|
|||||||
|
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);
|
||||||
12
packages/playground/src/experiments/hello/index.tsx
Normal file
12
packages/playground/src/experiments/hello/index.tsx
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
const Foo = () => {
|
||||||
|
return <div></div>;
|
||||||
|
};
|
||||||
|
|
||||||
|
const worker = () =>
|
||||||
|
new Worker(new URL('./script.ts', import.meta.url), {
|
||||||
|
type: 'module',
|
||||||
|
});
|
||||||
|
|
||||||
|
const view = <Foo />;
|
||||||
|
|
||||||
|
export { worker, view };
|
||||||
@@ -1,6 +1,5 @@
|
|||||||
import { Bob, plugins } from '@bob-the-algorithm/core';
|
import { Bob, plugins } from '@bob-the-algorithm/core';
|
||||||
import { createWorker } from '../../features/runner/worker';
|
import { createWorker } from '../../features/experiment/worker';
|
||||||
import { convertResult } from '../../utils/graph';
|
|
||||||
|
|
||||||
const MIN = 1000 * 60;
|
const MIN = 1000 * 60;
|
||||||
const HOUR = 1000 * 60 * 60;
|
const HOUR = 1000 * 60 * 60;
|
||||||
@@ -13,7 +12,7 @@ const transport = plugins.transport({
|
|||||||
const realistic = async () => {
|
const realistic = async () => {
|
||||||
try {
|
try {
|
||||||
const bob = new Bob({
|
const bob = new Bob({
|
||||||
plugins: { transport },
|
plugins: { transport, capabilities: plugins.capabilities() },
|
||||||
});
|
});
|
||||||
const result = await bob.run({
|
const result = await bob.run({
|
||||||
context: {
|
context: {
|
||||||
@@ -144,13 +143,11 @@ const realistic = async () => {
|
|||||||
},
|
},
|
||||||
],
|
],
|
||||||
});
|
});
|
||||||
return convertResult(result);
|
return result;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error(e);
|
console.error(e);
|
||||||
throw e;
|
throw e;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
createWorker({
|
createWorker(realistic);
|
||||||
realistic,
|
|
||||||
});
|
|
||||||
99
packages/playground/src/features/experiment/context.tsx
Normal file
99
packages/playground/src/features/experiment/context.tsx
Normal file
@@ -0,0 +1,99 @@
|
|||||||
|
import { CalulationResult, GraphNode } from '@bob-the-algorithm/core';
|
||||||
|
import {
|
||||||
|
ReactNode,
|
||||||
|
createContext,
|
||||||
|
useCallback,
|
||||||
|
useEffect,
|
||||||
|
useMemo,
|
||||||
|
useState,
|
||||||
|
} from 'react';
|
||||||
|
|
||||||
|
type ExperimentResult = {
|
||||||
|
payload: CalulationResult<any>;
|
||||||
|
};
|
||||||
|
|
||||||
|
type ExperimentContextValue = {
|
||||||
|
result?: ExperimentResult;
|
||||||
|
error?: any;
|
||||||
|
loading: boolean;
|
||||||
|
selectNode: (node?: GraphNode) => void;
|
||||||
|
selectedNode?: GraphNode;
|
||||||
|
};
|
||||||
|
|
||||||
|
type ExperimentProviderProps = {
|
||||||
|
children: ReactNode;
|
||||||
|
worker: () => Worker;
|
||||||
|
};
|
||||||
|
|
||||||
|
const ExperimentContext = createContext<ExperimentContextValue>({
|
||||||
|
loading: false,
|
||||||
|
selectNode: () => { },
|
||||||
|
});
|
||||||
|
|
||||||
|
const ExperimentProvider: React.FC<ExperimentProviderProps> = ({
|
||||||
|
children,
|
||||||
|
worker,
|
||||||
|
}) => {
|
||||||
|
const [result, setResult] = useState<ExperimentResult>();
|
||||||
|
const [error, setError] = useState<any>();
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
const [selectedNode, setSelectedNode] = useState<GraphNode>();
|
||||||
|
|
||||||
|
const selectNode = useCallback((node?: GraphNode) => {
|
||||||
|
setSelectedNode(node);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
let workerInstance: Worker | undefined;
|
||||||
|
const run = async () => {
|
||||||
|
setLoading(true);
|
||||||
|
setError(undefined);
|
||||||
|
setResult(undefined);
|
||||||
|
const workerInstance = worker();
|
||||||
|
workerInstance.onmessage = (e) => {
|
||||||
|
switch (e.data.type) {
|
||||||
|
case 'error': {
|
||||||
|
setError(e.data);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case 'output': {
|
||||||
|
setResult(e.data);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
setLoading(false);
|
||||||
|
};
|
||||||
|
workerInstance.onerror = (e) => {
|
||||||
|
setError(e);
|
||||||
|
setLoading(false);
|
||||||
|
};
|
||||||
|
workerInstance.postMessage({
|
||||||
|
type: 'run',
|
||||||
|
});
|
||||||
|
};
|
||||||
|
run();
|
||||||
|
return () => {
|
||||||
|
workerInstance?.terminate();
|
||||||
|
};
|
||||||
|
}, [worker]);
|
||||||
|
|
||||||
|
const value = useMemo(
|
||||||
|
() => ({
|
||||||
|
result,
|
||||||
|
error,
|
||||||
|
loading,
|
||||||
|
selectNode,
|
||||||
|
selectedNode,
|
||||||
|
}),
|
||||||
|
[result, error, loading, selectNode, selectedNode],
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ExperimentContext.Provider value={value}>
|
||||||
|
{children}
|
||||||
|
</ExperimentContext.Provider>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export type { ExperimentContextValue };
|
||||||
|
export { ExperimentContext, ExperimentProvider };
|
||||||
15
packages/playground/src/features/experiment/create.tsx
Normal file
15
packages/playground/src/features/experiment/create.tsx
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
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 };
|
||||||
19
packages/playground/src/features/experiment/hooks.ts
Normal file
19
packages/playground/src/features/experiment/hooks.ts
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
import { useContext } from 'react';
|
||||||
|
import { ExperimentContext } from './context';
|
||||||
|
|
||||||
|
const useExperimentResult = () => {
|
||||||
|
const { result } = useContext(ExperimentContext);
|
||||||
|
return result?.payload;
|
||||||
|
};
|
||||||
|
|
||||||
|
const useSelectNode = () => {
|
||||||
|
const { selectNode } = useContext(ExperimentContext);
|
||||||
|
return selectNode;
|
||||||
|
};
|
||||||
|
|
||||||
|
const useSelectedNode = () => {
|
||||||
|
const { selectedNode } = useContext(ExperimentContext);
|
||||||
|
return selectedNode;
|
||||||
|
};
|
||||||
|
|
||||||
|
export { useExperimentResult, useSelectNode, useSelectedNode };
|
||||||
4
packages/playground/src/features/experiment/index.ts
Normal file
4
packages/playground/src/features/experiment/index.ts
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
export { createWorker } from './worker';
|
||||||
|
export { ExperimentProvider as Experminent } from './context';
|
||||||
|
export { createExperiment } from './create';
|
||||||
|
export { useExperimentResult } from './hooks';
|
||||||
23
packages/playground/src/features/experiment/worker.ts
Normal file
23
packages/playground/src/features/experiment/worker.ts
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
type WorkerFn = (...args: any[]) => any;
|
||||||
|
|
||||||
|
const createWorker = (fn: WorkerFn) => {
|
||||||
|
const run = async () => {
|
||||||
|
const startTime = performance.now();
|
||||||
|
try {
|
||||||
|
const result = await fn();
|
||||||
|
const endTime = performance.now();
|
||||||
|
const duration = endTime - startTime;
|
||||||
|
self.postMessage({ type: 'output', payload: result, duration });
|
||||||
|
} catch (error) {
|
||||||
|
self.postMessage({ type: 'error', payload: error });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
self.addEventListener('message', (event) => {
|
||||||
|
const { type } = event.data;
|
||||||
|
if (type === 'run') {
|
||||||
|
run();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export { createWorker };
|
||||||
@@ -1,100 +0,0 @@
|
|||||||
import {
|
|
||||||
useCallback,
|
|
||||||
useContext,
|
|
||||||
useEffect,
|
|
||||||
useMemo,
|
|
||||||
useRef,
|
|
||||||
useState,
|
|
||||||
} from 'react';
|
|
||||||
import { RunnerContext } from './context';
|
|
||||||
|
|
||||||
type BlockProps = {
|
|
||||||
worker: Worker;
|
|
||||||
action: string;
|
|
||||||
presenter?: React.FC<any>;
|
|
||||||
};
|
|
||||||
|
|
||||||
const id = (function*() {
|
|
||||||
let i = 0;
|
|
||||||
while (true) {
|
|
||||||
yield i++;
|
|
||||||
}
|
|
||||||
})();
|
|
||||||
|
|
||||||
const Block: React.FC<BlockProps> = ({
|
|
||||||
worker,
|
|
||||||
action,
|
|
||||||
presenter: Presenter,
|
|
||||||
}) => {
|
|
||||||
const currentId = useRef(id.next().value);
|
|
||||||
const { vars } = useContext(RunnerContext);
|
|
||||||
const [output, setOutput] = useState<unknown>();
|
|
||||||
const [error, setError] = useState<unknown>();
|
|
||||||
const [running, setRunning] = useState<boolean>();
|
|
||||||
const [duration, setDuration] = useState<number>();
|
|
||||||
|
|
||||||
const view = useMemo(() => {
|
|
||||||
if (error) {
|
|
||||||
return error.toString();
|
|
||||||
}
|
|
||||||
if (Presenter) {
|
|
||||||
return <Presenter output={output} />;
|
|
||||||
}
|
|
||||||
return JSON.stringify(output, null, 2);
|
|
||||||
}, [output, error, Presenter]);
|
|
||||||
|
|
||||||
const runBlock = useCallback(async () => {
|
|
||||||
setRunning(true);
|
|
||||||
setError(undefined);
|
|
||||||
setOutput(undefined);
|
|
||||||
|
|
||||||
try {
|
|
||||||
worker.postMessage({
|
|
||||||
type: 'run',
|
|
||||||
action,
|
|
||||||
vars,
|
|
||||||
id: currentId.current,
|
|
||||||
});
|
|
||||||
} catch (error) {
|
|
||||||
setError(error);
|
|
||||||
console.error(error);
|
|
||||||
}
|
|
||||||
|
|
||||||
setRunning(false);
|
|
||||||
}, [worker, vars, action]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const listener = (event: MessageEvent) => {
|
|
||||||
const { type, payload, id, duration } = event.data;
|
|
||||||
if (id !== currentId.current) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
setDuration(duration);
|
|
||||||
setRunning(false);
|
|
||||||
if (type === 'output') {
|
|
||||||
setOutput(payload);
|
|
||||||
}
|
|
||||||
if (type === 'error') {
|
|
||||||
setError(payload);
|
|
||||||
console.error(payload);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
worker.addEventListener('message', listener);
|
|
||||||
return () => {
|
|
||||||
worker.removeEventListener('message', listener);
|
|
||||||
};
|
|
||||||
}, [worker]);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
<button onClick={runBlock} disabled={running}>
|
|
||||||
Run
|
|
||||||
</button>
|
|
||||||
{duration && <div>Duration: {duration.toFixed(2)}ms</div>}
|
|
||||||
{running && <div>Running...</div>}
|
|
||||||
{view}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export { Block };
|
|
||||||
@@ -1,42 +0,0 @@
|
|||||||
import { createContext, useCallback, useMemo } from 'react';
|
|
||||||
|
|
||||||
type Vars = Record<string, unknown>;
|
|
||||||
|
|
||||||
type RunnerContextValue = {
|
|
||||||
vars: Vars;
|
|
||||||
run: (fn: (vars: Vars) => Promise<void>) => Promise<void>;
|
|
||||||
};
|
|
||||||
|
|
||||||
type RunnerProviderProps = {
|
|
||||||
vars?: Vars;
|
|
||||||
children: React.ReactNode;
|
|
||||||
};
|
|
||||||
|
|
||||||
const RunnerContext = createContext<RunnerContextValue>({
|
|
||||||
vars: {},
|
|
||||||
run: async () => {},
|
|
||||||
});
|
|
||||||
|
|
||||||
const RunnerProvider: React.FC<RunnerProviderProps> = ({
|
|
||||||
vars = {},
|
|
||||||
children,
|
|
||||||
}) => {
|
|
||||||
const currentVars = useMemo(() => vars, [vars]);
|
|
||||||
|
|
||||||
const run = useCallback(
|
|
||||||
async (fn: (vars: Vars) => Promise<void>) => {
|
|
||||||
const output = await fn(currentVars);
|
|
||||||
return output;
|
|
||||||
},
|
|
||||||
[currentVars],
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<RunnerContext.Provider value={{ vars, run }}>
|
|
||||||
{children}
|
|
||||||
</RunnerContext.Provider>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export type { Vars };
|
|
||||||
export { RunnerContext, RunnerProvider };
|
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
export { RunnerProvider } from './context';
|
|
||||||
export { Block } from './block';
|
|
||||||
export { createWorker } from './worker';
|
|
||||||
@@ -1,21 +0,0 @@
|
|||||||
type WorkerFn = Record<string, (...args: any[]) => any>;
|
|
||||||
|
|
||||||
const createWorker = (fn: WorkerFn) => {
|
|
||||||
self.addEventListener('message', (event) => {
|
|
||||||
const { action, vars = {}, id } = event.data;
|
|
||||||
const run = async () => {
|
|
||||||
const startTime = performance.now();
|
|
||||||
try {
|
|
||||||
const result = await fn[action](vars);
|
|
||||||
const endTime = performance.now();
|
|
||||||
const duration = endTime - startTime;
|
|
||||||
self.postMessage({ type: 'output', payload: result, id, duration });
|
|
||||||
} catch (error) {
|
|
||||||
self.postMessage({ type: 'error', payload: error, id });
|
|
||||||
}
|
|
||||||
};
|
|
||||||
run();
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
export { createWorker };
|
|
||||||
3
packages/playground/src/main.css
Normal file
3
packages/playground/src/main.css
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
@tailwind base;
|
||||||
|
@tailwind components;
|
||||||
|
@tailwind utilities;
|
||||||
@@ -1,3 +1,4 @@
|
|||||||
|
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';
|
||||||
|
|||||||
@@ -1,7 +0,0 @@
|
|||||||
import { Block } from '../../features/runner'
|
|
||||||
import { worker } from './worker';
|
|
||||||
import { Presenter } from '../../presenters/graph';
|
|
||||||
|
|
||||||
# Hello World
|
|
||||||
|
|
||||||
<Block worker={worker} action="realistic" presenter={Presenter} />
|
|
||||||
@@ -1,6 +0,0 @@
|
|||||||
import './script.ts';
|
|
||||||
const worker = new Worker(new URL('./script.ts', import.meta.url), {
|
|
||||||
type: 'module',
|
|
||||||
});
|
|
||||||
|
|
||||||
export { worker };
|
|
||||||
@@ -1,111 +1,70 @@
|
|||||||
import { useMemo, useState } from 'react';
|
import { useMemo, useState } from 'react';
|
||||||
import { GraphCanvas } from 'reagraph';
|
import { GraphCanvas } from 'reagraph';
|
||||||
import { ConvertedResult } from '../../utils/graph';
|
import { useExperimentResult } from '../../features/experiment';
|
||||||
import { Plan } from './plan';
|
import { convertResult } from '../../utils/graph';
|
||||||
|
import {
|
||||||
|
useSelectNode,
|
||||||
|
useSelectedNode,
|
||||||
|
} from '../../features/experiment/hooks';
|
||||||
|
|
||||||
type PresenterProps = {
|
const Graph: React.FC = () => {
|
||||||
output: ConvertedResult;
|
const data = useExperimentResult();
|
||||||
};
|
const selectedNode = useSelectedNode();
|
||||||
|
const selectNode = useSelectNode();
|
||||||
const Presenter: React.FC<PresenterProps> = ({ output }) => {
|
const output = useMemo(() => {
|
||||||
const [currentStep, setCurrentStep] = useState(0);
|
if (!data) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
return convertResult(data);
|
||||||
|
}, [data]);
|
||||||
const [visualize, setVisualize] = useState(false);
|
const [visualize, setVisualize] = useState(false);
|
||||||
const [selectedNode, setSelectedNode] = useState<string | undefined>(
|
|
||||||
undefined,
|
|
||||||
);
|
|
||||||
const selectedPath = useMemo(() => {
|
const selectedPath = useMemo(() => {
|
||||||
if (!selectedNode) {
|
if (!selectedNode) {
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
const result: string[] = [];
|
const result: string[] = [];
|
||||||
let current = output.result.nodes.find((n) => n.id === selectedNode);
|
let current = output?.result.nodes.find((n) => n.id === selectedNode.id);
|
||||||
|
|
||||||
while (current) {
|
while (current) {
|
||||||
result.push(current.id);
|
result.push(current.id);
|
||||||
if (!current.parent) {
|
if (!current.parent) {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
current = output.result.nodes.find((n) => n.id === current?.parent);
|
current = output?.result.nodes.find((n) => n.id === current?.parent);
|
||||||
}
|
}
|
||||||
|
|
||||||
return result;
|
return result;
|
||||||
}, [selectedNode, output]);
|
}, [selectedNode, output]);
|
||||||
const completed = useMemo(() => {
|
|
||||||
return (
|
|
||||||
output?.result?.completed
|
|
||||||
.map((c) => ({
|
|
||||||
id: c.id,
|
|
||||||
score: c.score,
|
|
||||||
}))
|
|
||||||
.sort((a, b) => b.score - a.score)
|
|
||||||
.slice(0, 10) || []
|
|
||||||
);
|
|
||||||
}, [output?.result?.completed]);
|
|
||||||
|
|
||||||
const maxStep = useMemo(
|
|
||||||
() => Math.max(...(output?.nodes?.map((n) => n.data?.exploreId) || [])),
|
|
||||||
[output],
|
|
||||||
);
|
|
||||||
const collapsedNodeIds = useMemo(
|
|
||||||
() =>
|
|
||||||
output?.nodes
|
|
||||||
?.filter((n) => n.data?.exploreId > currentStep)
|
|
||||||
.map((n) => n.id),
|
|
||||||
[output, currentStep],
|
|
||||||
);
|
|
||||||
if (!output) {
|
if (!output) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
console.log(output);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
Nodes count: {output.nodes.length}
|
Nodes count: {output.nodes.length}
|
||||||
<button onClick={() => setVisualize(!visualize)}>
|
<button onClick={() => setVisualize(!visualize)}>
|
||||||
{visualize ? 'Hide' : 'Show'} Visualize
|
{visualize ? 'Hide' : 'Show'} Visualize
|
||||||
</button>
|
</button>
|
||||||
{visualize && (
|
|
||||||
<>
|
|
||||||
<button onClick={() => setCurrentStep(currentStep - 1)}>Prev</button>
|
|
||||||
<input
|
|
||||||
type="range"
|
|
||||||
min={0}
|
|
||||||
max={maxStep}
|
|
||||||
value={currentStep}
|
|
||||||
onChange={(e) => setCurrentStep(parseInt(e.target.value))}
|
|
||||||
/>
|
|
||||||
<button onClick={() => setCurrentStep(currentStep + 1)}>Next</button>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
{completed.map((c) => (
|
|
||||||
<div key={c.id} onClick={() => setSelectedNode(c.id)}>
|
|
||||||
{c.id} - {c.score}
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
{selectedNode && <Plan id={selectedNode} output={output} />}
|
|
||||||
{visualize && (
|
{visualize && (
|
||||||
<div style={{ position: 'relative', height: '70vh' }}>
|
<div style={{ position: 'relative', height: '70vh' }}>
|
||||||
<GraphCanvas
|
<GraphCanvas
|
||||||
{...output}
|
{...output}
|
||||||
collapsedNodeIds={collapsedNodeIds}
|
|
||||||
labelType="all"
|
labelType="all"
|
||||||
|
layoutType="hierarchicalTd"
|
||||||
onNodeClick={(node) => {
|
onNodeClick={(node) => {
|
||||||
if (node.id === selectedNode) {
|
if (node.id === selectedNode?.id) {
|
||||||
setSelectedNode(undefined);
|
selectNode(undefined);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
setSelectedNode(node.id);
|
const nextNode = data?.nodes.find((n) => n.id === node.id);
|
||||||
|
selectNode(nextNode);
|
||||||
}}
|
}}
|
||||||
selections={selectedPath}
|
selections={selectedPath}
|
||||||
renderNode={({ size, opacity, node }) => {
|
renderNode={({ size, opacity, node }) => {
|
||||||
let color = 'gray';
|
let color = 'gray';
|
||||||
if (
|
|
||||||
node.data?.exploreId < currentStep &&
|
|
||||||
node.data?.exploreId > 0
|
|
||||||
) {
|
|
||||||
color = 'yellow';
|
|
||||||
}
|
|
||||||
if (node.data?.exploreId === currentStep) {
|
|
||||||
color = 'blue';
|
|
||||||
}
|
|
||||||
if (node.data?.deadEnd) {
|
if (node.data?.deadEnd) {
|
||||||
color = 'red';
|
color = 'red';
|
||||||
}
|
}
|
||||||
@@ -136,4 +95,4 @@ const Presenter: React.FC<PresenterProps> = ({ output }) => {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export { Presenter };
|
export { Graph };
|
||||||
|
|||||||
@@ -40,7 +40,7 @@ const Node = ({ node, output }: NodeProps) => {
|
|||||||
if (node.type === 'travel') {
|
if (node.type === 'travel') {
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
{time} Travel: {node.location}
|
{time} Travel: {node.context.location}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
90
packages/playground/src/presenters/plan/index.tsx
Normal file
90
packages/playground/src/presenters/plan/index.tsx
Normal file
@@ -0,0 +1,90 @@
|
|||||||
|
import { GraphNode } from '@bob-the-algorithm/core';
|
||||||
|
import { useMemo } from 'react';
|
||||||
|
import { convertResult } from '../../utils/graph';
|
||||||
|
import { format } from 'date-fns';
|
||||||
|
import { useExperimentResult } from '../../features/experiment';
|
||||||
|
import { useSelectNode } from '../../features/experiment/hooks';
|
||||||
|
|
||||||
|
type PlanProps = {
|
||||||
|
node: GraphNode;
|
||||||
|
};
|
||||||
|
|
||||||
|
type NodeProps = {
|
||||||
|
node: GraphNode;
|
||||||
|
};
|
||||||
|
|
||||||
|
const Node = ({ node }: NodeProps) => {
|
||||||
|
const selectNode = useSelectNode();
|
||||||
|
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(() => {
|
||||||
|
if (!output) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
const result: GraphNode[] = [];
|
||||||
|
let current = node;
|
||||||
|
if (!current) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
while (current) {
|
||||||
|
result.push(current);
|
||||||
|
if (!current.parent) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
current = output.result.nodes.find((n) => n.id === current?.parent)!;
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}, [output, node]);
|
||||||
|
|
||||||
|
if (!output) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{nodes.map((n) => (
|
||||||
|
<Node key={n.id} node={n} />
|
||||||
|
))}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export { Plan };
|
||||||
8
packages/playground/src/utils/experiments.ts
Normal file
8
packages/playground/src/utils/experiments.ts
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
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 };
|
||||||
@@ -1,8 +0,0 @@
|
|||||||
const imports = import.meta.glob('../pages/*/index.mdx');
|
|
||||||
|
|
||||||
const pages = Object.entries(imports).map(([path, loader]) => {
|
|
||||||
const slug = path.replace('./pages/', '').replace('/index.mdx', '');
|
|
||||||
return { slug, loader };
|
|
||||||
});
|
|
||||||
|
|
||||||
export { pages };
|
|
||||||
15
packages/playground/tailwind.config.js
Normal file
15
packages/playground/tailwind.config.js
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
/** @type {import('tailwindcss').Config} */
|
||||||
|
const { nextui } = require('@nextui-org/react');
|
||||||
|
|
||||||
|
export default {
|
||||||
|
content: [
|
||||||
|
'./index.html',
|
||||||
|
'./src/**/*.{js,ts,jsx,tsx}',
|
||||||
|
'./node_modules/@nextui-org/theme/dist/**/*.{js,ts,jsx,tsx}',
|
||||||
|
],
|
||||||
|
theme: {
|
||||||
|
extend: {},
|
||||||
|
},
|
||||||
|
darkMode: 'class',
|
||||||
|
plugins: [nextui()],
|
||||||
|
};
|
||||||
1
packages/playground/tsconfig.node.tsbuildinfo
Normal file
1
packages/playground/tsconfig.node.tsbuildinfo
Normal file
File diff suppressed because one or more lines are too long
2
packages/playground/vite.config.d.ts
vendored
Normal file
2
packages/playground/vite.config.d.ts
vendored
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
declare const _default: import("vite").UserConfig;
|
||||||
|
export default _default;
|
||||||
9
packages/playground/vite.config.js
Normal file
9
packages/playground/vite.config.js
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
import { defineConfig } from 'vite';
|
||||||
|
import react from '@vitejs/plugin-react-swc';
|
||||||
|
import mdx from '@mdx-js/rollup';
|
||||||
|
var ASSET_URL = process.env.ASSET_URL || '';
|
||||||
|
// https://vitejs.dev/config/
|
||||||
|
export default defineConfig({
|
||||||
|
base: ASSET_URL,
|
||||||
|
plugins: [mdx(), react()],
|
||||||
|
});
|
||||||
2589
pnpm-lock.yaml
generated
2589
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user