Skip to content

Commit b87801d

Browse files
author
Kevin
committed
feat: add HistorySparkline component and expected progress calculation; enhance Status component with completion metrics
1 parent e5767ff commit b87801d

File tree

10 files changed

+176
-36
lines changed

10 files changed

+176
-36
lines changed

package.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
"@react-three/postprocessing": "^3.0.4",
1919
"@tanstack/react-query": "^5.75.0",
2020
"@trpc/client": "^11.1.2",
21+
"@types/react-sparklines": "^1.7.5",
2122
"@uidotdev/usehooks": "^2.4.1",
2223
"ansi-to-react": "^6.1.6",
2324
"async-mutex": "^0.5.0",
@@ -31,6 +32,7 @@
3132
"react": "^19.1.0",
3233
"react-dom": "^19.1.0",
3334
"react-dropzone": "^14.3.8",
35+
"react-sparklines": "^1.7.0",
3436
"react-spring": "^10.0.0",
3537
"react-use": "^17.6.0",
3638
"smart-service": "workspace:*",

src/Status.tsx

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
11
import { useAgentInfo } from "client/state";
22
import { colors } from "colors";
3+
import { HistorySparkline } from "components/HistorySparkline";
34
import { Components } from "leva/plugin";
4-
import { chain, isUndefined, noop } from "lodash";
5+
import { chain, isUndefined, noop, round } from "lodash";
56
import { useCss } from "react-use";
7+
import { useLerp } from "hooks/useLerp";
68

79
const vectorProps = {
810
onUpdate: noop,
@@ -57,6 +59,9 @@ export function Status({ id }: { id: number }) {
5759
},
5860
},
5961
});
62+
63+
const a1 = useLerp(a?.progress?.finished ?? 0);
64+
6065
if (!a) return null;
6166
const status =
6267
a.constraints?.length ||
@@ -76,7 +81,7 @@ export function Status({ id }: { id: number }) {
7681
label: isUndefined(a.state)
7782
? "Initialising"
7883
: a.state === "unknown"
79-
? "Not simulated"
84+
? "--"
8085
: a.state === "finished"
8186
? "Finished"
8287
: `${a.state === "idle" ? "Waiting for" : "Constrained by"} ${chain(
@@ -88,6 +93,7 @@ export function Status({ id }: { id: number }) {
8893
.value()}`,
8994
}
9095
: { color: colors.success, label: "Active" };
96+
9197
return (
9298
<div className={cls}>
9399
<h4>
@@ -118,6 +124,15 @@ export function Status({ id }: { id: number }) {
118124
}}
119125
/>
120126
</Components.Row>
127+
<Components.Row input>
128+
<Components.Label>Completion</Components.Label>
129+
<Components.Label>
130+
{round(a1)}/{a.progress?.total ?? "--"} (
131+
{a.progress?.finished ? round((a1 / a.progress.total) * 100) : "0"}
132+
%)
133+
</Components.Label>
134+
</Components.Row>
135+
<HistorySparkline id={id} />
121136
</div>
122137
);
123138
}

src/client/run.ts

Lines changed: 37 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,13 +6,15 @@ import {
66
identity,
77
isArray,
88
isNumber,
9+
omit,
10+
pick,
911
range,
1012
slice,
1113
throttle,
1214
trim,
1315
} from "lodash";
1416
import { useRef } from "react";
15-
import { AdgProgress, Output } from "smart";
17+
import { Output } from "smart";
1618
import { id } from "utils";
1719
import { appendAtom, clearAtom, logAtom, State } from "./state";
1820

@@ -83,13 +85,19 @@ export function useRun() {
8385
return;
8486
setBuffering(true);
8587
const controller = new AbortController();
88+
89+
// ─── Options ──────────────────────────
90+
8691
const options = {
8792
agents: contents.count,
8893
map: await mapFile.text(),
8994
scen: await scenarioFile.text(),
9095
paths: await solutionFile.text(),
9196
flipXY: flip,
9297
};
98+
99+
// ─── Commit ──────────────────────────
100+
93101
let actions: State[] = [];
94102
const f = throttle(
95103
() => {
@@ -100,8 +108,17 @@ export function useRun() {
100108
1500,
101109
{ trailing: true, leading: false }
102110
);
111+
112+
// ─── State ───────────────────────────
113+
103114
const agentState: State["agentState"] = {};
104-
let adg: AdgProgress | undefined = undefined;
115+
const progress: State["progress"] = {};
116+
let adg: State["adg"] = undefined;
117+
let prev: State["step"] | undefined = undefined;
118+
let stats: State["stats"] = undefined;
119+
120+
// ─────────────────────────────────────
121+
105122
return await new Promise<void>((res, rej) => {
106123
abort.current = () => {
107124
controller.abort();
@@ -138,11 +155,19 @@ export function useRun() {
138155
break;
139156
case "tick":
140157
actions.push({
141-
state: d,
158+
step: d,
142159
adg,
143160
agentState: structuredClone(agentState),
161+
progress: structuredClone(progress),
144162
});
145163
f();
164+
prev = d;
165+
break;
166+
case "exec_progress":
167+
progress[d.agent] = pick(d, "finished", "total");
168+
break;
169+
case "stats":
170+
stats = omit(d, "type");
146171
break;
147172
case "error":
148173
console.error(d.error);
@@ -159,6 +184,15 @@ export function useRun() {
159184
rej(error);
160185
},
161186
onComplete: () => {
187+
if (prev) {
188+
actions.push({
189+
step: prev,
190+
adg,
191+
agentState: structuredClone(agentState),
192+
progress: structuredClone(progress),
193+
stats: structuredClone(stats),
194+
});
195+
}
162196
s.unsubscribe();
163197
res();
164198
},

src/client/state.ts

Lines changed: 24 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@ import {
99
fromPairs,
1010
isEqual,
1111
isUndefined,
12+
max,
13+
range,
1214
round,
1315
throttle,
1416
thru,
@@ -17,7 +19,7 @@ import {
1719
import { store } from "store";
1820
import { useEffect, useMemo, useState } from "react";
1921
import { useEffectOnce } from "react-use";
20-
import { AdgProgress, StateChange, Step } from "smart";
22+
import { AdgProgress, ExecProgress, StateChange, Stats, Step } from "smart";
2123
import { lerp, lerpRadians } from "utils";
2224
import { useSpeed } from "./play";
2325
import { selectionAtom } from "./selection";
@@ -26,9 +28,11 @@ import { useSolutionContents } from "./run";
2628
const CHUNK_SIZE = 256;
2729

2830
export type State = {
29-
state: Step;
31+
step: Step;
3032
adg?: AdgProgress;
3133
agentState?: Record<number, StateChange["value"]>;
34+
progress?: Record<number, Pick<ExecProgress, "finished" | "total">>;
35+
stats?: Omit<Stats, "type">;
3236
};
3337

3438
export const logAtom = atom<string[]>([]);
@@ -78,6 +82,14 @@ const setCacheAtom = atom(null, async (get, set) => {
7882
);
7983
});
8084

85+
export const historyAtom = atom((get) => {
86+
const cache = get(cacheAtom);
87+
const t = floor(get(roundedTimeSmoothAtom));
88+
return range(max([0, t - CHUNK_SIZE / 2])!, t).map(
89+
(i) => cache[floor(i / CHUNK_SIZE)]?.state?.[i % CHUNK_SIZE]
90+
);
91+
});
92+
8193
// Atom to get interpolated item
8294
export const currentItemAtom = atom<State | null>((get) => {
8395
const t = get(roundedTimeSmoothAtom);
@@ -98,6 +110,7 @@ export const useAgentInfo = (i: number) => {
98110
const { data: solution } = useSolutionContents();
99111
const initial = useMemo(
100112
() => ({
113+
progress: undefined,
101114
state: "unknown",
102115
position: thru(
103116
solution?.paths?.[i]?.[0],
@@ -114,9 +127,10 @@ export const useAgentInfo = (i: number) => {
114127
() =>
115128
atom((get) => {
116129
const a = get(currentItemAtom);
117-
const agent = a?.state?.agents?.[i];
130+
const agent = a?.step?.agents?.[i];
118131
if (!agent) return;
119132
return {
133+
progress: a.progress?.[i],
120134
id: i,
121135
state: a.agentState?.[i],
122136
constraints: a.adg?.constraints?.[i]?.constraining_agent,
@@ -140,7 +154,10 @@ export const useAgentInfo = (i: number) => {
140154
: previous;
141155
};
142156

143-
function dist(a: [number, number, number], b: [number, number, number]) {
157+
function dist(
158+
a: readonly [number, number, number],
159+
b: readonly [number, number, number]
160+
) {
144161
return Math.sqrt(
145162
(a[0] - b[0]) ** 2 + (a[1] - b[1]) ** 2 + (a[2] - b[2]) ** 2
146163
);
@@ -166,7 +183,7 @@ export const useAgentPosition = (i: number) => {
166183
rotation: zip(p.rotation, v0.rotation).map(([a, b]) =>
167184
lerpRadians(a!, b!, alpha(speed))
168185
) as [number, number, number],
169-
};
186+
} as typeof v0;
170187
}
171188
return v0;
172189
});
@@ -196,9 +213,9 @@ export const appendAtom = atom<null, [State[]], unknown>(
196213
null,
197214
(_get, set, items) => {
198215
for (const item of items) {
199-
const i = floor(item.state.clock / CHUNK_SIZE);
216+
const i = floor(item.step.clock / CHUNK_SIZE);
200217
const chunk = cache[i] ?? { length: 0, state: [] };
201-
chunk.state[item.state.clock % CHUNK_SIZE] = item;
218+
chunk.state[item.step.clock % CHUNK_SIZE] = item;
202219
chunk.length++;
203220
cache[i] = chunk;
204221
if (chunk.length === CHUNK_SIZE) {

src/components/Agent.tsx

Lines changed: 22 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -29,17 +29,15 @@ const AnimatedAgentInstance = a(AgentInstance);
2929
function Constraint({
3030
from = 0,
3131
to = 0,
32+
color,
3233
...props
3334
}: {
3435
from?: number;
3536
to?: number;
37+
color?: string;
3638
} & Partial<ComponentProps<typeof AnimatedLine>>) {
37-
const {
38-
current: { position: a = [0, 0, 0] as [number, number, number] } = {},
39-
} = useAgentPosition(from) ?? {};
40-
const {
41-
current: { position: b = [0, 0, 0] as [number, number, number] } = {},
42-
} = useAgentPosition(to) ?? {};
39+
const { current: { position: a } = {} } = useAgentPosition(from) ?? {};
40+
const { current: { position: b } = {} } = useAgentPosition(to) ?? {};
4341

4442
const springs = useSpring({
4543
from: { offset: 0 },
@@ -51,10 +49,10 @@ function Constraint({
5149
return (
5250
<AnimatedLine
5351
{...props}
54-
points={[a, b]}
52+
points={[a, b] as unknown}
5553
dashOffset={springs.offset}
5654
dashScale={20}
57-
color="#ffc400"
55+
color={color}
5856
dashed
5957
renderOrder={9997}
6058
depthTest={false}
@@ -118,7 +116,7 @@ export function CurrentAgent({
118116
</AnimatedBillboard>
119117
<AnimatedRing
120118
scale={s.scale}
121-
args={[0.6 / 2, 0.7 / 2, 32, 32]}
119+
args={[0.45 / 2, 0.55 / 2, 32, 32]}
122120
position={[x, y - 0.06, z]}
123121
rotation={[-Math.PI / 2, 0, 0]}
124122
>
@@ -168,26 +166,27 @@ export function Path({
168166

169167
const constrained = !!agent?.constraints?.length;
170168

169+
const color = agent
170+
? agent.state === "unknown"
171+
? colors.idle
172+
: isUndefined(agent.state)
173+
? colors.idle
174+
: agent.state === "finished"
175+
? colors.idle
176+
: agent.state === "idle"
177+
? colors.error
178+
: constrained
179+
? colors.warning
180+
: colors.success
181+
: "#000";
171182
return (
172183
<>
173184
{agent && (
174185
<CurrentAgent
175186
agent={agent}
176187
visible={visible}
177188
hovered={hovered}
178-
color={
179-
agent.state === "unknown"
180-
? colors.idle
181-
: isUndefined(agent.state)
182-
? colors.idle
183-
: agent.state === "finished"
184-
? colors.idle
185-
: agent.state === "idle"
186-
? colors.error
187-
: constrained
188-
? colors.warning
189-
: colors.success
190-
}
189+
color={color}
191190
/>
192191
)}
193192
{visibleTransitions(
@@ -224,6 +223,7 @@ export function Path({
224223
<Constraint
225224
from={agent?.id}
226225
to={to}
226+
color={color}
227227
lineWidth={s.scale.to((s) => s * 2)}
228228
/>
229229
))}

0 commit comments

Comments
 (0)