diff --git a/src/components/chat/ChatToolCallBlock.tsx b/src/components/chat/ChatToolCallBlock.tsx index d396a76..a7f2c79 100644 --- a/src/components/chat/ChatToolCallBlock.tsx +++ b/src/components/chat/ChatToolCallBlock.tsx @@ -268,12 +268,7 @@ function getToolDescription(toolCall: ToolCall): string { return (params.title as string | undefined) ?? "数据图表"; } case "render_junctions": { - const nodeAreaMap = - params.node_area_map && typeof params.node_area_map === "object" - ? (params.node_area_map as Record) - : {}; - const areaIds = Array.isArray(params.area_ids) ? params.area_ids : []; - return `${Object.keys(nodeAreaMap).length} 个节点 · ${areaIds.length || new Set(Object.values(nodeAreaMap).map(String)).size} 个分区`; + return (params.render_ref as string | undefined) ?? "渲染引用"; } default: return ""; @@ -398,28 +393,14 @@ function buildAction(toolCall: ToolCall): ChatToolAction | null { yAxisName: params.y_axis_name as string | undefined, }; case "render_junctions": { - const nodeAreaMap = - params.node_area_map && typeof params.node_area_map === "object" - ? Object.fromEntries( - Object.entries(params.node_area_map as Record) - .map(([key, value]) => [String(key), String(value ?? "")]) - .filter(([, value]) => value.trim().length > 0), - ) - : {}; + const renderRef = + typeof params.render_ref === "string" ? params.render_ref.trim() : ""; + if (!renderRef) { + return null; + } return { type: "render_junctions", - nodeAreaMap, - areaIds: Array.isArray(params.area_ids) - ? params.area_ids.map((item) => String(item).trim()).filter(Boolean) - : [], - areaColors: - params.area_colors && typeof params.area_colors === "object" - ? Object.fromEntries( - Object.entries(params.area_colors as Record) - .map(([key, value]) => [String(key), String(value ?? "")]) - .filter(([, value]) => value.trim().length > 0), - ) - : {}, + renderRef, }; } default: diff --git a/src/components/chat/GlobalChatbox.tsx b/src/components/chat/GlobalChatbox.tsx index f6985fe..45cc983 100644 --- a/src/components/chat/GlobalChatbox.tsx +++ b/src/components/chat/GlobalChatbox.tsx @@ -1,7 +1,13 @@ "use client"; -import React, { useCallback, useEffect, useRef, useState } from "react"; -import { Box, Drawer, alpha, useTheme } from "@mui/material"; +import React, { + useCallback, + useEffect, + useLayoutEffect, + useRef, + useState, +} from "react"; +import { Box, Drawer, alpha, useMediaQuery, useTheme } from "@mui/material"; import type { AgentModel } from "@/lib/chatStream"; import { AgentComposer } from "./AgentComposer"; @@ -27,6 +33,7 @@ export const GlobalChatbox: React.FC = ({ open, onClose }) => { const bottomRef = useRef(null); const inputRef = useRef(null); const theme = useTheme(); + const isDesktop = useMediaQuery(theme.breakpoints.up("sm")); const { speechState, @@ -158,6 +165,32 @@ export const GlobalChatbox: React.FC = ({ open, onClose }) => { }; }, [isResizing]); + useLayoutEffect(() => { + const body = document.body; + const html = document.documentElement; + const previousBodyPaddingRight = body.style.paddingRight; + const previousBodyTransition = body.style.transition; + const previousBodyBoxSizing = body.style.boxSizing; + const previousHtmlBoxSizing = html.style.boxSizing; + const reservedWidth = open && isDesktop ? `${width}px` : "0px"; + + body.style.boxSizing = "border-box"; + html.style.boxSizing = "border-box"; + body.style.paddingRight = reservedWidth; + body.style.transition = isResizing + ? previousBodyTransition + : [previousBodyTransition, "padding-right 240ms cubic-bezier(0.2, 0.8, 0.2, 1)"] + .filter(Boolean) + .join(", "); + + return () => { + body.style.paddingRight = previousBodyPaddingRight; + body.style.transition = previousBodyTransition; + body.style.boxSizing = previousBodyBoxSizing; + html.style.boxSizing = previousHtmlBoxSizing; + }; + }, [isDesktop, isResizing, open, width]); + return ( ) => ({ (params.end as string | undefined), }); -const resolveStringRecord = (value: unknown): Record => { - if (!value || typeof value !== "object" || Array.isArray(value)) { - return {}; - } - - return Object.fromEntries( - Object.entries(value as Record) - .map(([key, recordValue]) => [String(key), String(recordValue ?? "")]) - .filter(([, recordValue]) => recordValue.trim().length > 0), - ); -}; - -const resolveStringArray = (value: unknown): string[] => { - if (!Array.isArray(value)) { - return []; - } - - return value.map((item) => String(item).trim()).filter(Boolean); -}; - const compactNames = (names: string[]) => { if (!names.length) return ""; return names.length > 3 @@ -251,20 +231,20 @@ const buildToolAction = ( } if (tool === "render_junctions") { - const nodeAreaMap = resolveStringRecord(params.node_area_map); - const areaIds = resolveStringArray(params.area_ids); - const areaColors = resolveStringRecord(params.area_colors); + const renderRef = + typeof params.render_ref === "string" ? params.render_ref.trim() : ""; return { - action: { - type: "render_junctions", - nodeAreaMap, - areaIds, - areaColors, - }, + action: renderRef + ? { + type: "render_junctions", + renderRef, + sessionId: undefined, + } + : null, kind: "map", title: "渲染节点分区", - description: `${Object.keys(nodeAreaMap).length} 个节点`, + description: renderRef || "渲染引用", }; } @@ -286,6 +266,11 @@ export const useAgentToolActions = () => { event.params, ); + const normalizedAction = + action?.type === "render_junctions" + ? { ...action, sessionId: event.sessionId } + : action; + options.appendArtifact(options.assistantMessageId, { id: `${event.tool}-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`, tool: event.tool, @@ -295,8 +280,8 @@ export const useAgentToolActions = () => { params: event.params, }); - if (action) { - dispatchToolAction(action); + if (normalizedAction) { + dispatchToolAction(normalizedAction); } }, [dispatchToolAction], diff --git a/src/components/olmap/core/Controls/useToolbarChatActions.ts b/src/components/olmap/core/Controls/useToolbarChatActions.ts index b606d01..bf9f3a9 100644 --- a/src/components/olmap/core/Controls/useToolbarChatActions.ts +++ b/src/components/olmap/core/Controls/useToolbarChatActions.ts @@ -5,8 +5,13 @@ import Point from "ol/geom/Point"; import { bbox, featureCollection } from "@turf/turf"; import { useChatToolActionHandler } from "@/hooks/useChatToolActionHandler"; -import { applyJunctionAreaRender } from "@components/olmap/DMALeakDetection/applyJunctionAreaRender"; +import { + applyJunctionAreaRender, + type JunctionAreaRenderPayload, +} from "@components/olmap/DMALeakDetection/applyJunctionAreaRender"; +import { apiFetch } from "@/lib/apiFetch"; import { queryFeaturesByIds } from "@/utils/mapQueryService"; +import { config } from "@/config/config"; import { useMap } from "../MapComponent"; type UseToolbarChatActionsParams = { @@ -30,6 +35,7 @@ export const useToolbarChatActions = ({ }: UseToolbarChatActionsParams) => { const map = useMap(); const chatJunctionRenderCleanupRef = useRef<(() => void) | null>(null); + const renderRequestSeqRef = useRef(0); const disposeChatJunctionRender = useCallback(() => { chatJunctionRenderCleanupRef.current?.(); @@ -122,22 +128,82 @@ export const useToolbarChatActions = ({ } case "render_junctions": { disposeChatJunctionRender(); + renderRequestSeqRef.current += 1; + const requestSeq = renderRequestSeqRef.current; - if (Object.keys(action.nodeAreaMap).length === 0) { + if (!action.renderRef || !map) { break; } - if (map) { - chatJunctionRenderCleanupRef.current = applyJunctionAreaRender( - map, - { - nodeAreaMap: action.nodeAreaMap, - areaIds: action.areaIds, - areaColors: action.areaColors, - }, - { propertyKey: "chat_junction_render_index" }, - ); - } + void (async () => { + try { + const query = action.sessionId + ? `?session_id=${encodeURIComponent(action.sessionId)}` + : ""; + const response = await apiFetch( + `${config.AGENT_URL}/api/v1/agent/chat/render-ref/${encodeURIComponent(action.renderRef)}${query}`, + { + method: "GET", + projectHeaderMode: "include", + userHeaderMode: "include", + skipAuthRedirect: true, + }, + ); + + if (!response.ok) { + throw new Error(`render ref request failed: ${response.status}`); + } + + const payload = (await response.json()) as { + data?: { + node_area_map?: Record; + area_ids?: unknown[]; + area_colors?: Record; + }; + }; + + const data = payload.data; + if (!data?.node_area_map) { + throw new Error("render ref payload missing node_area_map"); + } + + const renderPayload: JunctionAreaRenderPayload = { + nodeAreaMap: Object.fromEntries( + Object.entries(data.node_area_map).map(([key, value]) => [ + String(key), + String(value ?? ""), + ]), + ), + areaIds: Array.isArray(data.area_ids) + ? data.area_ids.map((item) => String(item).trim()).filter(Boolean) + : [], + areaColors: + data.area_colors && typeof data.area_colors === "object" + ? Object.fromEntries( + Object.entries(data.area_colors).map(([key, value]) => [ + String(key), + String(value ?? ""), + ]), + ) + : {}, + }; + + if ( + requestSeq !== renderRequestSeqRef.current || + Object.keys(renderPayload.nodeAreaMap).length === 0 + ) { + return; + } + + chatJunctionRenderCleanupRef.current = applyJunctionAreaRender( + map, + renderPayload, + { propertyKey: "chat_junction_render_index" }, + ); + } catch (error) { + console.error("Failed to resolve render_ref for junction render:", error); + } + })(); break; } } diff --git a/src/store/chatToolStore.ts b/src/store/chatToolStore.ts index 5d44ecf..317ffa4 100644 --- a/src/store/chatToolStore.ts +++ b/src/store/chatToolStore.ts @@ -37,9 +37,8 @@ export type ChatToolAction = } | { type: "render_junctions"; - nodeAreaMap: Record; - areaIds?: string[]; - areaColors?: Record; + renderRef: string; + sessionId?: string; }; interface ChatToolState {