diff --git a/src/components/chat/ChatToolCallBlock.tsx b/src/components/chat/ChatToolCallBlock.tsx index a552a00..2b63abb 100644 --- a/src/components/chat/ChatToolCallBlock.tsx +++ b/src/components/chat/ChatToolCallBlock.tsx @@ -118,6 +118,12 @@ const TOOL_META: Record = { actionLabel: "定位到地图", color: "#3ba272", }, + zoom_to_map: { + label: "缩放到坐标", + icon: , + actionLabel: "缩放到地图", + color: "#0ea5e9", + }, view_history: { label: "查看计算结果", icon: , @@ -176,6 +182,46 @@ function normalizeLocateIds(params: Record): string[] { return []; } +function readFiniteNumber(value: unknown): number | null { + if (typeof value === "number" && Number.isFinite(value)) { + return value; + } + if (typeof value === "string" && value.trim()) { + const parsed = Number(value); + return Number.isFinite(parsed) ? parsed : null; + } + return null; +} + +function buildZoomTo3857Action( + params: Record, +): Extract | null { + const rawCoordinate = params.coordinate ?? params.coordinates ?? params.center; + const tuple = Array.isArray(rawCoordinate) + ? rawCoordinate + : [params.x ?? params.lon ?? params.longitude, params.y ?? params.lat ?? params.latitude]; + const x = readFiniteNumber(tuple[0]); + const y = readFiniteNumber(tuple[1]); + if (x === null || y === null) { + return null; + } + + const zoom = readFiniteNumber(params.zoom); + const durationMs = readFiniteNumber(params.duration_ms ?? params.durationMs); + const rawSourceCrs = params.source_crs ?? params.sourceCrs ?? params.crs; + const normalizedSourceCrs = + typeof rawSourceCrs === "string" ? rawSourceCrs.trim().toUpperCase() : ""; + const sourceCrs = + normalizedSourceCrs === "EPSG:4326" ? "EPSG:4326" : "EPSG:3857"; + return { + type: "zoom_to_map", + coordinate: [x, y], + sourceCrs, + zoom: zoom ?? undefined, + durationMs: durationMs ?? undefined, + }; +} + function getToolDescription(toolCall: ToolCall): string { const { params } = toolCall; const resolveScadaFeatureInfos = (): [string, string][] => { @@ -281,6 +327,14 @@ function getToolDescription(toolCall: ToolCall): string { case "render_junctions": { return (params.render_ref as string | undefined) ?? "渲染引用"; } + case "zoom_to_map": { + const action = buildZoomTo3857Action(params); + if (!action) { + return "地图坐标"; + } + const zoom = action.zoom === undefined ? "" : ` · zoom ${action.zoom}`; + return `${action.coordinate[0]}, ${action.coordinate[1]} · ${action.sourceCrs}${zoom}`; + } case APPLY_LAYER_STYLE_TOOL: { const payload = parseApplyLayerStylePayload(params); return payload ? describeApplyLayerStyle(payload) : "图层样式"; @@ -341,6 +395,8 @@ function buildAction(toolCall: ToolCall): ChatToolAction | null { (params.end as string | undefined), }); switch (toolCall.tool) { + case "zoom_to_map": + return buildZoomTo3857Action(params); case "locate_features": { const featureTypeRaw = params.feature_type; const featureType = diff --git a/src/components/chat/hooks/useAgentToolActions.ts b/src/components/chat/hooks/useAgentToolActions.ts index ff9191a..b3ec0da 100644 --- a/src/components/chat/hooks/useAgentToolActions.ts +++ b/src/components/chat/hooks/useAgentToolActions.ts @@ -148,6 +148,46 @@ const compactNames = (names: string[]) => { : names.join(", "); }; +const readFiniteNumber = (value: unknown): number | null => { + if (typeof value === "number" && Number.isFinite(value)) { + return value; + } + if (typeof value === "string" && value.trim()) { + const parsed = Number(value); + return Number.isFinite(parsed) ? parsed : null; + } + return null; +}; + +const parseZoomTo3857Action = ( + params: Record, +): Extract | null => { + const rawCoordinate = params.coordinate ?? params.coordinates ?? params.center; + const tuple = Array.isArray(rawCoordinate) + ? rawCoordinate + : [params.x ?? params.lon ?? params.longitude, params.y ?? params.lat ?? params.latitude]; + const x = readFiniteNumber(tuple[0]); + const y = readFiniteNumber(tuple[1]); + if (x === null || y === null) { + return null; + } + + const zoom = readFiniteNumber(params.zoom); + const durationMs = readFiniteNumber(params.duration_ms ?? params.durationMs); + const rawSourceCrs = params.source_crs ?? params.sourceCrs ?? params.crs; + const normalizedSourceCrs = + typeof rawSourceCrs === "string" ? rawSourceCrs.trim().toUpperCase() : ""; + const sourceCrs = + normalizedSourceCrs === "EPSG:4326" ? "EPSG:4326" : "EPSG:3857"; + return { + type: "zoom_to_map", + coordinate: [x, y], + sourceCrs, + zoom: zoom ?? undefined, + durationMs: durationMs ?? undefined, + }; +}; + const buildLocateArtifact = ( tool: string, params: Record, @@ -190,6 +230,18 @@ const buildToolAction = ( }; } + if (tool === "zoom_to_map") { + const action = parseZoomTo3857Action(params); + return { + action, + kind: "map", + title: "缩放到地图坐标", + description: action + ? `${action.coordinate[0]}, ${action.coordinate[1]} (${action.sourceCrs})` + : "地图坐标", + }; + } + if (tool === "locate_features" || LOCATE_TOOL_CONFIG[tool]) { const locate = buildLocateArtifact(tool, params); return { diff --git a/src/components/olmap/core/Controls/useToolbarChatActions.ts b/src/components/olmap/core/Controls/useToolbarChatActions.ts index f29a982..7e472ea 100644 --- a/src/components/olmap/core/Controls/useToolbarChatActions.ts +++ b/src/components/olmap/core/Controls/useToolbarChatActions.ts @@ -2,6 +2,7 @@ import { useCallback, useEffect, useRef, type Dispatch, type SetStateAction } fr import Feature from "ol/Feature"; import { GeoJSON } from "ol/format"; import Point from "ol/geom/Point"; +import { transform } from "ol/proj"; import { bbox, featureCollection } from "@turf/turf"; import { useChatToolActionHandler } from "@/hooks/useChatToolActionHandler"; @@ -110,6 +111,18 @@ export const useToolbarChatActions = ({ locateFeatures(action.ids, action.layer, action.geometryKind); break; } + case "zoom_to_map": { + const center = + action.sourceCrs === "EPSG:4326" + ? transform(action.coordinate, "EPSG:4326", "EPSG:3857") + : action.coordinate; + map?.getView().animate({ + center, + zoom: action.zoom ?? map.getView().getZoom() ?? 18, + duration: action.durationMs ?? 1000, + }); + break; + } case "view_history": { setChatPanelFeatureInfos(action.featureInfos); setChatPanelType(action.dataType); diff --git a/src/store/chatToolStore.ts b/src/store/chatToolStore.ts index 7a0e4d4..6147ee7 100644 --- a/src/store/chatToolStore.ts +++ b/src/store/chatToolStore.ts @@ -15,6 +15,13 @@ export type ChatToolAction = layer: string; geometryKind: "point" | "line"; } + | { + type: "zoom_to_map"; + coordinate: [number, number]; + sourceCrs?: "EPSG:3857" | "EPSG:4326"; + zoom?: number; + durationMs?: number; + } | { type: "view_history"; featureInfos: [string, string][];