feat: extend supply workbench demo and map navigation
This commit is contained in:
@@ -21,6 +21,7 @@ import {
|
||||
type AgentApprovalMode,
|
||||
type AgentChatMessage,
|
||||
type AgentConnectionStatus,
|
||||
type AgentEventTriggerContext,
|
||||
type AgentModelOption,
|
||||
type AgentPermissionReply,
|
||||
type AgentPermissionRequest,
|
||||
@@ -694,6 +695,70 @@ export function useWorkbenchAgent({
|
||||
[approvalMode, chat, selectedModel, streaming]
|
||||
);
|
||||
|
||||
const triggerEvent = useCallback(
|
||||
async (trigger: AgentEventTriggerContext, prompt: string) => {
|
||||
if (streaming) {
|
||||
throw new Error("Agent 正在处理上一条指令,请稍后重试事件触发。");
|
||||
}
|
||||
|
||||
stopSessionStreamSubscription();
|
||||
const reserved = await clientRef.current.createSession({ triggerContext: trigger });
|
||||
const nextSessionId = reserved.session_id;
|
||||
const loadedSession = await clientRef.current.loadSession(nextSessionId);
|
||||
if (!loadedSession) {
|
||||
throw new Error("Agent 事件会话创建后无法读取。");
|
||||
}
|
||||
|
||||
applySessionId(nextSessionId);
|
||||
setSessionTitle(loadedSession.title || "水质投诉事件调查");
|
||||
setPermissionOverrides({});
|
||||
setQuestionOverrides({});
|
||||
const nextMessages = toAgentUiMessages(loadedSession.messages);
|
||||
chat.setMessages(nextMessages);
|
||||
setStreamRenderState(createCompletedStreamRenderState(nextMessages));
|
||||
setConnectionStatus("online");
|
||||
|
||||
if (loadedSession.runStatus === "running" || loadedSession.isStreaming) {
|
||||
setStatusLabel("Agent 事件调查进行中");
|
||||
startSessionStreamSubscription(nextSessionId);
|
||||
return { sessionId: nextSessionId, reused: true };
|
||||
}
|
||||
|
||||
if (nextMessages.length > 0) {
|
||||
setStatusLabel("Agent 事件调查会话已打开");
|
||||
await refreshSessionHistory();
|
||||
return { sessionId: nextSessionId, reused: true };
|
||||
}
|
||||
|
||||
setStatusLabel("Agent 正在调查水质投诉事件");
|
||||
const eventRun = chat.sendMessage(
|
||||
{ text: prompt },
|
||||
{
|
||||
body: {
|
||||
session_id: nextSessionId,
|
||||
model: selectedModel || undefined,
|
||||
approval_mode: approvalMode,
|
||||
trigger_context: trigger
|
||||
}
|
||||
}
|
||||
);
|
||||
void eventRun
|
||||
.then(() => refreshSessionHistory())
|
||||
.catch(() => undefined);
|
||||
return { sessionId: nextSessionId, reused: reserved.event_duplicate === true };
|
||||
},
|
||||
[
|
||||
approvalMode,
|
||||
applySessionId,
|
||||
chat,
|
||||
refreshSessionHistory,
|
||||
selectedModel,
|
||||
startSessionStreamSubscription,
|
||||
stopSessionStreamSubscription,
|
||||
streaming
|
||||
]
|
||||
);
|
||||
|
||||
const stopPrompt = useCallback(() => {
|
||||
chat.stop();
|
||||
stopSessionStreamSubscription();
|
||||
@@ -845,6 +910,7 @@ export function useWorkbenchAgent({
|
||||
replyPermission,
|
||||
replyQuestion,
|
||||
resolveRenderRef,
|
||||
triggerEvent,
|
||||
submitPrompt
|
||||
};
|
||||
}
|
||||
|
||||
@@ -50,6 +50,7 @@ import {
|
||||
scadaInteractionLayers
|
||||
} from "../map/scada";
|
||||
import { registerSupplyAssetImages } from "../map/supply-icons";
|
||||
import { installCityScene } from "../map/city-scene-control";
|
||||
import { useMapInteractions } from "./use-map-interactions";
|
||||
import { useSimulationLayerVisibility } from "./use-simulation-layer-visibility";
|
||||
|
||||
@@ -79,7 +80,10 @@ type UseWorkbenchMapResult = {
|
||||
sourceStatuses: WorkbenchSourceStatus[];
|
||||
availableSourceIds: WaterNetworkSourceId[];
|
||||
sourceFeatureCounts: Partial<Record<WaterNetworkSourceId, number>>;
|
||||
getScadaFeatures: (deviceIds?: string[], signal?: AbortSignal) => Promise<FeatureCollection<Point, ScadaDevice>>;
|
||||
getScadaFeatures: (
|
||||
deviceIds?: string[],
|
||||
signal?: AbortSignal
|
||||
) => Promise<FeatureCollection<Point, ScadaDevice>>;
|
||||
scadaObservedAt: string | null;
|
||||
refreshGeoServerLayers: () => boolean;
|
||||
fitNetworkBounds: () => void;
|
||||
@@ -96,6 +100,7 @@ export type WorkbenchSourceStatus = {
|
||||
|
||||
const SOURCE_STATUS_LABELS: Record<string, string> = {
|
||||
"mapbox-base": "Mapbox 底图",
|
||||
"tianditu-base": "天地图底图",
|
||||
"geoserver-mvt": "GeoServer MVT",
|
||||
"geoserver-catalog": "GeoServer 图层目录",
|
||||
"scada-api": "SCADA API",
|
||||
@@ -106,6 +111,10 @@ const SOURCE_STATUS_LABELS: Record<string, string> = {
|
||||
const SOURCE_GROUP_BY_ID: Record<string, string> = {
|
||||
"mapbox-light": "mapbox-base",
|
||||
"mapbox-satellite": "mapbox-base",
|
||||
"tianditu-vector": "tianditu-base",
|
||||
"tianditu-vector-annotation": "tianditu-base",
|
||||
"tianditu-image": "tianditu-base",
|
||||
"tianditu-image-annotation": "tianditu-base",
|
||||
...Object.fromEntries(
|
||||
GEOSERVER_WATER_NETWORK_SOURCE_IDS.map((sourceId) => [sourceId, "geoserver-mvt"])
|
||||
),
|
||||
@@ -197,9 +206,11 @@ export function useWorkbenchMap({
|
||||
? current
|
||||
: availability.availableSourceIds
|
||||
);
|
||||
setSourceFeatureCounts((current) => areFeatureCountsEqual(current, availability.featureCounts)
|
||||
? current
|
||||
: availability.featureCounts);
|
||||
setSourceFeatureCounts((current) =>
|
||||
areFeatureCountsEqual(current, availability.featureCounts)
|
||||
? current
|
||||
: availability.featureCounts
|
||||
);
|
||||
|
||||
if (availability.failedSourceIds.length > 0) {
|
||||
updateSourceStatus(
|
||||
@@ -251,12 +262,13 @@ export function useWorkbenchMap({
|
||||
setMapReady(false);
|
||||
setMapError(null);
|
||||
const mapboxToken = env.TJWATER_MAPBOX_ACCESS_TOKEN || undefined;
|
||||
const tiandituToken = env.TJWATER_TIANDITU_TOKEN || undefined;
|
||||
const scadaAbortController = new AbortController();
|
||||
let scadaRefreshTimeoutId: number | undefined;
|
||||
updateSourceStatus(setSourceStatuses, "scada-api", "loading", "正在通过API加载SCADA设备。");
|
||||
const map = new maplibregl.Map({
|
||||
container: containerRef.current,
|
||||
style: createBaseStyle(mapboxToken),
|
||||
style: createBaseStyle(mapboxToken, tiandituToken),
|
||||
pitch: 0,
|
||||
bearing: 0,
|
||||
maxZoom: MAP_MAX_ZOOM,
|
||||
@@ -265,9 +277,11 @@ export function useWorkbenchMap({
|
||||
});
|
||||
|
||||
window.__waterNetworkMap = map;
|
||||
let removeCityScene: (() => void) | undefined;
|
||||
|
||||
map.on("load", async () => {
|
||||
map.resize();
|
||||
removeCityScene = installCityScene(map);
|
||||
(map.getSource(SCADA_SOURCE_ID) as GeoJSONSource | undefined)?.setData(
|
||||
scadaCollectionRef.current
|
||||
);
|
||||
@@ -279,11 +293,7 @@ export function useWorkbenchMap({
|
||||
map.addSource(SIMULATION_SOURCE_IDS.impactArea, simulationSources.impactArea);
|
||||
map.addSource(SIMULATION_SOURCE_IDS.annotations, simulationSources.annotations);
|
||||
map.addSource(SIMULATION_SOURCE_IDS.backendAnalysis, simulationSources.backendAnalysis);
|
||||
syncSimulationSourceData(
|
||||
map,
|
||||
simulationOverlayRef.current,
|
||||
allowDemoSimulationRef.current
|
||||
);
|
||||
syncSimulationSourceData(map, simulationOverlayRef.current, allowDemoSimulationRef.current);
|
||||
await registerSupplyAssetImages(map);
|
||||
const scadaRegistration = await registerScadaImages(map);
|
||||
const activeScadaBusinessLayers =
|
||||
@@ -340,10 +350,7 @@ export function useWorkbenchMap({
|
||||
});
|
||||
|
||||
const refreshScada = async () => {
|
||||
const snapshotPromise = fetchScadaSnapshot(
|
||||
getAccessToken,
|
||||
scadaAbortController.signal
|
||||
);
|
||||
const snapshotPromise = fetchScadaSnapshot(getAccessToken, scadaAbortController.signal);
|
||||
scadaLoadPromiseRef.current = snapshotPromise
|
||||
.then((snapshot) => snapshot.collection)
|
||||
.catch(() => scadaCollectionRef.current);
|
||||
@@ -428,6 +435,7 @@ export function useWorkbenchMap({
|
||||
if (scadaRefreshTimeoutId !== undefined) {
|
||||
window.clearTimeout(scadaRefreshTimeoutId);
|
||||
}
|
||||
removeCityScene?.();
|
||||
map.remove();
|
||||
mapRef.current = null;
|
||||
delete window.__waterNetworkMap;
|
||||
@@ -500,8 +508,10 @@ function areFeatureCountsEqual(
|
||||
) {
|
||||
const currentEntries = Object.entries(current);
|
||||
const nextEntries = Object.entries(next);
|
||||
return currentEntries.length === nextEntries.length
|
||||
&& nextEntries.every(([sourceId, count]) => current[sourceId as WaterNetworkSourceId] === count);
|
||||
return (
|
||||
currentEntries.length === nextEntries.length &&
|
||||
nextEntries.every(([sourceId, count]) => current[sourceId as WaterNetworkSourceId] === count)
|
||||
);
|
||||
}
|
||||
|
||||
function networkViewKey(view: NetworkViewParams) {
|
||||
@@ -549,8 +559,9 @@ function syncDataDependentGeoServerLayers(
|
||||
|
||||
DATA_DEPENDENT_GEOSERVER_SOURCE_IDS.forEach((sourceId) => {
|
||||
if (availableSourceIdSet.has(sourceId)) return;
|
||||
map.getStyle().layers
|
||||
.filter((layer) => "source" in layer && layer.source === sourceId)
|
||||
map
|
||||
.getStyle()
|
||||
.layers.filter((layer) => "source" in layer && layer.source === sourceId)
|
||||
.reverse()
|
||||
.forEach((layer) => map.removeLayer(layer.id));
|
||||
if (map.getSource(sourceId)) map.removeSource(sourceId);
|
||||
@@ -711,11 +722,15 @@ function getSourceGroupFromErrorEvent(event: { sourceId?: string; error?: { mess
|
||||
return "mapbox-base";
|
||||
}
|
||||
|
||||
if (message.includes("tianditu.gov.cn") || message.includes("tianditu")) {
|
||||
return "tianditu-base";
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
function getSourceStatusMessage(sourceGroupId: string, status: WorkbenchSourceStatusValue) {
|
||||
if (sourceGroupId === "mapbox-base") {
|
||||
if (["mapbox-base", "tianditu-base"].includes(sourceGroupId)) {
|
||||
return status === "online"
|
||||
? "底图瓦片已恢复正常。"
|
||||
: "底图瓦片请求中断,业务图层仍可继续使用。";
|
||||
|
||||
Reference in New Issue
Block a user