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:
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 };
|
||||
Reference in New Issue
Block a user