feat: init

This commit is contained in:
Morten Olsen
2023-09-06 10:56:36 +02:00
commit 0a418b076a
55 changed files with 6076 additions and 0 deletions

12
.eslintrc Normal file
View 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
View 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
View 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 }}

88
.github/workflows/release-package.yml vendored Normal file
View File

@@ -0,0 +1,88 @@
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:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- run: corepack enable
- 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: |
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'
needs: build
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
- run: corepack enable
- 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 $(git describe --tag --abbrev=0) --no-git-tag-version
pnpm publish --no-git-checks
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}

3
.gitignore vendored Normal file
View File

@@ -0,0 +1,3 @@
/node_modules/
/.pnpm-store/
/.turbo/

2
.npmrc Normal file
View File

@@ -0,0 +1,2 @@
node-linker=hoisted
store-dir=.pnpm-store

13
.prettierrc.json Normal file
View 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
View File

@@ -0,0 +1,6 @@
{
"generator-x-repo": {
"root": "@morten-olsen/bob",
"config": "@morten-olsen/bob-config"
}
}

18
package.json Normal file
View 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
View File

@@ -0,0 +1,2 @@
/node_modules/
/dist/

View File

@@ -0,0 +1,4 @@
> bob-the-algorithm@ build /Users/alice/work/private/bob/packages/algorithm
> tsc --build configs/tsconfig.libs.json

View File

@@ -0,0 +1,10 @@
{
"compilerOptions": {
"declarationDir": "../dist/cjs/types",
"outDir": "../dist/cjs"
},
"extends": "@bob-the-algorithm/config/tsconfig.cjs.json",
"include": [
"../src/**/*"
]
}

View File

@@ -0,0 +1,10 @@
{
"compilerOptions": {
"declarationDir": "../dist/esm/types",
"outDir": "../dist/esm"
},
"extends": "@bob-the-algorithm/config/tsconfig.esm.json",
"include": [
"../src/**/*"
]
}

View File

@@ -0,0 +1,11 @@
{
"include": [],
"references": [
{
"path": "./tsconfig.cjs.json"
},
{
"path": "./tsconfig.esm.json"
}
]
}

View File

@@ -0,0 +1,27 @@
{
"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 --build configs/tsconfig.libs.json"
},
"types": "./dist/cjs/types/index.d.ts"
}

View 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 };

View 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 };

View 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 };

View 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';

View 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 };

View 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 };

View 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 };

View 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 };

View 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 };

View File

@@ -0,0 +1,3 @@
{
"extends": "./configs/tsconfig.cjs.json"
}

View File

@@ -0,0 +1,5 @@
{
"name": "@bob-the-algorithm/config",
"private": true,
"version": "0.0.1"
}

View 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
}
}

View File

@@ -0,0 +1,12 @@
{
"extends": "./tsconfig.base.json",
"compilerOptions": {
"lib": [
"ES2019",
"DOM"
],
"target": "ES2019",
"module": "CommonJS",
"moduleResolution": "Node"
}
}

View File

@@ -0,0 +1,8 @@
{
"extends": "./tsconfig.base.json",
"compilerOptions": {
"lib": ["ES2022", "DOM"],
"target": "ES2022",
"module": "ESNext"
}
}

View 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
View 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?

View 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>

View 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"
}
}

View File

@@ -0,0 +1,7 @@
import { Page } from './containers/page';
const App: React.FC = () => {
return <Page slug=".hello" />;
};
export { App };

View 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 };

View 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 };

View 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 };

View File

@@ -0,0 +1,3 @@
export { RunnerProvider } from './context';
export { Block } from './block';
export { createWorker } from './worker';

View 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 };

View 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>,
);

View 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} />

View File

@@ -0,0 +1,148 @@
import { calulation, plugins } from 'bob-the-algorithm';
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,
});

View File

@@ -0,0 +1,5 @@
const worker = new Worker(new URL('./script.ts', import.meta.url), {
type: 'module',
});
export { worker };

View 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 };

View 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 };

View 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 };

View 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
View File

@@ -0,0 +1 @@
/// <reference types="vite/client" />

View 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" }]
}

View File

@@ -0,0 +1,10 @@
{
"compilerOptions": {
"composite": true,
"skipLibCheck": true,
"module": "ESNext",
"moduleResolution": "bundler",
"allowSyntheticDefaultImports": true
},
"include": ["vite.config.ts"]
}

View 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

File diff suppressed because it is too large Load Diff

2
pnpm-workspace.yaml Normal file
View File

@@ -0,0 +1,2 @@
packages:
- packages/*

8
tsconfig.json Normal file
View File

@@ -0,0 +1,8 @@
{
"include": [],
"references": [
{
"path": "./packages/algorithm/configs/tsconfig.libs.json"
}
]
}

42
turbo.json Normal file
View 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.*"
]
}
}
}