"use client"; import type { Map as MapLibreMap } from "maplibre-gl"; import { useCallback, useEffect, useMemo, useRef, useState, type ComponentProps, type CSSProperties } from "react"; import { X } from "lucide-react"; import { AnimatePresence, motion, useReducedMotion } from "motion/react"; import { AgentCollapsedRail, AgentCommandPanel, AgentPersona, type AgentUiResult } from "@/features/agent"; import { toTrustedMapAction, type UIEnvelopePayload } from "@/features/agent/ui-envelope"; import type { FrontendActionRequest } from "@/features/agent/frontend-action"; import { MapErrorNotice, MapLoadingNotice, MapSourceStatusNotice, showMapNotice, type MapSourceStatus } from "@/features/map/core/components/notice"; import { MapScaleLine } from "@/features/map/core/components/scale-line"; import { MapToolbar, type MapToolbarItem } from "@/features/map/core/components/toolbar"; import { MapZoom } from "@/features/map/core/components/zoom"; import { MAP_CONTROL_SURFACE_CLASS_NAME, MAP_MAJOR_PANEL_RADIUS_CLASS_NAME } from "@/features/map/core/components/map-control-styles"; import { cn } from "@/lib/utils"; import { AgentTaskTicker } from "./components/agent-task-ticker"; import { FeaturePopover } from "./components/feature-popover"; import { ScheduledConditionFeed } from "./components/scheduled-condition-feed"; import { ToolbarPanel, type ExportViewPreset, type ToolbarPanelProps, type ToolbarToolId } from "./components/toolbar-panel"; import { WorkbenchTopBar } from "./components/workbench-top-bar"; import { SCHEDULED_CONDITION_REFRESH_INTERVAL_MS, createOperationalScheduledConditions } from "./data/scheduled-conditions"; import { WORKBENCH_SCENARIOS, WORKBENCH_USER } from "./data/workbench-session"; import { useWorkbenchAgent } from "./hooks/use-workbench-agent"; import { useWorkbenchDrawing, type WorkbenchDrawMode } from "./hooks/use-workbench-drawing"; import { useWorkbenchMap } from "./hooks/use-workbench-map"; import { toMapFeatureReference } from "./hooks/use-map-interactions"; import { useWorkbenchMeasurement, type WorkbenchMeasureMode, type WorkbenchMeasureUnit } from "./hooks/use-workbench-measurement"; import { getWorkbenchBasemapTone, getWorkbenchLayoutCssVariables } from "./layout/workbench-layout"; import { getResponsiveWorkbenchPadding } from "./map/camera"; import { exportMapViewImage } from "./map/export-view"; import { BASE_LAYER_IDS, BASE_LAYER_OPTIONS, INITIAL_LAYER_VISIBILITY, MAP_LEGEND_ITEMS, WORKBENCH_LAYER_GROUPS, createLayerControlItems, getWorkbenchLayerIds } from "./map/map-control-config"; import { waterNetworkToolbarItems } from "./map/toolbar-config"; import type { DetailFeature, ScheduledConditionItem, ScheduledConditionRecord, WorkbenchAlert } from "./types"; import { createAlertQueueConversationPrompt } from "./utils/scheduled-condition-prompts"; const HEADER_DATA_TIME_STEP_MINUTES = 5; const INITIAL_HEADER_DATA_TIME = "同步中"; const WORKBENCH_LAYOUT_CSS_VARIABLES = getWorkbenchLayoutCssVariables(); function formatHeaderDataTime(date: Date) { const snappedMinutes = Math.floor(date.getMinutes() / HEADER_DATA_TIME_STEP_MINUTES) * HEADER_DATA_TIME_STEP_MINUTES; const year = date.getFullYear(); const month = String(date.getMonth() + 1).padStart(2, "0"); const day = String(date.getDate()).padStart(2, "0"); const hours = String(date.getHours()).padStart(2, "0"); const minutes = String(snappedMinutes).padStart(2, "0"); return `${year}-${month}-${day} ${hours}:${minutes}`; } function getMsUntilNextHeaderDataTimeTick(date: Date) { const nextTick = new Date(date); const nextMinutes = Math.floor(date.getMinutes() / HEADER_DATA_TIME_STEP_MINUTES) * HEADER_DATA_TIME_STEP_MINUTES + HEADER_DATA_TIME_STEP_MINUTES; nextTick.setMinutes(nextMinutes, 0, 0); return Math.max(nextTick.getTime() - date.getTime(), 1_000); } export function MapWorkbenchPage() { const hasMapboxToken = Boolean(process.env.NEXT_PUBLIC_MAPBOX_ACCESS_TOKEN); const mapContainerRef = useRef(null); const [detailFeature, setDetailFeature] = useState(null); const [impactVisible, setImpactVisible] = useState(false); const [isLargeScreen, setIsLargeScreen] = useState(false); const [activeToolId, setActiveToolId] = useState(null); const [conditionFeedVisible, setConditionFeedVisible] = useState(false); const [conditionFeedMounted, setConditionFeedMounted] = useState(true); const [conditionFeedExpanded, setConditionFeedExpanded] = useState(false); const [taskTickerVisible, setTaskTickerVisible] = useState(true); const [selectedConditionId, setSelectedConditionId] = useState(null); const [conditionFocusRequest, setConditionFocusRequest] = useState<{ conditionId: string; requestId: number; } | null>(null); const [activeBaseLayerId, setActiveBaseLayerId] = useState(hasMapboxToken ? "mapbox-light" : "none"); const basemapTone = getWorkbenchBasemapTone(activeBaseLayerId); const [activeScenarioId, setActiveScenarioId] = useState(WORKBENCH_SCENARIOS[0]?.id ?? ""); const [activeMeasureModeId, setActiveMeasureModeId] = useState("distance"); const [activeMeasureUnitId, setActiveMeasureUnitId] = useState("km"); const [activeDrawToolId, setActiveDrawToolId] = useState(null); const [layerVisibility, setLayerVisibility] = useState>(INITIAL_LAYER_VISIBILITY); const [agentUiResults, setAgentUiResults] = useState([]); const [scheduledConditions, setScheduledConditions] = useState([]); const [scheduledConditionsLoading, setScheduledConditionsLoading] = useState(true); const [headerDataTime, setHeaderDataTime] = useState(INITIAL_HEADER_DATA_TIME); const baseLayerOptions = useMemo( () => BASE_LAYER_OPTIONS.map((option) => ({ ...option, disabled: option.id !== "none" && !hasMapboxToken })), [hasMapboxToken] ); const handleSelectFeature = useCallback((feature: DetailFeature) => { setDetailFeature(feature); }, []); useEffect(() => { const mediaQuery = window.matchMedia("(min-width: 1024px)"); const handleChange = () => setIsLargeScreen(mediaQuery.matches); handleChange(); setConditionFeedVisible(window.innerWidth >= 1280); mediaQuery.addEventListener("change", handleChange); return () => mediaQuery.removeEventListener("change", handleChange); }, []); useEffect(() => { let timeoutId: number; const refreshHeaderDataTime = () => { const now = new Date(); setHeaderDataTime(formatHeaderDataTime(now)); timeoutId = window.setTimeout(refreshHeaderDataTime, getMsUntilNextHeaderDataTimeTick(now)); }; refreshHeaderDataTime(); return () => window.clearTimeout(timeoutId); }, []); useEffect(() => { const refreshScheduledConditions = () => { setScheduledConditions(createOperationalScheduledConditions(new Date())); setScheduledConditionsLoading(false); }; refreshScheduledConditions(); const intervalId = window.setInterval(refreshScheduledConditions, SCHEDULED_CONDITION_REFRESH_INTERVAL_MS); return () => window.clearInterval(intervalId); }, []); const shouldShowConditionFeed = conditionFeedVisible && !activeToolId; useEffect(() => { if (shouldShowConditionFeed) { setConditionFeedMounted(true); return; } const timeoutId = window.setTimeout(() => { setConditionFeedMounted(false); }, 170); return () => window.clearTimeout(timeoutId); }, [shouldShowConditionFeed]); const agent = useWorkbenchAgent({ onUiEnvelope: handleAgentUiEnvelope, onFrontendAction: handleFrontendAction }); const leftPanelOpen = isLargeScreen && agent.panelOpen; const rightPanelOpen = isLargeScreen && shouldShowConditionFeed; const { mapRef, mapReady, mapError, sourceStatuses, fitNetworkBounds } = useWorkbenchMap({ containerRef: mapContainerRef, leftPanelOpen, rightPanelOpen, impactVisible, onSelectFeature: handleSelectFeature, selectedFeature: detailFeature }); const activeAgentUiResults = useMemo( () => agentUiResults.filter((result) => result.sessionId === agent.sessionId), [agent.sessionId, agentUiResults] ); const conditionAlerts = useMemo( () => createScheduledConditionAlerts(scheduledConditions), [scheduledConditions] ); const runningTickerConditions = useMemo( () => scheduledConditions.filter( (condition): condition is ScheduledConditionRecord => condition.kind === "condition" && condition.status === "running" ), [scheduledConditions] ); const workbenchAlerts = conditionAlerts; const drawing = useWorkbenchDrawing({ mapRef, mapReady, activeMode: activeDrawToolId, onModeChange: setActiveDrawToolId }); const measurement = useWorkbenchMeasurement({ mapRef, mapReady, mode: activeMeasureModeId, unit: activeMeasureUnitId }); const prefersReducedMotion = useReducedMotion(); const taskTickerAvailable = runningTickerConditions.length > 0; const shouldShowTaskTicker = !agent.mobileOpen && !conditionFeedExpanded && taskTickerVisible && taskTickerAvailable; const taskTickerEnterState = prefersReducedMotion ? { opacity: 1, scale: 1, y: 0 } : { opacity: 1, scale: 1, y: 0, transition: { type: "spring" as const, stiffness: 420, damping: 30, mass: 0.75 } }; const taskTickerExitState = prefersReducedMotion ? { opacity: 0, scale: 1, y: 0, transition: { duration: 0 } } : { opacity: 0, scale: 0.96, y: -8, transition: { duration: 0.18, ease: [0.5, 0, 0.2, 1] as const } }; useEffect(() => { if (!mapReady || !mapRef.current) { return; } applyBaseLayerVisibility(mapRef.current, activeBaseLayerId); }, [activeBaseLayerId, mapReady, mapRef]); const toggleToolbarTool = useCallback((toolId: ToolbarToolId) => { setActiveToolId((current) => { return current === toolId ? null : toolId; }); }, []); const handleConditionFocusRequestHandled = useCallback((requestId: number) => { setConditionFocusRequest((current) => (current?.requestId === requestId ? null : current)); }, []); const toolbarItems = useMemo( () => waterNetworkToolbarItems.map((item) => ({ ...item, active: activeToolId === item.id, disabled: !mapReady, onClick: () => toggleToolbarTool(item.id as ToolbarToolId) })), [activeToolId, mapReady, toggleToolbarTool] ); const layerControlItems = useMemo(() => createLayerControlItems(layerVisibility), [layerVisibility]); function handleToggleLayer(layerControlId: string, visible: boolean) { const map = mapRef.current; setLayerVisibility((current) => ({ ...current, [layerControlId]: visible })); if (layerControlId === "simulation") { setImpactVisible(visible); } if (!map || !mapReady) { return; } getWorkbenchLayerIds(map, layerControlId).forEach((layerId) => { if (map.getLayer(layerId)) { map.setLayoutProperty(layerId, "visibility", visible ? "visible" : "none"); } }); } async function handleExportView(preset: ExportViewPreset) { const map = mapRef.current; if (!map || !mapReady) { showMapNotice({ tone: "warning", message: "地图尚未就绪,无法导出视图。" }); return; } const targetLongEdge = getExportTargetLongEdge(preset); const exportLabel = getExportPresetLabel(preset); showMapNotice({ id: "map-view-export", tone: "loading", title: "正在导出", message: `正在生成${exportLabel}地图截图...`, duration: Infinity }); try { const result = await exportMapViewImage(map, { scale: preset === "current" ? 1 : undefined, targetLongEdge, selectedFeature: toMapFeatureReference(detailFeature) }); showMapNotice({ id: "map-view-export", tone: "success", message: `已导出 ${result.width} × ${result.height} PNG。` }); } catch { showMapNotice({ id: "map-view-export", tone: "error", title: "导出失败", message: "当前底图或瓦片可能限制了画布导出。" }); } } function handleRefreshTiles() { const map = mapRef.current; if (!map || !mapReady) { showMapNotice({ tone: "warning", message: "地图尚未就绪,无法刷新瓦片。" }); return; } map.triggerRepaint(); showMapNotice({ tone: "success", message: "已请求地图重新渲染,业务瓦片将在视图变化时刷新。" }); } function handleShowDataStatus() { if (sourceStatuses.length === 0) { showMapNotice({ tone: "info", message: "数据源状态正在初始化。" }); return; } showMapNotice({ tone: sourceStatuses.some((sourceStatus) => sourceStatus.status === "offline") ? "warning" : "info", title: "数据状态", message: sourceStatuses.map((sourceStatus) => `${sourceStatus.sourceName}:${sourceStatus.status}`).join(" · "), duration: 5000 }); } function handleShowShortcuts() { showMapNotice({ tone: "info", title: "快捷键", message: "双击完成线/面绘制;圆形标注先点圆心,再点半径;Esc 可作为后续快捷键扩展。", duration: 6000 }); } function handleSelectScenario(scenarioId: string) { const scenario = WORKBENCH_SCENARIOS.find((item) => item.id === scenarioId); setActiveScenarioId(scenarioId); showMapNotice({ tone: "success", title: "方案已切换", message: `当前方案:${scenario?.name ?? scenarioId}。地图模拟结果暂未自动切换。` }); } function handlePreviewScenario() { setImpactVisible(true); setLayerVisibility((current) => ({ ...current, simulation: true })); if (mapRef.current && mapReady) { WORKBENCH_LAYER_GROUPS.simulation.forEach((layerId) => { if (mapRef.current?.getLayer(layerId)) { mapRef.current.setLayoutProperty(layerId, "visibility", "visible"); } }); } showMapNotice({ tone: "info", title: "预览影响范围", message: "已打开模拟结果图层,可在右侧图层面板继续调整显示。" }); } function handleCompareScenario() { showMapNotice({ tone: "info", title: "方案比较", message: "候选方案比较将汇总影响范围、阀门操作与保供风险。" }); } function handleExportScenarioReport() { showMapNotice({ tone: "success", title: "场景报告", message: "已生成当前场景报告任务,可继续导出地图截图或配置。" }); } async function handleAnalyzeAlertsWithAgent() { if (workbenchAlerts.length === 0) { showMapNotice({ tone: "info", title: "当前无待复核工况", message: "当前没有需要汇总的工况提醒。" }); return; } if (agent.streaming) { showMapNotice({ tone: "warning", message: "Agent 正在处理上一条指令。" }); return; } agent.expandPanel(); try { await agent.submitPrompt( createAlertQueueConversationPrompt({ alerts: workbenchAlerts, conditions: scheduledConditions }) ); showMapNotice({ tone: "success", title: "已发起工况汇总", message: "已将待复核工况发送到 Agent。" }); } catch (error) { showMapNotice({ tone: "error", title: "工况汇总发起失败", message: error instanceof Error ? error.message : "无法将待复核工况发送到 Agent。" }); } } function handleSelectAlert(alert: WorkbenchAlert) { const conditionId = getScheduledConditionIdFromAlertId(alert.id); if (conditionId) { const condition = scheduledConditions.find((item) => item.id === conditionId); if (!condition) { showMapNotice({ tone: "warning", title: "工况任务未找到", message: `未找到提醒关联的工况任务:${conditionId}。` }); return; } setConditionFeedVisible(true); setActiveToolId(null); setConditionFocusRequest((current) => ({ conditionId, requestId: (current?.requestId ?? 0) + 1 })); return; } showMapNotice({ tone: "warning", title: alert.title, message: alert.description }); } async function handleAgentUiEnvelope(payload: UIEnvelopePayload, sessionId: string) { const { envelope } = payload; if (envelope.kind === "map_action") { await handleAgentMapAction(envelope.action, envelope.params, sessionId, envelope.fallbackText); return; } setAgentUiResults((current) => [ ...current.slice(-5), { id: payload.envelope_id || `agent-ui-${Date.now().toString(36)}`, sessionId, createdAt: payload.created_at, envelope } ]); showMapNotice({ tone: "info", title: envelope.kind === "chart" ? "Agent 图表已渲染" : "Agent 面板已渲染", message: envelope.fallbackText ?? (envelope.kind === "chart" ? "已在 Agent 面板显示可信图表。" : "已在 Agent 面板显示可信组件。") }); } async function handleFrontendAction(request: FrontendActionRequest, signal: AbortSignal) { const map = mapRef.current; if (request.name === "zoom_to_map") { if (!map || !mapReady) throw new Error("MAP_NOT_READY"); const x = Number(request.params.x); const y = Number(request.params.y); const center: [number, number] = request.params.source_crs === "EPSG:4326" ? [x, y] : [x * 180 / 20037508.34, Math.atan(Math.exp(y * Math.PI / 20037508.34)) * 360 / Math.PI - 90]; const zoom = Number(request.params.zoom ?? 18); map.easeTo({ center, zoom, padding: getResponsiveWorkbenchPadding(map, leftPanelOpen, rightPanelOpen), duration: Number(request.params.duration_ms ?? 500) }); return { center, zoom }; } if (request.name === "locate_features") { if (!map || !mapReady) throw new Error("MAP_NOT_READY"); const ids = request.params.ids as string[]; const featureType = String(request.params.feature_type); const response = await fetch("/api/agent-locate", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ ids, featureType }), signal }); const geojson = await response.json(); if (!response.ok) throw new Error(geojson.code ?? "WFS_FAILED"); const features = geojson.features as Array<{ id?: string | number; properties?: Record; geometry: { coordinates: unknown } }>; if (!features.length) throw new Error("FEATURE_NOT_FOUND"); if (map.getSource("agent-locate")) (map.getSource("agent-locate") as unknown as { setData: (data: unknown) => void }).setData(geojson); else { map.addSource("agent-locate", { type: "geojson", data: geojson }); map.addLayer({ id: "agent-locate-lines", type: "line", source: "agent-locate", filter: ["==", ["geometry-type"], "LineString"], paint: { "line-color": "#f97316", "line-width": 6 } }); map.addLayer({ id: "agent-locate-points", type: "circle", source: "agent-locate", filter: ["==", ["geometry-type"], "Point"], paint: { "circle-color": "#f97316", "circle-radius": 8, "circle-stroke-color": "#fff", "circle-stroke-width": 2 } }); } const coordinates: number[][] = []; const collect = (value: unknown) => { if (Array.isArray(value) && value.length >= 2 && typeof value[0] === "number" && typeof value[1] === "number") coordinates.push(value as number[]); else if (Array.isArray(value)) value.forEach(collect); }; features.forEach((feature) => collect(feature.geometry.coordinates)); const bounds: [number, number, number, number] = [Math.min(...coordinates.map((c) => c[0])), Math.min(...coordinates.map((c) => c[1])), Math.max(...coordinates.map((c) => c[0])), Math.max(...coordinates.map((c) => c[1]))]; map.fitBounds([[bounds[0], bounds[1]], [bounds[2], bounds[3]]], { padding: getResponsiveWorkbenchPadding(map, leftPanelOpen, rightPanelOpen), maxZoom: 18 }); const locatedIds = features.map((feature) => String(feature.id ?? feature.properties?.id ?? "")).filter(Boolean); return { locatedIds, missingIds: ids.filter((id) => !locatedIds.includes(id)), featureType, bounds }; } const history = request.name === "view_history"; const itemCount = history ? (request.params.feature_infos as unknown[]).length : Array.isArray(request.params.device_ids) ? request.params.device_ids.length : 1; setAgentUiResults((current) => [...current.slice(-5), { id: request.actionId, sessionId: request.sessionId, createdAt: Date.now(), envelope: { kind: "registered_component", schemaVersion: "agent-ui@1", component: history ? "HistoryPanel" : "ScadaPanel", surface: "side_panel", props: request.params } }]); return { component: history ? "HistoryPanel" : "ScadaPanel", rendered: true, itemCount }; } async function handleAgentMapAction(action: string, params: unknown, sessionId: string, fallbackText?: string) { const trustedAction = toTrustedMapAction(action, params, fallbackText); if (!trustedAction) { showMapNotice({ tone: "warning", title: "地图动作已降级", message: fallbackText ?? "Agent 地图动作参数未通过前端校验。" }); return; } if (trustedAction.type === "zoom_to_map") { const map = mapRef.current; if (!map || !mapReady) { showMapNotice({ tone: "warning", message: "地图尚未就绪,无法执行 Agent 缩放动作。" }); return; } map.easeTo({ center: trustedAction.center, zoom: trustedAction.zoom ?? Math.max(map.getZoom(), 14), padding: getResponsiveWorkbenchPadding(map, leftPanelOpen, rightPanelOpen), duration: 500 }); showMapNotice({ tone: "info", title: "Agent 地图定位", message: trustedAction.fallbackText ?? "已根据 Agent 建议缩放到目标位置。" }); return; } if (trustedAction.type === "locate_features") { fitNetworkBounds(); showMapNotice({ tone: "info", title: "Agent 资产定位", message: trustedAction.fallbackText ?? (trustedAction.featureIds.length > 0 ? `已接收 ${trustedAction.featureIds.length} 个目标资产,当前先回到全网视图。` : "已根据 Agent 建议回到全网视图。") }); return; } if (trustedAction.type === "apply_layer_style") { if (trustedAction.layerGroupId && typeof trustedAction.visible === "boolean") { handleToggleLayer(trustedAction.layerGroupId, trustedAction.visible); } showMapNotice({ tone: "info", title: "Agent 图层样式", message: trustedAction.fallbackText ?? "已接收 Agent 图层样式建议,当前仅执行受控图层显隐。" }); return; } try { await agent.resolveRenderRef(trustedAction.renderRef, sessionId); showMapNotice({ tone: "info", title: "Agent 节点渲染", message: trustedAction.fallbackText ?? "已验证 render_ref,节点分类渲染器将在后续接入。" }); } catch (error) { showMapNotice({ tone: "error", title: "render_ref 解析失败", message: error instanceof Error ? error.message : "无法读取 Agent 渲染结果。" }); } } function handleExportConfig() { const config = { exportedAt: new Date().toISOString(), activeBaseLayerId, layerVisibility, activeToolId, activeMeasureModeId, activeMeasureUnitId }; const blob = new Blob([JSON.stringify(config, null, 2)], { type: "application/json" }); const url = URL.createObjectURL(blob); const link = document.createElement("a"); link.href = url; link.download = "drainage-network-map-config.json"; document.body.appendChild(link); link.click(); link.remove(); URL.revokeObjectURL(url); showMapNotice({ tone: "success", message: "当前地图配置已导出。" }); } const toolbarPanelProps: ToolbarPanelProps = { activeToolId, activeMeasureModeId, activeMeasureUnitId, measuring: measurement.measuring, measureResult: measurement.result, measureEnabled: mapReady, hasMeasurePoints: measurement.hasPoints, activeDrawToolId, drawingEnabled: mapReady, drawingAnnotations: drawing.annotations, canUndoDrawing: drawing.canUndo, canRedoDrawing: drawing.canRedo, canDeleteSelectedDrawing: drawing.canDeleteSelected, hasDrawingFeatures: drawing.hasFeatures, layerItems: layerControlItems, legendItems: MAP_LEGEND_ITEMS, baseLayerOptions, activeBaseLayerId, onSelectMeasureMode: setActiveMeasureModeId, onSelectMeasureUnit: setActiveMeasureUnitId, onStartMeasure: measurement.start, onStopMeasure: measurement.stop, onClearMeasure: measurement.clear, onCopyMeasure: measurement.copyResult, onSelectDrawTool: setActiveDrawToolId, onUndoDrawing: drawing.undo, onRedoDrawing: drawing.redo, onDeleteSelectedDrawing: drawing.deleteSelected, onClearDrawing: drawing.clear, onExportAnnotations: drawing.exportGeoJSON, onSelectBaseLayer: setActiveBaseLayerId, onToggleLayer: handleToggleLayer, onExportView: handleExportView, onRefreshTiles: handleRefreshTiles, onShowDataStatus: handleShowDataStatus, onShowShortcuts: handleShowShortcuts, onExportConfig: handleExportConfig }; const agentPanelProps: Omit, "collapsing" | "onCollapse"> = { personaState: agent.personaState, sessionTitle: agent.sessionTitle, sessionHistory: agent.sessionHistory, sessionHistoryLoading: agent.sessionHistoryLoading, activeSessionId: agent.sessionId, statusLabel: agent.statusLabel, streaming: agent.streaming, messages: agent.messages, streamRenderState: agent.streamRenderState, modelOptions: agent.modelOptions, selectedModel: agent.selectedModel, uiResults: activeAgentUiResults, onRefreshHistory: agent.refreshSessionHistory, onStartNewSession: agent.startNewSession, onLoadHistorySession: agent.loadHistorySession, onRenameHistorySession: agent.renameHistorySession, onDeleteHistorySession: agent.deleteHistorySession, onSelectModel: agent.setSelectedModel, onSubmitPrompt: agent.submitPrompt, onStopPrompt: agent.stopPrompt, onReplyPermission: agent.replyPermission, onReplyQuestion: agent.replyQuestion, onRejectQuestion: agent.rejectQuestion }; return (
{ setConditionFeedVisible((current) => !current); setActiveToolId(null); }} onToggleTaskTicker={() => setTaskTickerVisible((current) => !current)} onPreviewScenario={handlePreviewScenario} onCompareScenario={handleCompareScenario} onExportScenarioReport={handleExportScenarioReport} onAnalyzeAlertsWithAgent={handleAnalyzeAlertsWithAgent} onShowDataStatus={handleShowDataStatus} onRefreshTiles={handleRefreshTiles} onShowShortcuts={handleShowShortcuts} onExportConfig={handleExportConfig} />
{agent.panelOpen ? ( ) : ( )}
{conditionFeedMounted ? (
) : null}
{agent.mobileOpen ? (
) : null}
{!agent.mobileOpen ? (
) : null}
{shouldShowTaskTicker ? ( ) : null}
{!agent.mobileOpen ? ( ) : null}
setDetailFeature(null)} /> {!mapReady && !mapError ? : null} {!hasMapboxToken ? ( ) : null} {sourceStatuses.filter(isNoticeSourceStatus).map((sourceStatus) => ( ))} {mapError ? : null}
); } function isNoticeSourceStatus(sourceStatus: { status: string; }): sourceStatus is { id: string; sourceName: string; status: MapSourceStatus; message: string } { return sourceStatus.status === "online" || sourceStatus.status === "degraded" || sourceStatus.status === "offline"; } function applyBaseLayerVisibility(map: MapLibreMap, activeBaseLayerId: string) { BASE_LAYER_IDS.forEach((layerId) => { if (map.getLayer(layerId)) { map.setLayoutProperty(layerId, "visibility", activeBaseLayerId === layerId ? "visible" : "none"); } }); } function getExportTargetLongEdge(preset: ExportViewPreset) { if (preset === "4k") { return 3840; } return undefined; } function getExportPresetLabel(preset: ExportViewPreset) { if (preset === "4k") { return " 4K "; } return "当前分辨率"; } function createScheduledConditionAlerts(conditions: ScheduledConditionItem[]): WorkbenchAlert[] { return conditions .filter(isAlertCondition) .sort((a, b) => getScheduledConditionTimestamp(b) - getScheduledConditionTimestamp(a)) .map((condition) => { return { id: `scheduled-condition-alert-${condition.id}`, title: `待复核:${condition.title}`, description: condition.detail ?? condition.summary, time: formatScheduledConditionTime(condition.scheduledAt) }; }); } function isAlertCondition(condition: ScheduledConditionItem) { return condition.kind === "condition" && condition.status === "error"; } function getScheduledConditionTimestamp(condition: ScheduledConditionItem) { const scheduledAt = Date.parse(condition.scheduledAt); return Number.isNaN(scheduledAt) ? condition.updatedAt : scheduledAt; } function formatScheduledConditionTime(value: string) { const match = value.match(/T(\d{2}:\d{2})/); return match?.[1] ?? value; } const SCHEDULED_CONDITION_ALERT_ID_PREFIX = "scheduled-condition-alert-"; function getScheduledConditionIdFromAlertId(alertId: string) { if (!alertId.startsWith(SCHEDULED_CONDITION_ALERT_ID_PREFIX)) { return null; } return alertId.slice(SCHEDULED_CONDITION_ALERT_ID_PREFIX.length); } function MobileAgentToggle({ open, personaState, statusLabel, streaming, onOpen, onClose }: { open: boolean; personaState: ComponentProps["state"]; statusLabel: string; streaming: boolean; onOpen: () => void; onClose: () => void; }) { if (open) { return (
); } return (
); }