mirror of
https://github.com/morten-olsen/bob.git
synced 2026-02-08 01:46:29 +01:00
feat: init
This commit is contained in:
12
.eslintrc
Normal file
12
.eslintrc
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
{
|
||||||
|
"extends": "@react-native-community",
|
||||||
|
"rules": {
|
||||||
|
"react/react-in-jsx-scope": 0,
|
||||||
|
"prettier/prettier": [
|
||||||
|
"error",
|
||||||
|
{
|
||||||
|
"singleQuote": true
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
53
.github/release-drafter-config.yml
vendored
Normal file
53
.github/release-drafter-config.yml
vendored
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
name-template: '$RESOLVED_VERSION 🚀'
|
||||||
|
tag-template: '$RESOLVED_VERSION'
|
||||||
|
categories:
|
||||||
|
- title: '🚀 Features'
|
||||||
|
labels:
|
||||||
|
- 'feature'
|
||||||
|
- 'enhancement'
|
||||||
|
- title: '🐛 Bug Fixes'
|
||||||
|
labels:
|
||||||
|
- 'fix'
|
||||||
|
- 'bugfix'
|
||||||
|
- 'bug'
|
||||||
|
- title: '🧰 Maintenance'
|
||||||
|
label: 'chore'
|
||||||
|
change-template: '- $TITLE @$AUTHOR (#$NUMBER)'
|
||||||
|
change-title-escapes: '\<*_&' # You can add # and @ to disable mentions, and add ` to disable code blocks.
|
||||||
|
version-resolver:
|
||||||
|
major:
|
||||||
|
labels:
|
||||||
|
- 'major'
|
||||||
|
minor:
|
||||||
|
labels:
|
||||||
|
- 'enhancement'
|
||||||
|
- 'feature'
|
||||||
|
patch:
|
||||||
|
labels:
|
||||||
|
- 'bug'
|
||||||
|
- 'chore'
|
||||||
|
- 'fix'
|
||||||
|
- 'bugfix'
|
||||||
|
default: patch
|
||||||
|
autolabeler:
|
||||||
|
- label: 'chore'
|
||||||
|
files:
|
||||||
|
- '*.md'
|
||||||
|
branch:
|
||||||
|
- '/docs{0,1}\/.+/'
|
||||||
|
- label: 'bug'
|
||||||
|
title:
|
||||||
|
- '/fix/i'
|
||||||
|
body:
|
||||||
|
- '/fix/i'
|
||||||
|
- label: 'enhancement'
|
||||||
|
title:
|
||||||
|
- '/feature/i'
|
||||||
|
- '/feat/i'
|
||||||
|
body:
|
||||||
|
- '/feature/i'
|
||||||
|
- '/feat/i'
|
||||||
|
template: |
|
||||||
|
## Changes
|
||||||
|
|
||||||
|
$CHANGES
|
||||||
21
.github/workflows/auto-label.yml
vendored
Normal file
21
.github/workflows/auto-label.yml
vendored
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
name: Auto Labeler
|
||||||
|
on:
|
||||||
|
pull_request:
|
||||||
|
types: [opened, reopened, synchronize]
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
auto-labeler:
|
||||||
|
permissions:
|
||||||
|
contents: write
|
||||||
|
pull-requests: write
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: release-drafter/release-drafter@v5
|
||||||
|
with:
|
||||||
|
config-name: release-drafter-config.yml
|
||||||
|
disable-releaser: true
|
||||||
|
env:
|
||||||
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
100
.github/workflows/release-package.yml
vendored
Normal file
100
.github/workflows/release-package.yml
vendored
Normal file
@@ -0,0 +1,100 @@
|
|||||||
|
name: Node.js Package
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
pull_request:
|
||||||
|
types: [opened]
|
||||||
|
# release:
|
||||||
|
# types: [created]
|
||||||
|
|
||||||
|
env:
|
||||||
|
NODE_CACHE: 'pnpm'
|
||||||
|
NODE_VERSION: '20.x'
|
||||||
|
NODE_SCOPE: '@bob-the-algorithm'
|
||||||
|
NPM_GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
packages: read
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
test:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v3
|
||||||
|
- uses: actions/setup-node@v3
|
||||||
|
with:
|
||||||
|
cache: '${{ env.NODE_CACHE }}'
|
||||||
|
node-version: '${{ env.NODE_VERSION }}'
|
||||||
|
scope: '${{ env.NODE_SCOPE }}'
|
||||||
|
- run: |
|
||||||
|
corepack enable
|
||||||
|
pnpm install
|
||||||
|
pnpm run ci:test
|
||||||
|
build:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
needs: test
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v3
|
||||||
|
- uses: actions/setup-node@v3
|
||||||
|
with:
|
||||||
|
cache: '${{ env.NODE_CACHE }}'
|
||||||
|
node-version: '${{ env.NODE_VERSION }}'
|
||||||
|
registry-url: '${{ env.NODE_REGISTRY_URL }}'
|
||||||
|
scope: '${{ env.NODE_SCOPE }}'
|
||||||
|
|
||||||
|
- run: |
|
||||||
|
corepack enable
|
||||||
|
pnpm install
|
||||||
|
pnpm run build
|
||||||
|
- uses: actions/upload-artifact@v3
|
||||||
|
with:
|
||||||
|
name: lib
|
||||||
|
retention-days: 5
|
||||||
|
path: |
|
||||||
|
packages/*/dist
|
||||||
|
packages/*/package.json
|
||||||
|
package.json
|
||||||
|
README.md
|
||||||
|
update-release-draft:
|
||||||
|
if: github.ref == 'refs/heads/main'
|
||||||
|
permissions:
|
||||||
|
contents: write
|
||||||
|
pull-requests: write
|
||||||
|
packages: write
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: release-drafter/release-drafter@v5
|
||||||
|
with:
|
||||||
|
config-name: release-drafter-config.yml
|
||||||
|
publish: true
|
||||||
|
env:
|
||||||
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
release:
|
||||||
|
if: github.ref == 'refs/heads/main'
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
needs: [build, update-release-draft]
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
packages: write
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v3
|
||||||
|
with:
|
||||||
|
fetch-depth: 0
|
||||||
|
- uses: actions/setup-node@v3
|
||||||
|
with:
|
||||||
|
cache: '${{ env.NODE_CACHE }}'
|
||||||
|
node-version: '${{ env.NODE_VERSION }}'
|
||||||
|
scope: '${{ env.NODE_SCOPE }}'
|
||||||
|
- uses: actions/download-artifact@v3
|
||||||
|
with:
|
||||||
|
name: lib
|
||||||
|
path: ./
|
||||||
|
- run: |
|
||||||
|
pnpm install
|
||||||
|
git config user.name "Github Actions Bot"
|
||||||
|
git config user.email "<>"
|
||||||
|
pnpm version newversion $(git describe --tag --abbrev=0) --no-git-tag-version
|
||||||
|
pnpm publish
|
||||||
|
env:
|
||||||
|
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
|
||||||
3
.gitignore
vendored
Normal file
3
.gitignore
vendored
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
/node_modules/
|
||||||
|
/.pnpm-store/
|
||||||
|
/.turbo/
|
||||||
13
.prettierrc.json
Normal file
13
.prettierrc.json
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
{
|
||||||
|
"semi": true,
|
||||||
|
"tabWidth": 2,
|
||||||
|
"useTabs": false,
|
||||||
|
"singleQuote": true,
|
||||||
|
"jsxSingleQuote": false,
|
||||||
|
"bracketSameLine": false,
|
||||||
|
"arrowParens": "always",
|
||||||
|
"htmlWhitespaceSensitivity": "css",
|
||||||
|
"bracketSpacing": true,
|
||||||
|
"quoteProps": "as-needed",
|
||||||
|
"trailingComma": "all"
|
||||||
|
}
|
||||||
6
.yo-rc.json
Normal file
6
.yo-rc.json
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
{
|
||||||
|
"generator-x-repo": {
|
||||||
|
"root": "@morten-olsen/bob",
|
||||||
|
"config": "@morten-olsen/bob-config"
|
||||||
|
}
|
||||||
|
}
|
||||||
18
package.json
Normal file
18
package.json
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
{
|
||||||
|
"dependencies": {
|
||||||
|
"@react-native-community/eslint-config": "^3.2.0",
|
||||||
|
"eslint": "^8.33.0",
|
||||||
|
"prettier": "^2.8.3",
|
||||||
|
"turbo": "^1.9.9"
|
||||||
|
},
|
||||||
|
"private": true,
|
||||||
|
"scripts": {
|
||||||
|
"build": "turbo build",
|
||||||
|
"dev": "turbo dev",
|
||||||
|
"lint": "eslint packages/*/src",
|
||||||
|
"start": "turbo start",
|
||||||
|
"test": "turbo test"
|
||||||
|
},
|
||||||
|
"version": "0.0.1",
|
||||||
|
"name": "@bob-the-algorithm/repo"
|
||||||
|
}
|
||||||
2
packages/algorithm/.gitignore
vendored
Normal file
2
packages/algorithm/.gitignore
vendored
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
/node_modules/
|
||||||
|
/dist/
|
||||||
4
packages/algorithm/.turbo/turbo-build.log
Normal file
4
packages/algorithm/.turbo/turbo-build.log
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
|
||||||
|
> bob-the-algorithm@ build /Users/alice/work/private/bob/packages/algorithm
|
||||||
|
> tsc -p tsconfig.json
|
||||||
|
|
||||||
10
packages/algorithm/configs/tsconfig.cjs.json
Normal file
10
packages/algorithm/configs/tsconfig.cjs.json
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"declarationDir": "../dist/cjs/types",
|
||||||
|
"outDir": "../dist/cjs"
|
||||||
|
},
|
||||||
|
"extends": "@morten-olsen/bob-config/tsconfig.cjs.json",
|
||||||
|
"include": [
|
||||||
|
"../src/**/*"
|
||||||
|
]
|
||||||
|
}
|
||||||
10
packages/algorithm/configs/tsconfig.esm.json
Normal file
10
packages/algorithm/configs/tsconfig.esm.json
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"declarationDir": "../dist/esm/types",
|
||||||
|
"outDir": "../dist/esm"
|
||||||
|
},
|
||||||
|
"extends": "@morten-olsen/bob-config/tsconfig.esm.json",
|
||||||
|
"include": [
|
||||||
|
"../src/**/*"
|
||||||
|
]
|
||||||
|
}
|
||||||
11
packages/algorithm/configs/tsconfig.libs.json
Normal file
11
packages/algorithm/configs/tsconfig.libs.json
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
{
|
||||||
|
"include": [],
|
||||||
|
"references": [
|
||||||
|
{
|
||||||
|
"path": "./tsconfig.cjs.json"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "./tsconfig.esm.json"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
29
packages/algorithm/package.json
Normal file
29
packages/algorithm/package.json
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
{
|
||||||
|
"devDependencies": {
|
||||||
|
"@bob-the-algorithm/config": "workspace:^",
|
||||||
|
"typescript": "^5.0.4"
|
||||||
|
},
|
||||||
|
"exports": {
|
||||||
|
".": {
|
||||||
|
"import": {
|
||||||
|
"default": "./dist/esm/index.js",
|
||||||
|
"types": "./dist/esm/types/index.d.ts"
|
||||||
|
},
|
||||||
|
"require": {
|
||||||
|
"default": "./dist/cjs/index.js",
|
||||||
|
"types": "./dist/cjs/types/index.d.ts"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"files": [
|
||||||
|
"dist/**/*"
|
||||||
|
],
|
||||||
|
"main": "./dist/cjs/index.js",
|
||||||
|
"name": "bob-the-algorithm",
|
||||||
|
"scripts": {
|
||||||
|
"build": "tsc -p tsconfig.json",
|
||||||
|
"build:cjs": "tsc -p configs/tsconfig.cjs.json",
|
||||||
|
"build:esm": "tsc -p configs/tsconfig.esm.json"
|
||||||
|
},
|
||||||
|
"types": "./dist/cjs/types/index.d.ts"
|
||||||
|
}
|
||||||
87
packages/algorithm/src/algorithm/calulation.ts
Normal file
87
packages/algorithm/src/algorithm/calulation.ts
Normal file
@@ -0,0 +1,87 @@
|
|||||||
|
import { Attributes, GraphNode } from '../types/node';
|
||||||
|
import { Planable } from '../types/planable';
|
||||||
|
import { PluginAttributes, Plugins } from '../types/plugin';
|
||||||
|
import { expandNode } from './expand-node';
|
||||||
|
|
||||||
|
type CalulationOptions<TPlugins extends Plugins> = {
|
||||||
|
location: string;
|
||||||
|
time: number;
|
||||||
|
planables: Planable<PluginAttributes<TPlugins>>[];
|
||||||
|
plugins: TPlugins;
|
||||||
|
heuristic?: (result: any) => boolean;
|
||||||
|
onUpdated?: (result: any) => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
type CalulationResult<TAttributes extends Attributes> = {
|
||||||
|
root: GraphNode<TAttributes>;
|
||||||
|
nodes: GraphNode<TAttributes>[];
|
||||||
|
completed: GraphNode<TAttributes>[];
|
||||||
|
planables: Planable<TAttributes>[];
|
||||||
|
};
|
||||||
|
|
||||||
|
const idGen = () => {
|
||||||
|
let id = 0;
|
||||||
|
return () => {
|
||||||
|
id += 1;
|
||||||
|
return id.toString();
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const calulation = async <TPlugins extends Plugins>({
|
||||||
|
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 });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return { root, nodes, completed, planables };
|
||||||
|
};
|
||||||
|
|
||||||
|
export type { CalulationOptions, CalulationResult };
|
||||||
|
export { calulation };
|
||||||
83
packages/algorithm/src/algorithm/expand-node.ts
Normal file
83
packages/algorithm/src/algorithm/expand-node.ts
Normal file
@@ -0,0 +1,83 @@
|
|||||||
|
import { Attributes, GraphNode } from '../types/node';
|
||||||
|
import { Plugin } from '../types/plugin';
|
||||||
|
import { hasImpossible } from './is-impossible';
|
||||||
|
|
||||||
|
type ExpandOptions<TAttributes extends Attributes> = {
|
||||||
|
node: GraphNode<TAttributes>;
|
||||||
|
generateId: () => string;
|
||||||
|
plugins: Plugin[];
|
||||||
|
};
|
||||||
|
|
||||||
|
const expandNode = async <TAttributes extends Attributes>({
|
||||||
|
node,
|
||||||
|
generateId,
|
||||||
|
plugins,
|
||||||
|
}: ExpandOptions<TAttributes>): Promise<GraphNode<TAttributes>[]> => {
|
||||||
|
const isImpossible = hasImpossible({ node });
|
||||||
|
|
||||||
|
if (isImpossible) {
|
||||||
|
node.deadEnd = true;
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
const metaNodes = await Promise.all(
|
||||||
|
plugins.map(async (plugin) => {
|
||||||
|
if (!plugin.getMetaNodes) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
const pluginNodes = await plugin.getMetaNodes(node);
|
||||||
|
return pluginNodes.map(
|
||||||
|
(pluginNode) =>
|
||||||
|
({
|
||||||
|
...pluginNode,
|
||||||
|
parent: node.id,
|
||||||
|
exploreId: 0,
|
||||||
|
id: generateId(),
|
||||||
|
} as GraphNode<TAttributes>),
|
||||||
|
);
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
const planables = node.remaining.filter((planable) => {
|
||||||
|
const hasNonPlanable = plugins.some(
|
||||||
|
(plugin) => plugin.isPlanable && !plugin.isPlanable(node, planable),
|
||||||
|
);
|
||||||
|
return !hasNonPlanable;
|
||||||
|
});
|
||||||
|
|
||||||
|
const planableNodes = planables.map<GraphNode<TAttributes>>((planable) => {
|
||||||
|
const decreased = node.remaining.map((remainingPlanable) => {
|
||||||
|
if (remainingPlanable === planable) {
|
||||||
|
return {
|
||||||
|
...remainingPlanable,
|
||||||
|
count: (remainingPlanable.count || 1) - 1,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return remainingPlanable;
|
||||||
|
});
|
||||||
|
const remaining = decreased.filter(
|
||||||
|
(remainingPlanable) => remainingPlanable.count !== 0,
|
||||||
|
);
|
||||||
|
const startTime = Math.max(
|
||||||
|
node.time + node.duration,
|
||||||
|
planable.start?.min || 0,
|
||||||
|
);
|
||||||
|
return {
|
||||||
|
...node,
|
||||||
|
type: 'planable',
|
||||||
|
exploreId: 0,
|
||||||
|
id: generateId(),
|
||||||
|
score: node.score + planable.score,
|
||||||
|
planable: planable.id,
|
||||||
|
time: startTime,
|
||||||
|
duration: planable.duration,
|
||||||
|
remaining,
|
||||||
|
completed: remaining.length === 0,
|
||||||
|
parent: node.id,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
return [...planableNodes, ...metaNodes.flat()];
|
||||||
|
};
|
||||||
|
|
||||||
|
export { expandNode };
|
||||||
21
packages/algorithm/src/algorithm/is-impossible.ts
Normal file
21
packages/algorithm/src/algorithm/is-impossible.ts
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
import { Attributes, GraphNode } from '../types/node';
|
||||||
|
import { Planable } from '../types/planable';
|
||||||
|
|
||||||
|
type IsPlanableOptions<TAttributes extends Attributes> = {
|
||||||
|
node: GraphNode<TAttributes>;
|
||||||
|
};
|
||||||
|
|
||||||
|
const hasImpossible = <TAttributes extends Attributes>({
|
||||||
|
node,
|
||||||
|
}: IsPlanableOptions<TAttributes>): boolean => {
|
||||||
|
const impossible = node.remaining.find((planable: Planable) => {
|
||||||
|
if (planable.start) {
|
||||||
|
return planable.start.max < node.time + node.duration;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
});
|
||||||
|
|
||||||
|
return !!impossible;
|
||||||
|
};
|
||||||
|
|
||||||
|
export { hasImpossible };
|
||||||
5
packages/algorithm/src/index.ts
Normal file
5
packages/algorithm/src/index.ts
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
export type { GraphNode } from './types/node';
|
||||||
|
export type { Planable } from './types/planable';
|
||||||
|
export { expandNode } from './algorithm/expand-node';
|
||||||
|
export { calulation, type CalulationResult } from './algorithm/calulation';
|
||||||
|
export { plugins } from './plugins/index';
|
||||||
9
packages/algorithm/src/plugins/index.ts
Normal file
9
packages/algorithm/src/plugins/index.ts
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
import { Attributes } from '../types/node';
|
||||||
|
import { Plugin } from '../types/plugin';
|
||||||
|
import { transport } from './transport';
|
||||||
|
|
||||||
|
const plugins = {
|
||||||
|
transport,
|
||||||
|
} satisfies Record<string, (...args: any[]) => Plugin<Attributes>>;
|
||||||
|
|
||||||
|
export { plugins };
|
||||||
64
packages/algorithm/src/plugins/transport.ts
Normal file
64
packages/algorithm/src/plugins/transport.ts
Normal file
@@ -0,0 +1,64 @@
|
|||||||
|
import { GraphNode } from '../types/node';
|
||||||
|
import { Plugin } from '../types/plugin';
|
||||||
|
|
||||||
|
type GetTravelTime = (from: string, to: string) => Promise<number>;
|
||||||
|
|
||||||
|
type TransportOptions = {
|
||||||
|
getTravelTime: GetTravelTime;
|
||||||
|
};
|
||||||
|
|
||||||
|
type TransportAttributes = {
|
||||||
|
locations?: string[];
|
||||||
|
};
|
||||||
|
|
||||||
|
const transport = ({
|
||||||
|
getTravelTime,
|
||||||
|
}: TransportOptions): Plugin<TransportAttributes> => ({
|
||||||
|
getMetaNodes: async (node) => {
|
||||||
|
const locations =
|
||||||
|
(node.type !== 'travel' &&
|
||||||
|
[
|
||||||
|
...new Set(
|
||||||
|
node.remaining
|
||||||
|
.map((planable) => planable.attributes?.locations)
|
||||||
|
.flat(),
|
||||||
|
),
|
||||||
|
]
|
||||||
|
.filter((location) => location !== node.location)
|
||||||
|
.filter(Boolean)
|
||||||
|
.map((l) => l!)) ||
|
||||||
|
[];
|
||||||
|
|
||||||
|
const travelNodes = await Promise.all(
|
||||||
|
locations.map<Promise<GraphNode<TransportAttributes>>>(
|
||||||
|
async (location) => {
|
||||||
|
const travelTime = await getTravelTime(node.location, location);
|
||||||
|
return {
|
||||||
|
...node,
|
||||||
|
type: 'travel',
|
||||||
|
planable: undefined,
|
||||||
|
location,
|
||||||
|
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 };
|
||||||
21
packages/algorithm/src/types/node.ts
Normal file
21
packages/algorithm/src/types/node.ts
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
import type { Planable } from './planable';
|
||||||
|
|
||||||
|
type Attributes = any;
|
||||||
|
|
||||||
|
type GraphNode<TAttributes extends Attributes = Attributes> = {
|
||||||
|
id: string;
|
||||||
|
type: 'root' | 'planable' | 'travel';
|
||||||
|
score: number;
|
||||||
|
location: string;
|
||||||
|
parent: string | null;
|
||||||
|
time: number;
|
||||||
|
exploreId: number;
|
||||||
|
duration: number;
|
||||||
|
planable?: string;
|
||||||
|
remaining: Planable<TAttributes>[];
|
||||||
|
deadEnd?: boolean;
|
||||||
|
completed?: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type { Attributes };
|
||||||
|
export { GraphNode };
|
||||||
17
packages/algorithm/src/types/planable.ts
Normal file
17
packages/algorithm/src/types/planable.ts
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
import { Attributes } from './node';
|
||||||
|
|
||||||
|
type Planable<TAttributes extends Attributes = Attributes> = {
|
||||||
|
id: string;
|
||||||
|
duration: number;
|
||||||
|
score: number;
|
||||||
|
count?: number;
|
||||||
|
start?: {
|
||||||
|
min: number;
|
||||||
|
max: number;
|
||||||
|
};
|
||||||
|
attributes: TAttributes;
|
||||||
|
required?: boolean;
|
||||||
|
locations?: string[];
|
||||||
|
};
|
||||||
|
|
||||||
|
export type { Planable };
|
||||||
25
packages/algorithm/src/types/plugin.ts
Normal file
25
packages/algorithm/src/types/plugin.ts
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
import { Attributes, GraphNode } from './node';
|
||||||
|
import { Planable } from './planable';
|
||||||
|
|
||||||
|
type Plugin<TAttributes extends Attributes = Attributes> = {
|
||||||
|
getMetaNodes?: (
|
||||||
|
node: GraphNode<TAttributes>,
|
||||||
|
) => Promise<GraphNode<TAttributes>[]>;
|
||||||
|
isImpossible?: (node: GraphNode<TAttributes>) => Promise<boolean>;
|
||||||
|
isPlanable?: (
|
||||||
|
node: GraphNode<TAttributes>,
|
||||||
|
planable: Planable<TAttributes>,
|
||||||
|
) => boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
type Plugins = Plugin[];
|
||||||
|
|
||||||
|
type PluginAttributes<TPlugins extends Plugins> = {
|
||||||
|
[K in keyof TPlugins]: TPlugins[K] extends Plugin<infer TAttributes>
|
||||||
|
? TAttributes extends Attributes
|
||||||
|
? TAttributes
|
||||||
|
: never
|
||||||
|
: never;
|
||||||
|
}[number];
|
||||||
|
|
||||||
|
export type { Plugin, Plugins, PluginAttributes };
|
||||||
3
packages/algorithm/tsconfig.json
Normal file
3
packages/algorithm/tsconfig.json
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
{
|
||||||
|
"extends": "./configs/tsconfig.cjs.json"
|
||||||
|
}
|
||||||
5
packages/configs/package.json
Normal file
5
packages/configs/package.json
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
{
|
||||||
|
"name": "@bob-the-algorithm/config",
|
||||||
|
"private": true,
|
||||||
|
"version": "0.0.1"
|
||||||
|
}
|
||||||
19
packages/configs/tsconfig.base.json
Normal file
19
packages/configs/tsconfig.base.json
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"allowJs": true,
|
||||||
|
"allowSyntheticDefaultImports": true,
|
||||||
|
"checkJs": true,
|
||||||
|
"declaration": true,
|
||||||
|
"declarationMap": true,
|
||||||
|
"esModuleInterop": true,
|
||||||
|
"forceConsistentCasingInFileNames": true,
|
||||||
|
"jsx": "react-jsx",
|
||||||
|
"skipLibCheck": true,
|
||||||
|
"sourceMap": true,
|
||||||
|
"strict": true
|
||||||
|
},
|
||||||
|
"include": [],
|
||||||
|
"ts-node": {
|
||||||
|
"files": true
|
||||||
|
}
|
||||||
|
}
|
||||||
12
packages/configs/tsconfig.cjs.json
Normal file
12
packages/configs/tsconfig.cjs.json
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
{
|
||||||
|
"extends": "./tsconfig.base.json",
|
||||||
|
"compilerOptions": {
|
||||||
|
"lib": [
|
||||||
|
"ES2019",
|
||||||
|
"DOM"
|
||||||
|
],
|
||||||
|
"target": "ES2019",
|
||||||
|
"module": "CommonJS",
|
||||||
|
"moduleResolution": "Node"
|
||||||
|
}
|
||||||
|
}
|
||||||
8
packages/configs/tsconfig.esm.json
Normal file
8
packages/configs/tsconfig.esm.json
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
{
|
||||||
|
"extends": "./tsconfig.base.json",
|
||||||
|
"compilerOptions": {
|
||||||
|
"lib": ["ES2022", "DOM"],
|
||||||
|
"target": "ES2022",
|
||||||
|
"module": "ESNext"
|
||||||
|
}
|
||||||
|
}
|
||||||
18
packages/playground/.eslintrc.cjs
Normal file
18
packages/playground/.eslintrc.cjs
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
module.exports = {
|
||||||
|
root: true,
|
||||||
|
env: { browser: true, es2020: true },
|
||||||
|
extends: [
|
||||||
|
'eslint:recommended',
|
||||||
|
'plugin:@typescript-eslint/recommended',
|
||||||
|
'plugin:react-hooks/recommended',
|
||||||
|
],
|
||||||
|
ignorePatterns: ['dist', '.eslintrc.cjs'],
|
||||||
|
parser: '@typescript-eslint/parser',
|
||||||
|
plugins: ['react-refresh'],
|
||||||
|
rules: {
|
||||||
|
'react-refresh/only-export-components': [
|
||||||
|
'warn',
|
||||||
|
{ allowConstantExport: true },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
}
|
||||||
24
packages/playground/.gitignore
vendored
Normal file
24
packages/playground/.gitignore
vendored
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
# Logs
|
||||||
|
logs
|
||||||
|
*.log
|
||||||
|
npm-debug.log*
|
||||||
|
yarn-debug.log*
|
||||||
|
yarn-error.log*
|
||||||
|
pnpm-debug.log*
|
||||||
|
lerna-debug.log*
|
||||||
|
|
||||||
|
node_modules
|
||||||
|
dist
|
||||||
|
dist-ssr
|
||||||
|
*.local
|
||||||
|
|
||||||
|
# Editor directories and files
|
||||||
|
.vscode/*
|
||||||
|
!.vscode/extensions.json
|
||||||
|
.idea
|
||||||
|
.DS_Store
|
||||||
|
*.suo
|
||||||
|
*.ntvs*
|
||||||
|
*.njsproj
|
||||||
|
*.sln
|
||||||
|
*.sw?
|
||||||
13
packages/playground/index.html
Normal file
13
packages/playground/index.html
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<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" />
|
||||||
|
<title>Vite + React + TS</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="root"></div>
|
||||||
|
<script type="module" src="/src/main.tsx"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
32
packages/playground/package.json
Normal file
32
packages/playground/package.json
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
{
|
||||||
|
"name": "@bob-the-algorithm/playground",
|
||||||
|
"private": true,
|
||||||
|
"version": "0.0.0",
|
||||||
|
"type": "module",
|
||||||
|
"scripts": {
|
||||||
|
"dev": "vite",
|
||||||
|
"build": "tsc && vite build",
|
||||||
|
"lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0",
|
||||||
|
"preview": "vite preview"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"bob-the-algorithm": "workspace:^",
|
||||||
|
"date-fns": "^2.30.0",
|
||||||
|
"react": "^18.2.0",
|
||||||
|
"react-dom": "^18.2.0"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@mdx-js/rollup": "^2.3.0",
|
||||||
|
"@types/react": "^18.2.15",
|
||||||
|
"@types/react-dom": "^18.2.7",
|
||||||
|
"@typescript-eslint/eslint-plugin": "^6.0.0",
|
||||||
|
"@typescript-eslint/parser": "^6.0.0",
|
||||||
|
"@vitejs/plugin-react-swc": "^3.3.2",
|
||||||
|
"eslint": "^8.45.0",
|
||||||
|
"eslint-plugin-react-hooks": "^4.6.0",
|
||||||
|
"eslint-plugin-react-refresh": "^0.4.3",
|
||||||
|
"reagraph": "^4.13.0",
|
||||||
|
"typescript": "^5.0.2",
|
||||||
|
"vite": "^4.4.5"
|
||||||
|
}
|
||||||
|
}
|
||||||
7
packages/playground/src/app.tsx
Normal file
7
packages/playground/src/app.tsx
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
import { Page } from './containers/page';
|
||||||
|
|
||||||
|
const App: React.FC = () => {
|
||||||
|
return <Page slug=".hello" />;
|
||||||
|
};
|
||||||
|
|
||||||
|
export { App };
|
||||||
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 };
|
||||||
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 };
|
||||||
9
packages/playground/src/main.tsx
Normal file
9
packages/playground/src/main.tsx
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import ReactDOM from 'react-dom/client';
|
||||||
|
import { App } from './app.tsx';
|
||||||
|
|
||||||
|
ReactDOM.createRoot(document.getElementById('root')!).render(
|
||||||
|
<React.StrictMode>
|
||||||
|
<App />
|
||||||
|
</React.StrictMode>,
|
||||||
|
);
|
||||||
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 '@morten-olsen/bob';
|
||||||
|
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 };
|
||||||
139
packages/playground/src/presenters/graph/index.tsx
Normal file
139
packages/playground/src/presenters/graph/index.tsx
Normal file
@@ -0,0 +1,139 @@
|
|||||||
|
import { useMemo, useState } from 'react';
|
||||||
|
import { GraphCanvas } from 'reagraph';
|
||||||
|
import { ConvertedResult } from '../../utils/graph';
|
||||||
|
import { Plan } from './plan';
|
||||||
|
|
||||||
|
type PresenterProps = {
|
||||||
|
output: ConvertedResult;
|
||||||
|
};
|
||||||
|
|
||||||
|
const Presenter: React.FC<PresenterProps> = ({ output }) => {
|
||||||
|
const [currentStep, setCurrentStep] = useState(0);
|
||||||
|
const [visualize, setVisualize] = useState(false);
|
||||||
|
const [selectedNode, setSelectedNode] = useState<string | undefined>(
|
||||||
|
undefined,
|
||||||
|
);
|
||||||
|
const selectedPath = useMemo(() => {
|
||||||
|
if (!selectedNode) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
const result: string[] = [];
|
||||||
|
let current = output.result.nodes.find((n) => n.id === selectedNode);
|
||||||
|
|
||||||
|
while (current) {
|
||||||
|
result.push(current.id);
|
||||||
|
if (!current.parent) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
current = output.result.nodes.find((n) => n.id === current?.parent);
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}, [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) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
Nodes count: {output.nodes.length}
|
||||||
|
<button onClick={() => setVisualize(!visualize)}>
|
||||||
|
{visualize ? 'Hide' : 'Show'} Visualize
|
||||||
|
</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 && (
|
||||||
|
<div style={{ position: 'relative', height: '70vh' }}>
|
||||||
|
<GraphCanvas
|
||||||
|
{...output}
|
||||||
|
collapsedNodeIds={collapsedNodeIds}
|
||||||
|
labelType="all"
|
||||||
|
onNodeClick={(node) => {
|
||||||
|
if (node.id === selectedNode) {
|
||||||
|
setSelectedNode(undefined);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setSelectedNode(node.id);
|
||||||
|
}}
|
||||||
|
selections={selectedPath}
|
||||||
|
renderNode={({ size, opacity, node }) => {
|
||||||
|
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) {
|
||||||
|
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 { 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';
|
||||||
|
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 };
|
||||||
49
packages/playground/src/utils/graph.ts
Normal file
49
packages/playground/src/utils/graph.ts
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
import { CalulationResult } from 'bob-the-algorithm';
|
||||||
|
|
||||||
|
function msToHMS(ms: number) {
|
||||||
|
// 1- Convert to seconds:
|
||||||
|
let seconds = ms / 1000;
|
||||||
|
// 2- Extract hours:
|
||||||
|
const hours = seconds / 3600; // 3,600 seconds in 1 hour
|
||||||
|
seconds = seconds % 3600; // seconds remaining after extracting hours
|
||||||
|
// 3- Extract minutes:
|
||||||
|
const minutes = seconds / 60; // 60 seconds in 1 minute
|
||||||
|
// 4- Keep only seconds not extracted to minutes:
|
||||||
|
seconds = seconds % 60;
|
||||||
|
return hours + ':' + minutes + ':' + seconds;
|
||||||
|
}
|
||||||
|
const convertResult = (result: CalulationResult<any>) => {
|
||||||
|
const nodes = result.nodes.map((node) => {
|
||||||
|
let label = `root (${node.location})`;
|
||||||
|
if (node.type === 'planable') {
|
||||||
|
label = `task: ${node.planable!.toString()}`;
|
||||||
|
} else if (node.type === 'travel') {
|
||||||
|
label = `travel->${node.location}`;
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
id: node.id,
|
||||||
|
label: `${msToHMS(node.time)}: ${label}`,
|
||||||
|
data: {
|
||||||
|
type: node.type,
|
||||||
|
exploreId: node.exploreId,
|
||||||
|
completed: node.completed,
|
||||||
|
deadEnd: node.deadEnd,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
});
|
||||||
|
const edges = result.nodes
|
||||||
|
.filter((n) => n.parent)
|
||||||
|
.map((node) => ({
|
||||||
|
id: `${node.id}->${node.parent}`,
|
||||||
|
source: node.parent!,
|
||||||
|
target: node.id,
|
||||||
|
label: node.score.toFixed(2),
|
||||||
|
}));
|
||||||
|
|
||||||
|
return { nodes, edges, result };
|
||||||
|
};
|
||||||
|
|
||||||
|
type ConvertedResult = ReturnType<typeof convertResult>;
|
||||||
|
|
||||||
|
export type { ConvertedResult };
|
||||||
|
export { convertResult };
|
||||||
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
packages/playground/src/vite-env.d.ts
vendored
Normal file
1
packages/playground/src/vite-env.d.ts
vendored
Normal file
@@ -0,0 +1 @@
|
|||||||
|
/// <reference types="vite/client" />
|
||||||
25
packages/playground/tsconfig.json
Normal file
25
packages/playground/tsconfig.json
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"target": "ES2020",
|
||||||
|
"useDefineForClassFields": true,
|
||||||
|
"lib": ["ES2020", "DOM", "DOM.Iterable"],
|
||||||
|
"module": "ESNext",
|
||||||
|
"skipLibCheck": true,
|
||||||
|
|
||||||
|
/* Bundler mode */
|
||||||
|
"moduleResolution": "bundler",
|
||||||
|
"allowImportingTsExtensions": true,
|
||||||
|
"resolveJsonModule": true,
|
||||||
|
"isolatedModules": true,
|
||||||
|
"noEmit": true,
|
||||||
|
"jsx": "react-jsx",
|
||||||
|
|
||||||
|
/* Linting */
|
||||||
|
"strict": true,
|
||||||
|
"noUnusedLocals": true,
|
||||||
|
"noUnusedParameters": true,
|
||||||
|
"noFallthroughCasesInSwitch": true
|
||||||
|
},
|
||||||
|
"include": ["src"],
|
||||||
|
"references": [{ "path": "./tsconfig.node.json" }]
|
||||||
|
}
|
||||||
10
packages/playground/tsconfig.node.json
Normal file
10
packages/playground/tsconfig.node.json
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"composite": true,
|
||||||
|
"skipLibCheck": true,
|
||||||
|
"module": "ESNext",
|
||||||
|
"moduleResolution": "bundler",
|
||||||
|
"allowSyntheticDefaultImports": true
|
||||||
|
},
|
||||||
|
"include": ["vite.config.ts"]
|
||||||
|
}
|
||||||
8
packages/playground/vite.config.ts
Normal file
8
packages/playground/vite.config.ts
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
import { defineConfig } from 'vite';
|
||||||
|
import react from '@vitejs/plugin-react-swc';
|
||||||
|
import mdx from '@mdx-js/rollup';
|
||||||
|
|
||||||
|
// https://vitejs.dev/config/
|
||||||
|
export default defineConfig({
|
||||||
|
plugins: [mdx(), react()],
|
||||||
|
});
|
||||||
4571
pnpm-lock.yaml
generated
Normal file
4571
pnpm-lock.yaml
generated
Normal file
File diff suppressed because it is too large
Load Diff
2
pnpm-workspace.yaml
Normal file
2
pnpm-workspace.yaml
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
packages:
|
||||||
|
- packages/*
|
||||||
8
tsconfig.json
Normal file
8
tsconfig.json
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
{
|
||||||
|
"include": [],
|
||||||
|
"references": [
|
||||||
|
{
|
||||||
|
"path": "./packages/algorithm/configs/tsconfig.libs.json"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
42
turbo.json
Normal file
42
turbo.json
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
{
|
||||||
|
"$schema": "https://turbo.build/schema.json",
|
||||||
|
"pipeline": {
|
||||||
|
"build": {
|
||||||
|
"dependsOn": [
|
||||||
|
"^build"
|
||||||
|
],
|
||||||
|
"outputs": [
|
||||||
|
"dist/**"
|
||||||
|
],
|
||||||
|
"inputs": [
|
||||||
|
"src/**/*.tsx",
|
||||||
|
"src/**/*.ts",
|
||||||
|
"./tsconfig.*"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"test": {
|
||||||
|
"cache": false
|
||||||
|
},
|
||||||
|
"clean": {},
|
||||||
|
"dev": {
|
||||||
|
"dependsOn": [
|
||||||
|
"^build"
|
||||||
|
],
|
||||||
|
"cache": false
|
||||||
|
},
|
||||||
|
"start": {
|
||||||
|
"dependsOn": [
|
||||||
|
"^build",
|
||||||
|
"build"
|
||||||
|
],
|
||||||
|
"outputs": [
|
||||||
|
"dist/**"
|
||||||
|
],
|
||||||
|
"inputs": [
|
||||||
|
"src/**/*.tsx",
|
||||||
|
"src/**/*.ts",
|
||||||
|
"./tsconfig.*"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user