feat(agent): render local interactive answers and fold completed process

This commit is contained in:
2026-10-12 18:54:18 +08:00
parent e0eb6a721e
commit 2c5bf15cce
30 changed files with 1610 additions and 75 deletions
@@ -9,6 +9,13 @@ import {
import { WorkbenchMapController } from "./workbench-map-controller";
describe("WorkbenchMapController", () => {
it("returns only resolved IDs and rejects a missing target rather than fitting the whole network", async () => {
const map = createMap();
const controller = createController(map, pointCollection);
await expect(controller.locateFeatures("junctions", ["J-1", "missing"])).resolves.toEqual({ located_ids: ["J-1"], missing_ids: ["missing"], fitted: true });
expect(map.fitBounds).toHaveBeenCalledWith(expect.anything(), expect.objectContaining({ duration: 0 }));
await expect(controller.locateFeatures("junctions", ["missing"])).rejects.toThrow("FEATURE_NOT_FOUND");
});
it("locates points with zoom 18 and keeps selected state untouched", async () => {
const map = createMap();
const controller = createController(map, pointCollection);
@@ -61,7 +68,7 @@ describe("WorkbenchMapController", () => {
it("drops cached features and presentation state when a dynamic source disappears", async () => {
const map = createMap();
map.getLayer.mockReturnValue({} as never);
let collection = pointCollection;
let collection: FeatureCollection = { ...pointCollection, features: [{ ...pointCollection.features[0], properties: { id: "V-1" } }] };
const fetchFeatures = vi.fn(async () => collection);
const controller = new WorkbenchMapController({
getMap: () => map as unknown as MapLibreMap,
@@ -93,6 +100,7 @@ describe("WorkbenchMapController", () => {
...pointCollection,
features: [{
...pointCollection.features[0],
properties: { id: "V-1" },
geometry: { type: "Point", coordinates: [121.9, 29.1] }
}]
};
@@ -146,13 +146,35 @@ export class WorkbenchMapController implements WorkbenchMapCommands {
this.setHighlight(target);
}
private moveCameraToFeature(map: MapLibreMap, feature: Feature) {
async locateFeatures(sourceId: WaterNetworkSourceId, featureIds: string[], signal?: AbortSignal) {
const requestRevision = this.beginTargetCommand();
const target = { sourceId, featureId: featureIds[0] ?? "" };
if (!this.requireTargetMap(target)) throw new Error(this.state.errorCode ?? "MAP_NOT_READY");
const found: Array<{ id: string; feature: Feature }> = [];
const missing: string[] = [];
for (const id of [...new Set(featureIds)]) {
signal?.throwIfAborted();
const feature = await this.resolveTarget({ sourceId, featureId: id }, requestRevision);
if (requestRevision !== this.targetRequestRevision) throw new Error("ACTION_SUPERSEDED");
if (this.state.errorCode === "WFS_UNAVAILABLE" || this.state.errorCode === "SCADA_API_UNAVAILABLE") throw new Error(this.state.errorCode);
if (feature) found.push({ id, feature }); else missing.push(id);
}
signal?.throwIfAborted();
const map = this.requireTargetMap(target);
if (!map || found.length === 0) throw new Error(this.state.errorCode ?? "FEATURE_NOT_FOUND");
const combined: Feature = { type: "Feature", properties: {}, geometry: { type: "MultiPoint", coordinates: found.flatMap(({ feature }) => collectCoordinates(feature.geometry)) } };
if (!this.moveCameraToFeature(map, combined, 0)) throw new Error("FEATURE_NOT_FOUND");
this.setHighlight({ sourceId, featureId: found[0].id });
return { located_ids: found.map(({ id }) => id), missing_ids: missing, fitted: true as const };
}
private moveCameraToFeature(map: MapLibreMap, feature: Feature, durationOverride?: number) {
const coordinates = collectCoordinates(feature.geometry);
if (!coordinates.length) {
this.setError("FEATURE_NOT_FOUND");
return false;
}
const duration = this.options.reducedMotion?.() ? 0 : 600;
const duration = durationOverride ?? (this.options.reducedMotion?.() ? 0 : 600);
if (feature.geometry.type === "Point") {
map.easeTo({
center: coordinates[0] as [number, number],
@@ -559,7 +581,7 @@ export class WorkbenchMapController implements WorkbenchMapCommands {
sourceAvailabilityGeneration !== (this.sourceAvailabilityGenerations.get(target.sourceId) ?? 0)
) return null;
this.pendingTargetSourceId = null;
const feature = collection.features[0];
const feature = collection.features.find((item) => String(item.properties?.id ?? item.properties?.device_id ?? item.id) === target.featureId);
if (!feature) {
this.patchState({ pending: false, errorCode: "FEATURE_NOT_FOUND" });
return null;