"use client"; import React, { useCallback, useState } from "react"; import { Box, Button, Chip, Paper, Stack, Typography, alpha, useTheme, Collapse, IconButton, } from "@mui/material"; import LocationOnRounded from "@mui/icons-material/LocationOnRounded"; import TimelineRounded from "@mui/icons-material/TimelineRounded"; import SensorsRounded from "@mui/icons-material/SensorsRounded"; import CheckCircleRounded from "@mui/icons-material/CheckCircleRounded"; import KeyboardArrowDownRounded from "@mui/icons-material/KeyboardArrowDownRounded"; import KeyboardArrowUpRounded from "@mui/icons-material/KeyboardArrowUpRounded"; import { useChatToolStore, type ChatToolAction, } from "@/store/chatToolStore"; import type { ToolCall } from "./chatMessageSections"; import { APPLY_LAYER_STYLE_TOOL, describeApplyLayerStyle, parseApplyLayerStylePayload, } from "./toolCallStyleHelpers"; /* ------------------------------------------------------------------ */ /* Interactive card rendered inside a chat bubble for tool actions */ /* (locate nodes/pipes, open history/SCADA panels). */ /* ------------------------------------------------------------------ */ type ToolMeta = { label: string; icon: React.ReactNode; actionLabel: string; color: string; }; const LOCATE_TOOL_TO_LAYER: Record = { locate_features: "", locate_junctions: "geo_junctions_mat", locate_pipes: "geo_pipes_mat", locate_valves: "geo_valves", locate_reservoirs: "geo_reservoirs", locate_pumps: "geo_pumps", locate_tanks: "geo_tanks", }; const LOCATE_LINE_TOOLS = new Set(["locate_pipes"]); const LOCATE_ID_PARAM_KEYS = [ "ids", "id", "feature_ids", "feature_id", "node_ids", "node_id", "junction_ids", "junction_id", "pipe_ids", "pipe_id", "valve_ids", "valve_id", "reservoir_ids", "reservoir_id", "pump_ids", "pump_id", "tank_ids", "tank_id", ] as const; const TOOL_META: Record = { locate_features: { label: "定位要素", icon: , actionLabel: "定位到地图", color: "#5470c6", }, locate_junctions: { label: "定位节点", icon: , actionLabel: "定位到地图", color: "#5470c6", }, locate_pipes: { label: "定位管道", icon: , actionLabel: "定位到地图", color: "#91cc75", }, locate_valves: { label: "定位阀门", icon: , actionLabel: "定位到地图", color: "#9a60b4", }, locate_reservoirs: { label: "定位水源", icon: , actionLabel: "定位到地图", color: "#ea7ccc", }, locate_pumps: { label: "定位泵站", icon: , actionLabel: "定位到地图", color: "#fc8452", }, locate_tanks: { label: "定位水池", icon: , actionLabel: "定位到地图", color: "#3ba272", }, view_history: { label: "查看计算结果", icon: , actionLabel: "查看曲线", color: "#fac858", }, view_scada: { label: "查看监测数据", icon: , actionLabel: "查看数据", color: "#ee6666", }, show_chart: { label: "显示图表", icon: , actionLabel: "显示", color: "#73c0de", }, render_junctions: { label: "渲染节点", icon: , actionLabel: "应用渲染", color: "#3b82f6", }, [APPLY_LAYER_STYLE_TOOL]: { label: "图层样式", icon: , actionLabel: "应用样式", color: "#14b8a6", }, }; /* ---------- helpers ---------- */ function normalizeLocateIds(params: Record): string[] { for (const key of LOCATE_ID_PARAM_KEYS) { const rawValue = params[key]; if (Array.isArray(rawValue)) { const normalized = rawValue .map((id) => String(id).trim()) .filter(Boolean); if (normalized.length > 0) { return normalized; } } if (typeof rawValue === "string" || typeof rawValue === "number") { const normalized = String(rawValue) .split(",") .map((id) => id.trim()) .filter(Boolean); if (normalized.length > 0) { return normalized; } } } return []; } function getToolDescription(toolCall: ToolCall): string { const { params } = toolCall; const resolveScadaFeatureInfos = (): [string, string][] => { const rawFeatureInfos = params.feature_infos; if (Array.isArray(rawFeatureInfos)) { const normalizedFeatureInfos = rawFeatureInfos .map((item) => (Array.isArray(item) ? item : null)) .filter((item): item is [unknown, unknown] => Boolean(item)) .map( (item) => [String(item[0] ?? ""), String(item[1] ?? "scada")] as [ string, string, ], ) .filter(([id]) => id.trim().length > 0); if (normalizedFeatureInfos.length > 0) { return normalizedFeatureInfos; } } const rawDeviceIds = params.device_ids ?? params.deviceId ?? params.device_id ?? params.id ?? params.ids; const deviceIds = Array.isArray(rawDeviceIds) ? rawDeviceIds.map((id) => String(id)) : typeof rawDeviceIds === "string" ? rawDeviceIds .split(",") .map((id) => id.trim()) .filter(Boolean) : []; return deviceIds.map((id) => [id, "scada"]); }; const resolveTimeRange = () => ({ startTime: (params.start_time as string | undefined) ?? (params.startTime as string | undefined) ?? (params.from as string | undefined) ?? (params.start as string | undefined), endTime: (params.end_time as string | undefined) ?? (params.endTime as string | undefined) ?? (params.to as string | undefined) ?? (params.end as string | undefined), }); const resolveLocateFeatureType = (): string => { const rawType = params.feature_type; if (typeof rawType === "string" && rawType.trim()) { return rawType.trim().toLowerCase(); } return ""; }; switch (toolCall.tool) { case "locate_features": case "locate_junctions": case "locate_pipes": case "locate_valves": case "locate_reservoirs": case "locate_pumps": case "locate_tanks": { const ids = normalizeLocateIds(params); const idsText = ids.length > 3 ? `${ids.slice(0, 3).join(", ")} 等 ${ids.length} 个` : ids.join(", "); if (toolCall.tool !== "locate_features") { return idsText; } const featureType = resolveLocateFeatureType(); if (!featureType) { return idsText; } return idsText ? `${featureType} · ${idsText}` : featureType; } case "view_history": case "view_scada": { const infos = toolCall.tool === "view_scada" ? resolveScadaFeatureInfos() : ((params.feature_infos as [string, string][] | undefined) ?? []); const names = infos.map(([id]) => id); const base = names.length > 3 ? `${names.slice(0, 3).join(", ")} 等 ${names.length} 个` : names.join(", "); const { startTime, endTime } = resolveTimeRange(); if (!startTime && !endTime) { return base; } const rangeLabel = `时间段: ${startTime ?? "--"} ~ ${endTime ?? "--"}`; return base ? `${base} · ${rangeLabel}` : rangeLabel; } case "show_chart": { return (params.title as string | undefined) ?? "数据图表"; } case "render_junctions": { return (params.render_ref as string | undefined) ?? "渲染引用"; } case APPLY_LAYER_STYLE_TOOL: { const payload = parseApplyLayerStylePayload(params); return payload ? describeApplyLayerStyle(payload) : "图层样式"; } default: return ""; } } function buildAction(toolCall: ToolCall): ChatToolAction | null { const { params } = toolCall; const resolveScadaFeatureInfos = (): [string, string][] => { const rawFeatureInfos = params.feature_infos; if (Array.isArray(rawFeatureInfos)) { const normalizedFeatureInfos = rawFeatureInfos .map((item) => (Array.isArray(item) ? item : null)) .filter((item): item is [unknown, unknown] => Boolean(item)) .map( (item) => [String(item[0] ?? ""), String(item[1] ?? "scada")] as [ string, string, ], ) .filter(([id]) => id.trim().length > 0); if (normalizedFeatureInfos.length > 0) { return normalizedFeatureInfos; } } const rawDeviceIds = params.device_ids ?? params.deviceId ?? params.device_id ?? params.id ?? params.ids; const deviceIds = Array.isArray(rawDeviceIds) ? rawDeviceIds.map((id) => String(id)) : typeof rawDeviceIds === "string" ? rawDeviceIds .split(",") .map((id) => id.trim()) .filter(Boolean) : []; return deviceIds.map((id) => [id, "scada"]); }; const resolveTimeRange = () => ({ startTime: (params.start_time as string | undefined) ?? (params.startTime as string | undefined) ?? (params.from as string | undefined) ?? (params.start as string | undefined), endTime: (params.end_time as string | undefined) ?? (params.endTime as string | undefined) ?? (params.to as string | undefined) ?? (params.end as string | undefined), }); switch (toolCall.tool) { case "locate_features": { const featureTypeRaw = params.feature_type; const featureType = typeof featureTypeRaw === "string" ? featureTypeRaw.trim().toLowerCase() : ""; const config = locateFeatureTypeToConfig(featureType); if (!config) return null; return { type: "locate_features", ids: normalizeLocateIds(params), layer: config.layer, geometryKind: config.geometryKind, }; } case "locate_junctions": case "locate_pipes": case "locate_valves": case "locate_reservoirs": case "locate_pumps": case "locate_tanks": { const layer = LOCATE_TOOL_TO_LAYER[toolCall.tool]; if (!layer) return null; return { type: "locate_features", ids: normalizeLocateIds(params), layer, geometryKind: LOCATE_LINE_TOOLS.has(toolCall.tool) ? "line" : "point", }; } case "view_history": { const historyRange = resolveTimeRange(); return { type: "view_history", featureInfos: (params.feature_infos as [string, string][] | undefined) ?? [], dataType: (params.data_type as "realtime" | "scheme" | "none" | undefined) ?? "realtime", startTime: historyRange.startTime, endTime: historyRange.endTime, }; } case "view_scada": { const scadaRange = resolveTimeRange(); return { type: "view_scada", featureInfos: resolveScadaFeatureInfos(), startTime: scadaRange.startTime, endTime: scadaRange.endTime, }; } case "show_chart": return { type: "show_chart", title: params.title as string | undefined, chartType: (params.chart_type as "line" | "bar" | "pie" | undefined) ?? "line", xData: (params.x_data as string[] | undefined) ?? [], series: (params.series as | Array<{ name: string; data: number[]; type?: "line" | "bar" }> | undefined) ?? [], xAxisName: params.x_axis_name as string | undefined, yAxisName: params.y_axis_name as string | undefined, }; case "render_junctions": { const renderRef = typeof params.render_ref === "string" ? params.render_ref.trim() : ""; if (!renderRef) { return null; } return { type: "render_junctions", renderRef, }; } case APPLY_LAYER_STYLE_TOOL: { const payload = parseApplyLayerStylePayload(params); if (!payload) { return null; } return { type: "apply_layer_style", layerId: payload.layerId, resetToDefault: payload.resetToDefault, styleConfig: payload.styleConfig, }; } default: return null; } } /* ---------- component ---------- */ export interface ChatToolCallBlockProps { toolCall: ToolCall; } export const ChatToolCallBlock: React.FC = ({ toolCall, }) => { const theme = useTheme(); const dispatch = useChatToolStore((s) => s.dispatch); const [executed, setExecuted] = useState(false); const [expanded, setExpanded] = useState(false); const meta: ToolMeta = TOOL_META[toolCall.tool] ?? { label: toolCall.tool, icon: , actionLabel: "执行", color: "#00acc1", }; const description = getToolDescription(toolCall); const handleExecute = useCallback(() => { const action = buildAction(toolCall); if (action) { dispatch(action); setExecuted(true); } }, [toolCall, dispatch]); return ( setExpanded(!expanded)} sx={{ p: 1.5, display: "flex", alignItems: "center", cursor: "pointer", gap: 1.5, }} > {/* Icon */} {meta.icon} {/* Title */} {meta.label} {!expanded && description && ( • {description} )} {expanded ? : } {description && ( 执行参数 {description} )} {executed ? ( } label="已执行" size="small" sx={{ bgcolor: alpha("#00e676", 0.15), color: "#00c853", fontWeight: 700, fontSize: "0.75rem", }} /> ) : ( )} ); }; const locateFeatureTypeToConfig = ( featureType: string, ): { layer: string; geometryKind: "point" | "line" } | null => { switch (featureType) { case "junction": case "junctions": return { layer: "geo_junctions_mat", geometryKind: "point" }; case "pipe": case "pipes": return { layer: "geo_pipes_mat", geometryKind: "line" }; case "valve": case "valves": return { layer: "geo_valves", geometryKind: "point" }; case "reservoir": case "reservoirs": return { layer: "geo_reservoirs", geometryKind: "point" }; case "pump": case "pumps": return { layer: "geo_pumps", geometryKind: "point" }; case "tank": case "tanks": return { layer: "geo_tanks", geometryKind: "point" }; default: return null; } };