feat(drainage): execute frontend actions
This commit is contained in:
@@ -11,6 +11,7 @@ import {
|
||||
type AgentUiResult
|
||||
} from "@/features/agent";
|
||||
import { toTrustedMapAction, type UIEnvelopePayload } from "@/features/agent/ui-envelope";
|
||||
import type { FrontendActionRequest } from "@/features/agent/frontend-action";
|
||||
import {
|
||||
MapErrorNotice,
|
||||
MapLoadingNotice,
|
||||
@@ -39,6 +40,7 @@ import { WORKBENCH_SCENARIOS, WORKBENCH_USER } from "./data/workbench-session";
|
||||
import { useWorkbenchAgent } from "./hooks/use-workbench-agent";
|
||||
import { useWorkbenchDrawing, type WorkbenchDrawMode } from "./hooks/use-workbench-drawing";
|
||||
import { useWorkbenchMap } from "./hooks/use-workbench-map";
|
||||
import { toMapFeatureReference } from "./hooks/use-map-interactions";
|
||||
import { useWorkbenchMeasurement, type WorkbenchMeasureMode, type WorkbenchMeasureUnit } from "./hooks/use-workbench-measurement";
|
||||
import { exportMapViewImage } from "./map/export-view";
|
||||
import {
|
||||
@@ -184,7 +186,8 @@ export function MapWorkbenchPage() {
|
||||
});
|
||||
|
||||
const agent = useWorkbenchAgent({
|
||||
onUiEnvelope: handleAgentUiEnvelope
|
||||
onUiEnvelope: handleAgentUiEnvelope,
|
||||
onFrontendAction: handleFrontendAction
|
||||
});
|
||||
const activeAgentUiResults = useMemo(
|
||||
() => agentUiResults.filter((result) => result.sessionId === agent.sessionId),
|
||||
@@ -328,7 +331,8 @@ export function MapWorkbenchPage() {
|
||||
try {
|
||||
const result = await exportMapViewImage(map, {
|
||||
scale: preset === "current" ? 1 : undefined,
|
||||
targetLongEdge
|
||||
targetLongEdge,
|
||||
selectedFeature: toMapFeatureReference(detailFeature)
|
||||
});
|
||||
showMapNotice({
|
||||
id: "map-view-export",
|
||||
@@ -519,6 +523,22 @@ export function MapWorkbenchPage() {
|
||||
});
|
||||
}
|
||||
|
||||
async function handleFrontendAction(request: FrontendActionRequest, signal: AbortSignal) {
|
||||
const map = mapRef.current;
|
||||
if (request.name === "zoom_to_map") {
|
||||
if (!map || !mapReady) throw new Error("MAP_NOT_READY"); const x = Number(request.params.x); const y = Number(request.params.y);
|
||||
const center: [number, number] = request.params.source_crs === "EPSG:4326" ? [x, y] : [x * 180 / 20037508.34, Math.atan(Math.exp(y * Math.PI / 20037508.34)) * 360 / Math.PI - 90]; const zoom = Number(request.params.zoom ?? 18); map.easeTo({ center, zoom, duration: Number(request.params.duration_ms ?? 500) }); return { center, zoom };
|
||||
}
|
||||
if (request.name === "locate_features") {
|
||||
if (!map || !mapReady) throw new Error("MAP_NOT_READY"); const ids = request.params.ids as string[]; const featureType = String(request.params.feature_type);
|
||||
const response = await fetch("/api/agent-locate", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ ids, featureType }), signal }); const geojson = await response.json(); if (!response.ok) throw new Error(geojson.code ?? "WFS_FAILED");
|
||||
const features = geojson.features as Array<{ id?: string | number; properties?: Record<string, unknown>; geometry: { coordinates: unknown } }>; if (!features.length) throw new Error("FEATURE_NOT_FOUND");
|
||||
if (map.getSource("agent-locate")) (map.getSource("agent-locate") as unknown as { setData: (data: unknown) => void }).setData(geojson); else { map.addSource("agent-locate", { type: "geojson", data: geojson }); map.addLayer({ id: "agent-locate-lines", type: "line", source: "agent-locate", filter: ["==", ["geometry-type"], "LineString"], paint: { "line-color": "#f97316", "line-width": 6 } }); map.addLayer({ id: "agent-locate-points", type: "circle", source: "agent-locate", filter: ["==", ["geometry-type"], "Point"], paint: { "circle-color": "#f97316", "circle-radius": 8, "circle-stroke-color": "#fff", "circle-stroke-width": 2 } }); }
|
||||
const coordinates: number[][] = []; const collect = (value: unknown) => { if (Array.isArray(value) && value.length >= 2 && typeof value[0] === "number" && typeof value[1] === "number") coordinates.push(value as number[]); else if (Array.isArray(value)) value.forEach(collect); }; features.forEach((feature) => collect(feature.geometry.coordinates)); const bounds: [number, number, number, number] = [Math.min(...coordinates.map((c) => c[0])), Math.min(...coordinates.map((c) => c[1])), Math.max(...coordinates.map((c) => c[0])), Math.max(...coordinates.map((c) => c[1]))]; map.fitBounds([[bounds[0], bounds[1]], [bounds[2], bounds[3]]], { padding: 80, maxZoom: 18 }); const locatedIds = features.map((feature) => String(feature.id ?? feature.properties?.id ?? "")).filter(Boolean); return { locatedIds, missingIds: ids.filter((id) => !locatedIds.includes(id)), featureType, bounds };
|
||||
}
|
||||
const history = request.name === "view_history"; const itemCount = history ? (request.params.feature_infos as unknown[]).length : Array.isArray(request.params.device_ids) ? request.params.device_ids.length : 1; setAgentUiResults((current) => [...current.slice(-5), { id: request.actionId, sessionId: request.sessionId, createdAt: Date.now(), envelope: { kind: "registered_component", schemaVersion: "agent-ui@1", component: history ? "HistoryPanel" : "ScadaPanel", surface: "side_panel", props: request.params } }]); return { component: history ? "HistoryPanel" : "ScadaPanel", rendered: true, itemCount };
|
||||
}
|
||||
|
||||
async function handleAgentMapAction(action: string, params: unknown, sessionId: string, fallbackText?: string) {
|
||||
const trustedAction = toTrustedMapAction(action, params, fallbackText);
|
||||
if (!trustedAction) {
|
||||
|
||||
Reference in New Issue
Block a user