feat: expand intelligent investigation workspace
This commit is contained in:
@@ -0,0 +1,141 @@
|
||||
import type { FeatureCollection } from "geojson";
|
||||
import type { AnswerUiBlock } from "@/features/agent";
|
||||
import type {
|
||||
InvestigationOverlay,
|
||||
InvestigationPresentationCommand
|
||||
} from "../api/investigation-presentation";
|
||||
import type { ScadaAnalysisItem } from "../map/scada-analysis";
|
||||
import type { InvestigationPresentationActivity } from "./investigation-dev-preview";
|
||||
|
||||
export type PresentationCamera = {
|
||||
center: [number, number];
|
||||
zoom: number;
|
||||
bearing: number;
|
||||
pitch: number;
|
||||
};
|
||||
export type PresentationView = {
|
||||
charts: Array<Extract<InvestigationPresentationCommand, { action: "open_chart" }>>;
|
||||
blocks: AnswerUiBlock[];
|
||||
records: Array<Record<string, unknown>>;
|
||||
evidence: Array<{ evidence_id: string; summary: string }>;
|
||||
highlights: FeatureCollection;
|
||||
highlightLayers: string[];
|
||||
overlay: InvestigationOverlay | null;
|
||||
scada: { data: FeatureCollection; items: ScadaAnalysisItem[] } | null;
|
||||
camera: PresentationCamera | null;
|
||||
};
|
||||
export type PresentationHistoryEntry = {
|
||||
id: number;
|
||||
key?: string;
|
||||
taskId?: string;
|
||||
label: string;
|
||||
source: InvestigationPresentationActivity["source"];
|
||||
taskTitle: string | null;
|
||||
createdAt: number;
|
||||
status: "running" | "completed" | "error" | "cancelled";
|
||||
message?: string;
|
||||
snapshot: PresentationView;
|
||||
};
|
||||
export type PresentationHistoryControls = {
|
||||
currentEntryId: number | null;
|
||||
entries: PresentationHistoryEntry[];
|
||||
selectedId: number | null;
|
||||
onSelect: (id: number | null) => void;
|
||||
};
|
||||
export const emptyPresentationView = (): PresentationView => ({
|
||||
charts: [],
|
||||
blocks: [],
|
||||
records: [],
|
||||
evidence: [],
|
||||
highlights: { type: "FeatureCollection", features: [] },
|
||||
highlightLayers: [],
|
||||
overlay: null,
|
||||
scada: null,
|
||||
camera: null
|
||||
});
|
||||
/** Page-local immutable snapshots. Updating current never changes a pinned historical view. */
|
||||
export class PresentationViewHistory {
|
||||
current = emptyPresentationView();
|
||||
entries: PresentationHistoryEntry[] = [];
|
||||
selectedId: number | null = null;
|
||||
private sequence = 0;
|
||||
|
||||
get currentEntryId() {
|
||||
if (this.entries.at(-1)?.status === "running") return null;
|
||||
// Results share immutable values with their snapshot. Capturing a manually moved
|
||||
// camera for navigation does not turn the latest result into an older result.
|
||||
const keys = Object.keys(this.current) as Array<keyof PresentationView>;
|
||||
return (
|
||||
this.entries
|
||||
.slice()
|
||||
.reverse()
|
||||
.find(
|
||||
(entry) =>
|
||||
entry.status === "completed" &&
|
||||
keys.every((key) => key === "camera" || entry.snapshot[key] === this.current[key])
|
||||
)?.id ?? null
|
||||
);
|
||||
}
|
||||
get selected() {
|
||||
return this.entries.find((entry) => entry.id === this.selectedId) ?? null;
|
||||
}
|
||||
get visible() {
|
||||
return this.selected?.snapshot ?? this.current;
|
||||
}
|
||||
update(next: PresentationView) {
|
||||
this.current = next;
|
||||
}
|
||||
begin(
|
||||
label: string,
|
||||
source: PresentationHistoryEntry["source"],
|
||||
taskTitle: string | null,
|
||||
metadata: { key?: string; taskId?: string; createdAt?: number } = {}
|
||||
) {
|
||||
const id = ++this.sequence;
|
||||
this.entries = [
|
||||
...this.entries,
|
||||
{
|
||||
id,
|
||||
label,
|
||||
source,
|
||||
taskTitle,
|
||||
createdAt: Date.now(),
|
||||
...metadata,
|
||||
status: "running",
|
||||
snapshot: this.current
|
||||
}
|
||||
];
|
||||
return id;
|
||||
}
|
||||
finish(
|
||||
id: number,
|
||||
status: Exclude<PresentationHistoryEntry["status"], "running">,
|
||||
message?: string,
|
||||
snapshot = this.current
|
||||
) {
|
||||
this.entries = this.entries.map((entry) =>
|
||||
entry.id === id ? { ...entry, status, message, snapshot } : entry
|
||||
);
|
||||
}
|
||||
record(
|
||||
label: string,
|
||||
source: PresentationHistoryEntry["source"],
|
||||
taskTitle: string | null,
|
||||
metadata: { key?: string; taskId?: string; createdAt?: number } = {}
|
||||
) {
|
||||
const id = this.begin(label, source, taskTitle, metadata);
|
||||
this.finish(id, "completed");
|
||||
return id;
|
||||
}
|
||||
select(id: number | null) {
|
||||
if (id !== null && !this.entries.some((entry) => entry.id === id && entry.status !== "running"))
|
||||
return false;
|
||||
this.selectedId = id;
|
||||
return true;
|
||||
}
|
||||
reset() {
|
||||
this.current = emptyPresentationView();
|
||||
this.entries = [];
|
||||
this.selectedId = null;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user