fix(workbench): stabilize investigation presentation rendering

Keep investigation graphs stable during updates, defer expensive chart rendering, and prevent stale results when switching tasks. Adopt reference annotations and consolidate frontend documentation.
This commit is contained in:
2026-11-11 19:10:41 +08:00
parent 0ae0a41e61
commit a617aa95e9
35 changed files with 1268 additions and 715 deletions
@@ -120,16 +120,25 @@ export function useInvestigationPresentation(options: Options) {
const valuesRef = useRef(options);
valuesRef.current = options;
const storeRef = useRef(new PresentationViewHistory());
const displayScope = useRef(windowScope);
if (displayScope.current !== windowScope) {
displayScope.current = windowScope;
// The first render of a new task must not re-key the previous task's charts.
// Keep saved history; effects below still own cancellation and map cleanup.
storeRef.current.select(null);
storeRef.current.update(emptyPresentationView());
}
const [, refresh] = useState(0);
const publish = useCallback(() => refresh((value) => value + 1), []);
const [localPreview, setLocalPreview] = useState(false);
const localPreviewRef = useRef(false);
const [cameraPaused, setCameraPaused] = useState(false);
const cameraPausedRef = useRef(false);
const [status, setStatus] = useState<
"connecting" | "live" | "history" | "disconnected" | "error"
>("disconnected");
const [error, setError] = useState<string | null>(null);
const [connection, setConnection] = useState<{
scope: string;
status: "connecting" | "live" | "history" | "disconnected" | "error";
error: string | null;
}>({ scope: windowScope, status: "disconnected", error: null });
const [playbackSpeed, setPlaybackSpeed] = useState(1);
const [activity, setActivity] = useState<InvestigationPresentationActivity[]>([]);
const activityIdRef = useRef(0);
@@ -409,8 +418,9 @@ export function useInvestigationPresentation(options: Options) {
clear,
stopPulse: () => scene.stopPulse(),
onStatus: (next, message) => {
setStatus(!valuesRef.current.enabled && next === "live" ? "history" : next);
setError(message ?? null);
setConnection({ scope: windowScope,
status: !valuesRef.current.enabled && next === "live" ? "history" : next,
error: message ?? null });
},
execute: async (command, signal, event) => {
await executeCommand(command, signal, "live", event);
@@ -434,7 +444,8 @@ export function useInvestigationPresentation(options: Options) {
publish,
scene,
subscribe,
taskId
taskId,
windowScope
]);
useEffect(
() => () => {
@@ -576,8 +587,8 @@ export function useInvestigationPresentation(options: Options) {
evidence: visible.evidence,
highlightLayers: visible.highlightLayers,
cameraPaused: reviewing ? false : cameraPaused,
status,
error,
status: connection.scope === windowScope ? connection.status : taskId ? "connecting" as const : "disconnected" as const,
error: connection.scope === windowScope ? connection.error : null,
activity,
localPreview,
hasHistory: taskEntries.length > 0,