mirror of
https://github.com/morten-olsen/bob.git
synced 2026-02-08 01:46:29 +01:00
Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
732e47ceed |
2
.github/workflows/release-package.yml
vendored
2
.github/workflows/release-package.yml
vendored
@@ -34,7 +34,7 @@ jobs:
|
|||||||
pnpm install
|
pnpm install
|
||||||
pnpm run build
|
pnpm run build
|
||||||
env:
|
env:
|
||||||
ASSET_URL: https://mortenolsen.pro/bob/
|
ASSET_URL: https://mortenolsen.pro/bob
|
||||||
- uses: actions/upload-artifact@v3
|
- uses: actions/upload-artifact@v3
|
||||||
with:
|
with:
|
||||||
name: lib
|
name: lib
|
||||||
|
|||||||
2
.npmrc
2
.npmrc
@@ -1,3 +1,3 @@
|
|||||||
public-hoist-pattern[]=*@nextui-org/*
|
node-linker=hoisted
|
||||||
store-dir=.pnpm-store
|
store-dir=.pnpm-store
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
|
|
||||||
> @bob-the-algorithm/core@0.1.8 build /home/alice/Git/bob/packages/algorithm
|
> bob-the-algorithm@ build /Users/alice/work/private/bob/packages/algorithm
|
||||||
> tsc --build configs/tsconfig.libs.json
|
> tsc --build configs/tsconfig.libs.json
|
||||||
|
|
||||||
|
|||||||
@@ -25,8 +25,5 @@
|
|||||||
"build": "tsc --build configs/tsconfig.libs.json"
|
"build": "tsc --build configs/tsconfig.libs.json"
|
||||||
},
|
},
|
||||||
"types": "./dist/cjs/types/index.d.ts",
|
"types": "./dist/cjs/types/index.d.ts",
|
||||||
"version": "0.1.8",
|
"version": "0.1.8"
|
||||||
"dependencies": {
|
|
||||||
"@sinclair/typebox": "^0.31.14"
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,107 +1,87 @@
|
|||||||
import { GraphNode } from '../types/node';
|
import { Attributes, GraphNode } from '../types/node';
|
||||||
import { Planable } from '../types/planable';
|
import { Planable } from '../types/planable';
|
||||||
import { PluginAttributes, PluginContext, Plugins } from '../types/plugin';
|
import { PluginAttributes, Plugins } from '../types/plugin';
|
||||||
import { expandNode } from './expand-node';
|
import { expandNode } from './expand-node';
|
||||||
|
|
||||||
type CalulationOptions<TPlugins extends Plugins> = {
|
type CalulationOptions<TPlugins extends Plugins> = {
|
||||||
|
location: string;
|
||||||
|
time: number;
|
||||||
|
planables: Planable<PluginAttributes<TPlugins>>[];
|
||||||
plugins: TPlugins;
|
plugins: TPlugins;
|
||||||
};
|
|
||||||
|
|
||||||
type CalulationResult<TPlugins extends Plugins> = {
|
|
||||||
root: GraphNode<PluginAttributes<TPlugins>, PluginContext<TPlugins>>;
|
|
||||||
nodes: GraphNode<PluginAttributes<TPlugins>, PluginContext<TPlugins>>[];
|
|
||||||
completed: GraphNode<PluginAttributes<TPlugins>, PluginContext<TPlugins>>[];
|
|
||||||
planables: Planable<PluginAttributes<TPlugins>>[];
|
|
||||||
};
|
|
||||||
|
|
||||||
type RunOptions<TPlugins extends Plugins> = {
|
|
||||||
start: number;
|
|
||||||
context: PluginContext<TPlugins>;
|
|
||||||
planables: Planable<PluginAttributes<TPlugins>>[];
|
|
||||||
heuristic?: (result: any) => boolean;
|
heuristic?: (result: any) => boolean;
|
||||||
|
onUpdated?: (result: any) => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
type PlanableWithPlugins<TPlugins extends Plugins> = Planable<
|
type CalulationResult<TAttributes extends Attributes> = {
|
||||||
PluginAttributes<TPlugins>
|
root: GraphNode<TAttributes>;
|
||||||
>;
|
nodes: GraphNode<TAttributes>[];
|
||||||
|
completed: GraphNode<TAttributes>[];
|
||||||
|
planables: Planable<TAttributes>[];
|
||||||
|
};
|
||||||
|
|
||||||
class Bob<TPlugins extends Plugins> {
|
const idGen = () => {
|
||||||
#options: CalulationOptions<TPlugins>;
|
let id = 0;
|
||||||
#id: number = 0;
|
return () => {
|
||||||
|
id += 1;
|
||||||
|
return id.toString();
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
constructor(options: CalulationOptions<TPlugins>) {
|
const calulation = async <TPlugins extends Plugins>({
|
||||||
this.#options = options;
|
location,
|
||||||
|
time,
|
||||||
|
planables,
|
||||||
|
plugins,
|
||||||
|
heuristic,
|
||||||
|
onUpdated,
|
||||||
|
}: CalulationOptions<TPlugins>): Promise<
|
||||||
|
CalulationResult<PluginAttributes<TPlugins>>
|
||||||
|
> => {
|
||||||
|
const generateId = idGen();
|
||||||
|
let exploreId = 1;
|
||||||
|
const root: GraphNode<PluginAttributes<TPlugins>> = {
|
||||||
|
id: generateId(),
|
||||||
|
type: 'root',
|
||||||
|
score: 0,
|
||||||
|
parent: null,
|
||||||
|
duration: 0,
|
||||||
|
time,
|
||||||
|
location,
|
||||||
|
exploreId: 0,
|
||||||
|
remaining: planables,
|
||||||
|
};
|
||||||
|
|
||||||
|
const nodes: GraphNode<PluginAttributes<TPlugins>>[] = [root];
|
||||||
|
const leafNodes: GraphNode<PluginAttributes<TPlugins>>[] = [root];
|
||||||
|
const completed: GraphNode<PluginAttributes<TPlugins>>[] = [];
|
||||||
|
|
||||||
|
const popHighestScore = () => {
|
||||||
|
const highestScore = Math.max(...leafNodes.map((n) => n.score));
|
||||||
|
const highestScoreNode = leafNodes.find((n) => n.score === highestScore);
|
||||||
|
if (!highestScoreNode) {
|
||||||
|
throw new Error('No highest score node');
|
||||||
|
}
|
||||||
|
leafNodes.splice(leafNodes.indexOf(highestScoreNode), 1);
|
||||||
|
return highestScoreNode;
|
||||||
|
};
|
||||||
|
|
||||||
|
while (leafNodes.length > 0) {
|
||||||
|
const node = popHighestScore();
|
||||||
|
node.exploreId = exploreId++;
|
||||||
|
const expanded = await expandNode({ node, generateId, plugins });
|
||||||
|
nodes.push(...expanded);
|
||||||
|
completed.push(...expanded.filter((n) => n.remaining.length === 0));
|
||||||
|
leafNodes.push(...expanded.filter((n) => n.remaining.length > 0));
|
||||||
|
if (heuristic && heuristic({ root, nodes, completed, planables })) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
if (onUpdated) {
|
||||||
|
onUpdated({ root, nodes, completed, planables });
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#getNextId = (): string => {
|
return { root, nodes, completed, planables };
|
||||||
return (this.#id++).toString();
|
};
|
||||||
};
|
|
||||||
|
|
||||||
public run = async ({
|
export type { CalulationOptions, CalulationResult };
|
||||||
planables,
|
export { calulation };
|
||||||
start,
|
|
||||||
context,
|
|
||||||
heuristic,
|
|
||||||
}: RunOptions<TPlugins>): Promise<CalulationResult<TPlugins>> => {
|
|
||||||
const { plugins } = this.#options;
|
|
||||||
let exploreId = 1;
|
|
||||||
const root: GraphNode<
|
|
||||||
PluginAttributes<TPlugins>,
|
|
||||||
PluginContext<TPlugins>
|
|
||||||
> = {
|
|
||||||
id: this.#getNextId(),
|
|
||||||
context,
|
|
||||||
type: 'root',
|
|
||||||
score: 0,
|
|
||||||
parent: null,
|
|
||||||
duration: 0,
|
|
||||||
time: start,
|
|
||||||
exploreId: 0,
|
|
||||||
remaining: planables,
|
|
||||||
};
|
|
||||||
|
|
||||||
const nodes: GraphNode<
|
|
||||||
PluginAttributes<TPlugins>,
|
|
||||||
PluginContext<TPlugins>
|
|
||||||
>[] = [root];
|
|
||||||
const leafNodes: GraphNode<
|
|
||||||
PluginAttributes<TPlugins>,
|
|
||||||
PluginContext<TPlugins>
|
|
||||||
>[] = [root];
|
|
||||||
const completed: GraphNode<
|
|
||||||
PluginAttributes<TPlugins>,
|
|
||||||
PluginContext<TPlugins>
|
|
||||||
>[] = [];
|
|
||||||
|
|
||||||
const popHighestScore = () => {
|
|
||||||
const highestScore = Math.max(...leafNodes.map((n) => n.score));
|
|
||||||
const highestScoreNode = leafNodes.find((n) => n.score === highestScore);
|
|
||||||
if (!highestScoreNode) {
|
|
||||||
throw new Error('No highest score node');
|
|
||||||
}
|
|
||||||
leafNodes.splice(leafNodes.indexOf(highestScoreNode), 1);
|
|
||||||
return highestScoreNode;
|
|
||||||
};
|
|
||||||
|
|
||||||
while (leafNodes.length > 0) {
|
|
||||||
const node = popHighestScore();
|
|
||||||
node.exploreId = exploreId++;
|
|
||||||
const expanded = await expandNode({
|
|
||||||
node,
|
|
||||||
generateId: this.#getNextId,
|
|
||||||
plugins,
|
|
||||||
});
|
|
||||||
nodes.push(...expanded);
|
|
||||||
completed.push(...expanded.filter((n) => n.remaining.length === 0));
|
|
||||||
leafNodes.push(...expanded.filter((n) => n.remaining.length > 0));
|
|
||||||
if (heuristic && heuristic({ root, nodes, completed, planables })) {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return { root, nodes, completed, planables };
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
export type { CalulationOptions, CalulationResult, PlanableWithPlugins };
|
|
||||||
export { Bob };
|
|
||||||
|
|||||||
@@ -1,26 +1,18 @@
|
|||||||
import { Attributes, GraphNode } from '../types/node';
|
import { Attributes, GraphNode } from '../types/node';
|
||||||
import { Plugins } from '../types/plugin';
|
import { Plugin } from '../types/plugin';
|
||||||
import { hasImpossible } from './is-impossible';
|
import { hasImpossible } from './is-impossible';
|
||||||
|
|
||||||
type ExpandOptions<
|
type ExpandOptions<TAttributes extends Attributes> = {
|
||||||
TAttributes extends Attributes,
|
node: GraphNode<TAttributes>;
|
||||||
TContext extends Attributes,
|
|
||||||
> = {
|
|
||||||
node: GraphNode<TAttributes, TContext>;
|
|
||||||
generateId: () => string;
|
generateId: () => string;
|
||||||
plugins: Plugins;
|
plugins: Plugin[];
|
||||||
};
|
};
|
||||||
|
|
||||||
const expandNode = async <
|
const expandNode = async <TAttributes extends Attributes>({
|
||||||
TAttributes extends Attributes,
|
|
||||||
TContext extends Attributes,
|
|
||||||
>({
|
|
||||||
node,
|
node,
|
||||||
generateId,
|
generateId,
|
||||||
plugins,
|
plugins,
|
||||||
}: ExpandOptions<TAttributes, TContext>): Promise<
|
}: ExpandOptions<TAttributes>): Promise<GraphNode<TAttributes>[]> => {
|
||||||
GraphNode<TAttributes, TContext>[]
|
|
||||||
> => {
|
|
||||||
const isImpossible = hasImpossible({ node });
|
const isImpossible = hasImpossible({ node });
|
||||||
|
|
||||||
if (isImpossible) {
|
if (isImpossible) {
|
||||||
@@ -29,7 +21,7 @@ const expandNode = async <
|
|||||||
}
|
}
|
||||||
|
|
||||||
const metaNodes = await Promise.all(
|
const metaNodes = await Promise.all(
|
||||||
Object.values(plugins).map(async (plugin) => {
|
plugins.map(async (plugin) => {
|
||||||
if (!plugin.getMetaNodes) {
|
if (!plugin.getMetaNodes) {
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
@@ -47,7 +39,7 @@ const expandNode = async <
|
|||||||
);
|
);
|
||||||
|
|
||||||
const planables = node.remaining.filter((planable) => {
|
const planables = node.remaining.filter((planable) => {
|
||||||
const hasNonPlanable = Object.values(plugins).some(
|
const hasNonPlanable = plugins.some(
|
||||||
(plugin) => plugin.isPlanable && !plugin.isPlanable(node, planable),
|
(plugin) => plugin.isPlanable && !plugin.isPlanable(node, planable),
|
||||||
);
|
);
|
||||||
return !hasNonPlanable;
|
return !hasNonPlanable;
|
||||||
@@ -70,7 +62,7 @@ const expandNode = async <
|
|||||||
node.time + node.duration,
|
node.time + node.duration,
|
||||||
planable.start?.min || 0,
|
planable.start?.min || 0,
|
||||||
);
|
);
|
||||||
const nextNode = {
|
return {
|
||||||
...node,
|
...node,
|
||||||
type: 'planable',
|
type: 'planable',
|
||||||
exploreId: 0,
|
exploreId: 0,
|
||||||
@@ -83,14 +75,6 @@ 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,8 +1,5 @@
|
|||||||
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 {
|
export { expandNode } from './algorithm/expand-node';
|
||||||
Bob,
|
export { calulation, type CalulationResult } from './algorithm/calulation';
|
||||||
type CalulationResult,
|
export { plugins } from './plugins/index';
|
||||||
type PlanableWithPlugins,
|
|
||||||
} from './algorithm/calulation';
|
|
||||||
export { plugins, type AllPlugins } from './plugins';
|
|
||||||
|
|||||||
@@ -1,57 +0,0 @@
|
|||||||
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,20 +0,0 @@
|
|||||||
import type { TSchema, Static } from '@sinclair/typebox';
|
|
||||||
import type { Plugin } from '../types/plugin';
|
|
||||||
|
|
||||||
const createPlugin = <
|
|
||||||
TAttribuesSchema extends TSchema,
|
|
||||||
TContextSchema extends TSchema,
|
|
||||||
>(
|
|
||||||
attributes: TAttribuesSchema,
|
|
||||||
context: TContextSchema,
|
|
||||||
plugin: Omit<
|
|
||||||
Plugin<Static<TAttribuesSchema>, Static<TContextSchema>>,
|
|
||||||
'attributes' | 'context'
|
|
||||||
>,
|
|
||||||
): Plugin<Static<TAttribuesSchema>, Static<TContextSchema>> => ({
|
|
||||||
...plugin,
|
|
||||||
attributes,
|
|
||||||
context,
|
|
||||||
});
|
|
||||||
|
|
||||||
export { createPlugin };
|
|
||||||
@@ -1,15 +1,9 @@
|
|||||||
|
import { Attributes } from '../types/node';
|
||||||
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<Attributes>>;
|
||||||
} satisfies Record<string, (...args: any[]) => Plugin>;
|
|
||||||
|
|
||||||
type AllPlugins = {
|
|
||||||
[K in keyof typeof plugins]: ReturnType<(typeof plugins)[K]>;
|
|
||||||
};
|
|
||||||
|
|
||||||
export type { AllPlugins };
|
|
||||||
export { plugins };
|
export { plugins };
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { Type } from '@sinclair/typebox';
|
import { GraphNode } from '../types/node';
|
||||||
import { createPlugin } from './create';
|
import { Plugin } from '../types/plugin';
|
||||||
|
|
||||||
type GetTravelTime = (from: string, to: string) => Promise<number>;
|
type GetTravelTime = (from: string, to: string) => Promise<number>;
|
||||||
|
|
||||||
@@ -7,66 +7,58 @@ type TransportOptions = {
|
|||||||
getTravelTime: GetTravelTime;
|
getTravelTime: GetTravelTime;
|
||||||
};
|
};
|
||||||
|
|
||||||
const transport = ({ getTravelTime }: TransportOptions) =>
|
type TransportAttributes = {
|
||||||
createPlugin(
|
locations?: string[];
|
||||||
Type.Object({
|
};
|
||||||
locations: Type.Optional(Type.Array(Type.String())),
|
|
||||||
}),
|
|
||||||
Type.Object({
|
|
||||||
location: Type.String(),
|
|
||||||
}),
|
|
||||||
{
|
|
||||||
getMetaNodes: async (node) => {
|
|
||||||
const locations =
|
|
||||||
(node.type !== 'travel' &&
|
|
||||||
[
|
|
||||||
...new Set(
|
|
||||||
node.remaining
|
|
||||||
.map((planable) => planable.attributes?.locations)
|
|
||||||
.flat(),
|
|
||||||
),
|
|
||||||
]
|
|
||||||
.filter((location) => location !== node.context.location)
|
|
||||||
.filter(Boolean)
|
|
||||||
.map((l) => l!)) ||
|
|
||||||
[];
|
|
||||||
|
|
||||||
const travelNodes = await Promise.all(
|
const transport = ({
|
||||||
locations.map(async (location) => {
|
getTravelTime,
|
||||||
const travelTime = await getTravelTime(
|
}: TransportOptions): Plugin<TransportAttributes> => ({
|
||||||
node.context.location,
|
getMetaNodes: async (node) => {
|
||||||
location,
|
const locations =
|
||||||
);
|
(node.type !== 'travel' &&
|
||||||
return {
|
[
|
||||||
...node,
|
...new Set(
|
||||||
type: 'travel' as const,
|
node.remaining
|
||||||
context: {
|
.map((planable) => planable.attributes?.locations)
|
||||||
...node.context,
|
.flat(),
|
||||||
location,
|
),
|
||||||
},
|
]
|
||||||
planable: undefined,
|
.filter((location) => location !== node.location)
|
||||||
location,
|
.filter(Boolean)
|
||||||
exploreId: 0,
|
.map((l) => l!)) ||
|
||||||
score: node.score - 5,
|
[];
|
||||||
time: node.time + node.duration,
|
|
||||||
duration: travelTime,
|
|
||||||
parent: node.id,
|
|
||||||
};
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
|
|
||||||
return travelNodes;
|
const travelNodes = await Promise.all(
|
||||||
},
|
locations.map<Promise<GraphNode<TransportAttributes>>>(
|
||||||
isPlanable: (node, planable) => {
|
async (location) => {
|
||||||
if (
|
const travelTime = await getTravelTime(node.location, location);
|
||||||
planable.attributes?.locations &&
|
return {
|
||||||
!planable.attributes?.locations.includes(node.context.location)
|
...node,
|
||||||
) {
|
type: 'travel',
|
||||||
return false;
|
planable: undefined,
|
||||||
}
|
location,
|
||||||
return true;
|
exploreId: 0,
|
||||||
},
|
score: node.score - 20,
|
||||||
},
|
time: node.time + node.duration,
|
||||||
);
|
duration: travelTime,
|
||||||
|
parent: node.id,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
return travelNodes;
|
||||||
|
},
|
||||||
|
isPlanable: (node, planable) => {
|
||||||
|
if (
|
||||||
|
planable.attributes?.locations &&
|
||||||
|
!planable.attributes?.locations.includes(node.location)
|
||||||
|
) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
export { transport };
|
export { transport };
|
||||||
|
|||||||
@@ -2,14 +2,11 @@ import type { Planable } from './planable';
|
|||||||
|
|
||||||
type Attributes = any;
|
type Attributes = any;
|
||||||
|
|
||||||
type GraphNode<
|
type GraphNode<TAttributes extends Attributes = Attributes> = {
|
||||||
TAttributes extends Attributes = Attributes,
|
|
||||||
TContext extends Attributes = Attributes,
|
|
||||||
> = {
|
|
||||||
id: string;
|
id: string;
|
||||||
type: 'root' | 'planable' | 'travel';
|
type: 'root' | 'planable' | 'travel';
|
||||||
score: number;
|
score: number;
|
||||||
context: TContext;
|
location: string;
|
||||||
parent: string | null;
|
parent: string | null;
|
||||||
time: number;
|
time: number;
|
||||||
exploreId: number;
|
exploreId: number;
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ type Planable<TAttributes extends Attributes = Attributes> = {
|
|||||||
};
|
};
|
||||||
attributes: TAttributes;
|
attributes: TAttributes;
|
||||||
required?: boolean;
|
required?: boolean;
|
||||||
|
locations?: string[];
|
||||||
};
|
};
|
||||||
|
|
||||||
export type { Planable };
|
export type { Planable };
|
||||||
|
|||||||
@@ -1,40 +1,25 @@
|
|||||||
import { Expand, UnionToIntersection } from '../types/utils';
|
import { Attributes, GraphNode } from './node';
|
||||||
import { GraphNode } from './node';
|
|
||||||
import { Planable } from './planable';
|
import { Planable } from './planable';
|
||||||
|
|
||||||
type Plugin<TAttributes = any, TContext = any> = {
|
type Plugin<TAttributes extends Attributes = Attributes> = {
|
||||||
context: any;
|
|
||||||
attributes: any;
|
|
||||||
getMetaNodes?: (
|
getMetaNodes?: (
|
||||||
node: GraphNode<TAttributes, TContext>,
|
node: GraphNode<TAttributes>,
|
||||||
) => Promise<GraphNode<TAttributes, TContext>[]>;
|
) => Promise<GraphNode<TAttributes>[]>;
|
||||||
isImpossible?: (node: GraphNode<TAttributes, TContext>) => Promise<boolean>;
|
isImpossible?: (node: GraphNode<TAttributes>) => Promise<boolean>;
|
||||||
isPlanable?: (
|
isPlanable?: (
|
||||||
node: GraphNode<TAttributes, TContext>,
|
node: GraphNode<TAttributes>,
|
||||||
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 = Plugin[];
|
||||||
|
|
||||||
type PluginAttributes<TPlugins extends Plugins> = MergeRecords<{
|
type PluginAttributes<TPlugins extends Plugins> = {
|
||||||
[K in keyof TPlugins]: TPlugins[K] extends Plugin<infer TAttributes, any>
|
[K in keyof TPlugins]: TPlugins[K] extends Plugin<infer TAttributes>
|
||||||
|
? TAttributes extends Attributes
|
||||||
? TAttributes
|
? TAttributes
|
||||||
|
: never
|
||||||
: never;
|
: never;
|
||||||
}>;
|
}[number];
|
||||||
|
|
||||||
type MergeRecords<T extends Record<string, any>> = Expand<
|
export type { Plugin, Plugins, PluginAttributes };
|
||||||
UnionToIntersection<T[keyof T]>
|
|
||||||
>;
|
|
||||||
|
|
||||||
type PluginContext<TPlugins extends Plugins> = MergeRecords<{
|
|
||||||
[K in keyof TPlugins]: TPlugins[K] extends Plugin<any, infer TContext>
|
|
||||||
? TContext
|
|
||||||
: never;
|
|
||||||
}>;
|
|
||||||
|
|
||||||
export type { Plugin, Plugins, PluginAttributes, PluginContext };
|
|
||||||
|
|||||||
@@ -1,17 +0,0 @@
|
|||||||
// 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 };
|
|
||||||
@@ -1,12 +1,8 @@
|
|||||||
{
|
{
|
||||||
"extends": "./tsconfig.base.json",
|
"extends": "./tsconfig.base.json",
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
"lib": [
|
"lib": ["ES2022", "DOM"],
|
||||||
"ES2022",
|
|
||||||
"DOM"
|
|
||||||
],
|
|
||||||
"target": "ES2022",
|
"target": "ES2022",
|
||||||
"module": "ES2022",
|
"module": "ESNext"
|
||||||
"moduleResolution": "node"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,12 +1,12 @@
|
|||||||
<!DOCTYPE html>
|
<!doctype html>
|
||||||
<html lang="en">
|
<html lang="en">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8" />
|
<meta charset="UTF-8" />
|
||||||
|
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
||||||
<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 class="dark text-foreground bg-background">
|
<body>
|
||||||
<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>
|
||||||
|
|||||||
@@ -11,14 +11,9 @@
|
|||||||
},
|
},
|
||||||
"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"
|
||||||
"react-json-tree": "^0.18.0",
|
|
||||||
"react-router-dom": "^6.16.0",
|
|
||||||
"tailwindcss": "^3.3.3"
|
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@mdx-js/rollup": "^2.3.0",
|
"@mdx-js/rollup": "^2.3.0",
|
||||||
@@ -28,11 +23,9 @@
|
|||||||
"@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"
|
||||||
|
|||||||
@@ -1,6 +0,0 @@
|
|||||||
export default {
|
|
||||||
plugins: {
|
|
||||||
tailwindcss: {},
|
|
||||||
autoprefixer: {},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
@@ -1,14 +1,7 @@
|
|||||||
import { NextUIProvider } from '@nextui-org/react';
|
import { Page } from './containers/page';
|
||||||
import { Router } from './router';
|
|
||||||
|
|
||||||
const App: React.FC = () => {
|
const App: React.FC = () => {
|
||||||
return (
|
return <Page slug=".hello" />;
|
||||||
<div className="dark text-foreground bg-background">
|
|
||||||
<NextUIProvider>
|
|
||||||
<Router />
|
|
||||||
</NextUIProvider>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export { App };
|
export { App };
|
||||||
|
|||||||
@@ -1,13 +0,0 @@
|
|||||||
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 };
|
|
||||||
@@ -1,41 +0,0 @@
|
|||||||
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 };
|
|
||||||
@@ -1,97 +0,0 @@
|
|||||||
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 };
|
|
||||||
@@ -1,32 +0,0 @@
|
|||||||
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 };
|
|
||||||
@@ -1,67 +0,0 @@
|
|||||||
import { useEffect, useState } from 'react';
|
|
||||||
import { ExperimentProvider } from '../../features/experiment/context';
|
|
||||||
import { ExperimentView } from './view';
|
|
||||||
import { ExperimentInfo } from '../../features/experiment/types';
|
|
||||||
|
|
||||||
type PageProps = {
|
|
||||||
content: () => Promise<{ default: Experiment }>;
|
|
||||||
};
|
|
||||||
|
|
||||||
type Experiment = {
|
|
||||||
info: ExperimentInfo;
|
|
||||||
view: React.ComponentType;
|
|
||||||
};
|
|
||||||
|
|
||||||
const ExperimentPage: React.FC<PageProps> = ({ content }) => {
|
|
||||||
const [loading, setLoading] = useState(true);
|
|
||||||
const [error, setError] = useState<unknown>();
|
|
||||||
const [experiment, setExperiment] = useState<Experiment>();
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
setLoading(true);
|
|
||||||
setError(undefined);
|
|
||||||
const load = async () => {
|
|
||||||
try {
|
|
||||||
const { default: next } = (await content()) as {
|
|
||||||
default: Experiment;
|
|
||||||
};
|
|
||||||
console.log('n', next);
|
|
||||||
setExperiment(next);
|
|
||||||
} catch (err) {
|
|
||||||
setError(err);
|
|
||||||
} finally {
|
|
||||||
setLoading(false);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
load();
|
|
||||||
}, [content]);
|
|
||||||
|
|
||||||
if (error) {
|
|
||||||
return <div>Error: {error.toString()}</div>;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (loading || !experiment) {
|
|
||||||
return <div>Loading...</div>;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<ExperimentProvider experimentInfo={experiment.info}>
|
|
||||||
<ExperimentView>
|
|
||||||
<experiment.view />
|
|
||||||
</ExperimentView>
|
|
||||||
</ExperimentProvider>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
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 };
|
|
||||||
@@ -1,116 +0,0 @@
|
|||||||
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 };
|
|
||||||
@@ -1,46 +0,0 @@
|
|||||||
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 };
|
|
||||||
@@ -1,44 +0,0 @@
|
|||||||
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 };
|
|
||||||
@@ -1,17 +0,0 @@
|
|||||||
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 };
|
|
||||||
@@ -1,35 +0,0 @@
|
|||||||
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 { Stats } from './stats';
|
|
||||||
|
|
||||||
type ExperimentViewProps = {
|
|
||||||
children: React.ReactNode;
|
|
||||||
};
|
|
||||||
|
|
||||||
const ExperimentView: React.FC<ExperimentViewProps> = ({ children }) => {
|
|
||||||
return (
|
|
||||||
<Frame>
|
|
||||||
<div className="flex flex-row h-full">
|
|
||||||
<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 />
|
|
||||||
</div>
|
|
||||||
</Frame>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export { ExperimentView };
|
|
||||||
51
packages/playground/src/containers/page.tsx
Normal file
51
packages/playground/src/containers/page.tsx
Normal file
@@ -0,0 +1,51 @@
|
|||||||
|
import { useEffect, useState } from 'react';
|
||||||
|
import { pages } from '../utils/pages';
|
||||||
|
import { RunnerProvider } from '../features/runner';
|
||||||
|
type PageProps = {
|
||||||
|
slug: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
const Page: React.FC<PageProps> = ({ slug }) => {
|
||||||
|
const [Component, setComponent] = useState<React.FC>();
|
||||||
|
const [loading, setLoading] = useState(true);
|
||||||
|
const [error, setError] = useState<unknown>();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setLoading(true);
|
||||||
|
setError(undefined);
|
||||||
|
const load = async () => {
|
||||||
|
try {
|
||||||
|
const page = pages.find((page) => page.slug === slug);
|
||||||
|
if (!page) {
|
||||||
|
throw new Error(`Page not found: ${slug}`);
|
||||||
|
}
|
||||||
|
const { default: Component } = (await page.loader()) as {
|
||||||
|
default: React.FC;
|
||||||
|
};
|
||||||
|
setComponent(() => Component);
|
||||||
|
} catch (err) {
|
||||||
|
setError(err);
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
load();
|
||||||
|
}, [slug]);
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
return <div>Error: {error.toString()}</div>;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (loading || !Component) {
|
||||||
|
return <div>Loading...</div>;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<RunnerProvider>
|
||||||
|
<Component />
|
||||||
|
</RunnerProvider>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export { Page };
|
||||||
@@ -1,15 +0,0 @@
|
|||||||
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 };
|
|
||||||
@@ -1,38 +0,0 @@
|
|||||||
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 };
|
|
||||||
@@ -1,163 +0,0 @@
|
|||||||
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 };
|
|
||||||
@@ -1,6 +0,0 @@
|
|||||||
import { info } from './experiment';
|
|
||||||
|
|
||||||
export default {
|
|
||||||
info,
|
|
||||||
view: () => <>Hello</>,
|
|
||||||
};
|
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
# Hello
|
|
||||||
|
|
||||||
world
|
|
||||||
@@ -1,204 +0,0 @@
|
|||||||
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 };
|
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
import { info } from './experiment';
|
|
||||||
const Foo = () => {
|
|
||||||
return <div></div>;
|
|
||||||
};
|
|
||||||
|
|
||||||
export default {
|
|
||||||
info,
|
|
||||||
view: Foo,
|
|
||||||
};
|
|
||||||
@@ -1,111 +0,0 @@
|
|||||||
import {
|
|
||||||
AllPlugins,
|
|
||||||
CalulationResult,
|
|
||||||
GraphNode,
|
|
||||||
} from '@bob-the-algorithm/core';
|
|
||||||
import {
|
|
||||||
ReactNode,
|
|
||||||
createContext,
|
|
||||||
useCallback,
|
|
||||||
useEffect,
|
|
||||||
useMemo,
|
|
||||||
useState,
|
|
||||||
} from 'react';
|
|
||||||
import { ExperimentInfo } from './types';
|
|
||||||
|
|
||||||
type ExperimentResult = {
|
|
||||||
payload: CalulationResult<AllPlugins>;
|
|
||||||
duration: number;
|
|
||||||
};
|
|
||||||
|
|
||||||
type ExperimentContextValue = {
|
|
||||||
result?: ExperimentResult;
|
|
||||||
error?: any;
|
|
||||||
loading: boolean;
|
|
||||||
selectNode: (node?: GraphNode) => void;
|
|
||||||
selectedNode?: GraphNode;
|
|
||||||
};
|
|
||||||
|
|
||||||
type ExperimentProviderProps = {
|
|
||||||
children: ReactNode;
|
|
||||||
experimentInfo: ExperimentInfo;
|
|
||||||
};
|
|
||||||
|
|
||||||
const createWorker = () =>
|
|
||||||
new Worker(new URL('./worker.ts', import.meta.url), {
|
|
||||||
type: 'module',
|
|
||||||
});
|
|
||||||
|
|
||||||
const ExperimentContext = createContext<ExperimentContextValue>({
|
|
||||||
loading: false,
|
|
||||||
selectNode: () => { },
|
|
||||||
});
|
|
||||||
|
|
||||||
const ExperimentProvider: React.FC<ExperimentProviderProps> = ({
|
|
||||||
children,
|
|
||||||
experimentInfo,
|
|
||||||
}) => {
|
|
||||||
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 = createWorker();
|
|
||||||
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',
|
|
||||||
payload: experimentInfo,
|
|
||||||
});
|
|
||||||
};
|
|
||||||
run();
|
|
||||||
return () => {
|
|
||||||
workerInstance?.terminate();
|
|
||||||
};
|
|
||||||
}, [experimentInfo]);
|
|
||||||
|
|
||||||
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 };
|
|
||||||
@@ -1,29 +0,0 @@
|
|||||||
import { useContext } from 'react';
|
|
||||||
import { ExperimentContext } from './context';
|
|
||||||
|
|
||||||
const useExperimentResult = () => {
|
|
||||||
const { result } = useContext(ExperimentContext);
|
|
||||||
return result?.payload;
|
|
||||||
};
|
|
||||||
|
|
||||||
const useExperimentDuration = () => {
|
|
||||||
const { result } = useContext(ExperimentContext);
|
|
||||||
return result?.duration;
|
|
||||||
};
|
|
||||||
|
|
||||||
const useSelectNode = () => {
|
|
||||||
const { selectNode } = useContext(ExperimentContext);
|
|
||||||
return selectNode;
|
|
||||||
};
|
|
||||||
|
|
||||||
const useSelectedNode = () => {
|
|
||||||
const { selectedNode } = useContext(ExperimentContext);
|
|
||||||
return selectedNode;
|
|
||||||
};
|
|
||||||
|
|
||||||
export {
|
|
||||||
useExperimentResult,
|
|
||||||
useSelectNode,
|
|
||||||
useSelectedNode,
|
|
||||||
useExperimentDuration,
|
|
||||||
};
|
|
||||||
@@ -1,2 +0,0 @@
|
|||||||
export { ExperimentProvider as Experminent } from './context';
|
|
||||||
export { useExperimentResult } from './hooks';
|
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
import { AllPlugins, PlanableWithPlugins } from '@bob-the-algorithm/core';
|
|
||||||
|
|
||||||
type ExperimentInfo = {
|
|
||||||
planables: PlanableWithPlugins<AllPlugins>[];
|
|
||||||
context: any;
|
|
||||||
start: number;
|
|
||||||
};
|
|
||||||
|
|
||||||
export type { ExperimentInfo };
|
|
||||||
@@ -1,38 +0,0 @@
|
|||||||
import { Bob, plugins } from '@bob-the-algorithm/core';
|
|
||||||
import { ExperimentInfo } from './types';
|
|
||||||
|
|
||||||
const MIN = 1000 * 60;
|
|
||||||
|
|
||||||
const getTravelTime = async () => 30 * MIN;
|
|
||||||
const transport = plugins.transport({
|
|
||||||
getTravelTime,
|
|
||||||
});
|
|
||||||
|
|
||||||
const run = async (payload: ExperimentInfo) => {
|
|
||||||
const startTime = performance.now();
|
|
||||||
try {
|
|
||||||
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 duration = endTime - startTime;
|
|
||||||
self.postMessage({ type: 'output', payload: result, duration });
|
|
||||||
} catch (error) {
|
|
||||||
self.postMessage({ type: 'error', payload: error });
|
|
||||||
console.error(error);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
self.addEventListener('message', (event) => {
|
|
||||||
const { type, payload } = event.data;
|
|
||||||
if (type === 'run') {
|
|
||||||
run(payload);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
98
packages/playground/src/features/runner/block.tsx
Normal file
98
packages/playground/src/features/runner/block.tsx
Normal file
@@ -0,0 +1,98 @@
|
|||||||
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
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 };
|
||||||
42
packages/playground/src/features/runner/context.tsx
Normal file
42
packages/playground/src/features/runner/context.tsx
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
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 };
|
||||||
3
packages/playground/src/features/runner/index.ts
Normal file
3
packages/playground/src/features/runner/index.ts
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
export { RunnerProvider } from './context';
|
||||||
|
export { Block } from './block';
|
||||||
|
export { createWorker } from './worker';
|
||||||
21
packages/playground/src/features/runner/worker.ts
Normal file
21
packages/playground/src/features/runner/worker.ts
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
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 };
|
||||||
@@ -1,10 +0,0 @@
|
|||||||
@tailwind base;
|
|
||||||
@tailwind components;
|
|
||||||
@tailwind utilities;
|
|
||||||
|
|
||||||
html,
|
|
||||||
body,
|
|
||||||
#root,
|
|
||||||
#root > div {
|
|
||||||
height: 100%;
|
|
||||||
}
|
|
||||||
@@ -1,53 +0,0 @@
|
|||||||
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 };
|
|
||||||
7
packages/playground/src/pages/hello/index.mdx
Normal file
7
packages/playground/src/pages/hello/index.mdx
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
import { Block } from '../../features/runner'
|
||||||
|
import { worker } from './worker';
|
||||||
|
import { Presenter } from '../../presenters/graph';
|
||||||
|
|
||||||
|
# Hello World
|
||||||
|
|
||||||
|
<Block worker={worker} action="realistic" presenter={Presenter} />
|
||||||
148
packages/playground/src/pages/hello/script.ts
Normal file
148
packages/playground/src/pages/hello/script.ts
Normal file
@@ -0,0 +1,148 @@
|
|||||||
|
import { calulation, plugins } from '@bob-the-algorithm/core';
|
||||||
|
import { createWorker } from '../../features/runner/worker';
|
||||||
|
import { convertResult } from '../../utils/graph';
|
||||||
|
|
||||||
|
const MIN = 1000 * 60;
|
||||||
|
const HOUR = 1000 * 60 * 60;
|
||||||
|
|
||||||
|
const getTravelTime = async () => 30 * MIN;
|
||||||
|
|
||||||
|
const realistic = async () => {
|
||||||
|
const result = await calulation({
|
||||||
|
location: 'home',
|
||||||
|
time: 0,
|
||||||
|
heuristic: ({ completed }) => completed.length >= 3,
|
||||||
|
plugins: [
|
||||||
|
plugins.transport({
|
||||||
|
getTravelTime,
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
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 convertResult(result);
|
||||||
|
};
|
||||||
|
|
||||||
|
createWorker({
|
||||||
|
realistic,
|
||||||
|
});
|
||||||
5
packages/playground/src/pages/hello/worker.ts
Normal file
5
packages/playground/src/pages/hello/worker.ts
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
const worker = new Worker(new URL('./script.ts', import.meta.url), {
|
||||||
|
type: 'module',
|
||||||
|
});
|
||||||
|
|
||||||
|
export { worker };
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
const Experiments = () => {
|
|
||||||
return <div>Welcome</div>;
|
|
||||||
};
|
|
||||||
|
|
||||||
export { Experiments as Page };
|
|
||||||
@@ -1,92 +1,139 @@
|
|||||||
import { useMemo } from 'react';
|
import { useMemo, useState } from 'react';
|
||||||
import { GraphCanvas } from 'reagraph';
|
import { GraphCanvas } from 'reagraph';
|
||||||
import { useExperimentResult } from '../../features/experiment';
|
import { ConvertedResult } from '../../utils/graph';
|
||||||
import { convertResult } from '../../utils/graph';
|
import { Plan } from './plan';
|
||||||
import {
|
|
||||||
useSelectNode,
|
|
||||||
useSelectedNode,
|
|
||||||
} from '../../features/experiment/hooks';
|
|
||||||
|
|
||||||
const Graph: React.FC = () => {
|
type PresenterProps = {
|
||||||
const data = useExperimentResult();
|
output: ConvertedResult;
|
||||||
const selectedNode = useSelectedNode();
|
};
|
||||||
const selectNode = useSelectNode();
|
|
||||||
const output = useMemo(() => {
|
const Presenter: React.FC<PresenterProps> = ({ output }) => {
|
||||||
if (!data) {
|
const [currentStep, setCurrentStep] = useState(0);
|
||||||
return undefined;
|
const [visualize, setVisualize] = useState(false);
|
||||||
}
|
const [selectedNode, setSelectedNode] = useState<string | undefined>(
|
||||||
return convertResult(data);
|
undefined,
|
||||||
}, [data]);
|
);
|
||||||
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.id);
|
let current = output.result.nodes.find((n) => n.id === selectedNode);
|
||||||
|
|
||||||
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}
|
||||||
<div style={{ position: 'relative', height: '70vh' }}>
|
<button onClick={() => setVisualize(!visualize)}>
|
||||||
<GraphCanvas
|
{visualize ? 'Hide' : 'Show'} Visualize
|
||||||
{...output}
|
</button>
|
||||||
labelType="all"
|
{visualize && (
|
||||||
layoutType="hierarchicalTd"
|
<>
|
||||||
onNodeClick={(node) => {
|
<button onClick={() => setCurrentStep(currentStep - 1)}>Prev</button>
|
||||||
if (node.id === selectedNode?.id) {
|
<input
|
||||||
selectNode(undefined);
|
type="range"
|
||||||
return;
|
min={0}
|
||||||
}
|
max={maxStep}
|
||||||
const nextNode = data?.nodes.find((n) => n.id === node.id);
|
value={currentStep}
|
||||||
selectNode(nextNode);
|
onChange={(e) => setCurrentStep(parseInt(e.target.value))}
|
||||||
}}
|
/>
|
||||||
selections={selectedPath}
|
<button onClick={() => setCurrentStep(currentStep + 1)}>Next</button>
|
||||||
renderNode={({ size, opacity, node }) => {
|
</>
|
||||||
let color = 'gray';
|
)}
|
||||||
if (node.data?.deadEnd) {
|
{completed.map((c) => (
|
||||||
color = 'red';
|
<div key={c.id} onClick={() => setSelectedNode(c.id)}>
|
||||||
}
|
{c.id} - {c.score}
|
||||||
if (node.data?.completed) {
|
</div>
|
||||||
color = 'green';
|
))}
|
||||||
}
|
{selectedNode && <Plan id={selectedNode} output={output} />}
|
||||||
if (node.data?.type === 'root') {
|
{visualize && (
|
||||||
color = 'black';
|
<div style={{ position: 'relative', height: '70vh' }}>
|
||||||
}
|
<GraphCanvas
|
||||||
return (
|
{...output}
|
||||||
<group>
|
collapsedNodeIds={collapsedNodeIds}
|
||||||
<mesh>
|
labelType="all"
|
||||||
<circleGeometry attach="geometry" args={[size]} />
|
onNodeClick={(node) => {
|
||||||
<meshBasicMaterial
|
if (node.id === selectedNode) {
|
||||||
attach="material"
|
setSelectedNode(undefined);
|
||||||
color={color}
|
return;
|
||||||
opacity={opacity}
|
}
|
||||||
transparent
|
setSelectedNode(node.id);
|
||||||
/>
|
}}
|
||||||
</mesh>
|
selections={selectedPath}
|
||||||
</group>
|
renderNode={({ size, opacity, node }) => {
|
||||||
);
|
let color = 'gray';
|
||||||
}}
|
if (
|
||||||
/>
|
node.data?.exploreId < currentStep &&
|
||||||
</div>
|
node.data?.exploreId > 0
|
||||||
|
) {
|
||||||
|
color = 'yellow';
|
||||||
|
}
|
||||||
|
if (node.data?.exploreId === currentStep) {
|
||||||
|
color = 'blue';
|
||||||
|
}
|
||||||
|
if (node.data?.deadEnd) {
|
||||||
|
color = 'red';
|
||||||
|
}
|
||||||
|
if (node.data?.completed) {
|
||||||
|
color = 'green';
|
||||||
|
}
|
||||||
|
if (node.data?.type === 'root') {
|
||||||
|
color = 'black';
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<group>
|
||||||
|
<mesh>
|
||||||
|
<circleGeometry attach="geometry" args={[size]} />
|
||||||
|
<meshBasicMaterial
|
||||||
|
attach="material"
|
||||||
|
color={color}
|
||||||
|
opacity={opacity}
|
||||||
|
transparent
|
||||||
|
/>
|
||||||
|
</mesh>
|
||||||
|
</group>
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export { Graph };
|
export { Presenter };
|
||||||
|
|||||||
76
packages/playground/src/presenters/graph/plan.tsx
Normal file
76
packages/playground/src/presenters/graph/plan.tsx
Normal file
@@ -0,0 +1,76 @@
|
|||||||
|
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.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 };
|
||||||
@@ -1,90 +0,0 @@
|
|||||||
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 };
|
|
||||||
@@ -1,74 +0,0 @@
|
|||||||
import { GraphNode } from '@bob-the-algorithm/core';
|
|
||||||
import { useMemo } from 'react';
|
|
||||||
import { Listbox, ListboxItem, cn } from '@nextui-org/react';
|
|
||||||
import { useExperimentResult } from '../../features/experiment';
|
|
||||||
import { formatTime } from '../../utils/time';
|
|
||||||
|
|
||||||
type PlanProps = {
|
|
||||||
node: GraphNode;
|
|
||||||
onSelect: (node: GraphNode) => void;
|
|
||||||
};
|
|
||||||
|
|
||||||
const Plan: React.FC<PlanProps> = ({ node, onSelect }) => {
|
|
||||||
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.reverse();
|
|
||||||
}, [data, node]);
|
|
||||||
|
|
||||||
if (!data) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Listbox
|
|
||||||
variant="flat"
|
|
||||||
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>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export { Plan };
|
|
||||||
@@ -1,18 +0,0 @@
|
|||||||
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 };
|
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
import { AllPlugins, CalulationResult } from '@bob-the-algorithm/core';
|
import { 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<AllPlugins>) => {
|
const convertResult = (result: CalulationResult<any>) => {
|
||||||
const nodes = result.nodes.map((node) => {
|
const nodes = result.nodes.map((node) => {
|
||||||
let label = `root (${node.context.location})`;
|
let label = `root (${node.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.context.location}`;
|
label = `travel->${node.location}`;
|
||||||
}
|
}
|
||||||
return {
|
return {
|
||||||
id: node.id,
|
id: node.id,
|
||||||
|
|||||||
8
packages/playground/src/utils/pages.ts
Normal file
8
packages/playground/src/utils/pages.ts
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
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 };
|
||||||
@@ -1,6 +0,0 @@
|
|||||||
const formatTime = (time: number) => {
|
|
||||||
const toUtc = new Date(time).toUTCString();
|
|
||||||
return toUtc.slice(17, 22);
|
|
||||||
};
|
|
||||||
|
|
||||||
export { formatTime };
|
|
||||||
@@ -1,19 +0,0 @@
|
|||||||
/** @type {import('tailwindcss').Config} */
|
|
||||||
const path = require('path');
|
|
||||||
|
|
||||||
const { nextui } = require('@nextui-org/react');
|
|
||||||
const themePath = require.resolve('@nextui-org/theme/package.json');
|
|
||||||
const themeDir = path.dirname(themePath);
|
|
||||||
|
|
||||||
export default {
|
|
||||||
content: [
|
|
||||||
'./index.html',
|
|
||||||
'./src/**/*.{js,ts,jsx,tsx}',
|
|
||||||
`${themeDir}/dist/**/*.{js,ts,jsx,tsx}`,
|
|
||||||
],
|
|
||||||
theme: {
|
|
||||||
extend: {},
|
|
||||||
},
|
|
||||||
darkMode: 'class',
|
|
||||||
plugins: [nextui()],
|
|
||||||
};
|
|
||||||
@@ -6,6 +6,6 @@ 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}/dist/`,
|
||||||
plugins: [mdx(), react()],
|
plugins: [mdx(), react()],
|
||||||
});
|
});
|
||||||
2675
pnpm-lock.yaml
generated
2675
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user