feat(ui): complete drainage workbench style migration

This commit is contained in:
2026-09-16 15:51:42 +08:00
parent 0995172828
commit c0bccbbaf2
135 changed files with 4919 additions and 1794 deletions
+191 -313
View File
@@ -1,6 +1,3 @@
"use client";
import type { Map as MapLibreMap } from "maplibre-gl";
import {
useCallback,
useEffect,
@@ -8,39 +5,39 @@ import {
useRef,
useState,
type ComponentProps,
type CSSProperties,
type KeyboardEvent as ReactKeyboardEvent,
type PointerEvent as ReactPointerEvent
type CSSProperties
} from "react";
import { AnimatePresence, motion, useReducedMotion } from "motion/react";
import { CalendarClock } from "lucide-react";
import {
AgentCollapsedRail,
AgentCommandPanel,
AgentPersona,
toTrustedMapAction,
type FrontendActionRequest,
type UIEnvelopePayload,
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,
MapScaleLine,
MapSourceStatusNotice,
MapToolbar,
MapZoom,
showMapNotice,
type MapToolbarItem,
type MapSourceStatus
} from "@/features/map/core/components/notice";
import { showMapNotice } from "@/features/map/core/components/notice-actions";
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";
} from "@/features/map/core";
import { env } from "@/shared/config/env";
import { AgentTaskTicker } from "./components/agent-task-ticker";
import { MapDevPanel } from "./components/map-dev-panel";
import {
MobileWorkbenchSheet,
type MobileWorkbenchSheetSnap
} from "./components/mobile-workbench-sheet";
import { FeaturePopover } from "./components/feature-popover";
import { ScheduledConditionFeed } from "./components/scheduled-condition-feed";
import { WorkbenchAgentPanels } from "./components/workbench-agent-panels";
import {
ToolbarPanel,
type ExportViewPreset,
@@ -48,15 +45,12 @@ import {
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 { useWorkbenchMapController } from "./hooks/use-workbench-map-controller";
import { useWorkbenchRuntimeData } from "./hooks/use-workbench-runtime-data";
import { toMapFeatureReference } from "./hooks/use-map-interactions";
import {
useWorkbenchMeasurement,
@@ -65,20 +59,18 @@ import {
} from "./hooks/use-workbench-measurement";
import {
WORKBENCH_LAYOUT,
clampAgentPanelWidth,
getWorkbenchBasemapTone,
getWorkbenchLayoutCssVariables,
getWorkbenchViewportLayout
getWorkbenchLayoutCssVariables
} from "./layout/workbench-layout";
import { getResponsiveWorkbenchPadding } from "./map/camera";
import { exportMapViewImage } from "./map/export-view";
import { parseScadaAnalysisItems } from "./map/scada-analysis";
import {
BASE_LAYER_IDS,
BASE_LAYER_OPTIONS,
INITIAL_LAYER_VISIBILITY,
MAP_LEGEND_ITEMS,
WORKBENCH_LAYER_GROUPS,
applyBaseLayerVisibility,
createLayerControlItems,
getWorkbenchLayerIds
} from "./map/map-control-config";
@@ -89,53 +81,34 @@ import type {
ScheduledConditionRecord,
WorkbenchAlert
} from "./types";
import {
createScheduledConditionAlerts,
getScheduledConditionIdFromAlertId
} from "./utils/scheduled-condition-alerts";
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(env.DRAINAGE_MAPBOX_ACCESS_TOKEN);
const devPanelEnabled = env.DRAINAGE_ENABLE_DEV_PANEL;
const mapContainerRef = useRef<HTMLDivElement | null>(null);
const desktopAgentPanelRef = useRef<HTMLDivElement | null>(null);
const agentPanelResizeLeftRef = useRef(0);
const agentPanelResizeCleanupRef = useRef<(() => void) | null>(null);
const [detailFeature, setDetailFeature] = useState<DetailFeature | null>(null);
const [devPanelOpen, setDevPanelOpen] = useState(false);
const [impactVisible, setImpactVisible] = useState(false);
const [isLargeScreen, setIsLargeScreen] = useState(false);
const [viewportWidth, setViewportWidth] = useState<number>(WORKBENCH_LAYOUT.desktopMinWidth);
const [agentPanelWidth, setAgentPanelWidth] = useState<number | null>(null);
const [agentPanelResizing, setAgentPanelResizing] = useState(false);
const [agentPanelWidth, setAgentPanelWidth] = useState<number>(
WORKBENCH_LAYOUT.desktop.agentWidth
);
const [activeToolId, setActiveToolId] = useState<ToolbarToolId | null>(null);
const [conditionFeedVisible, setConditionFeedVisible] = useState(false);
const [conditionFeedMounted, setConditionFeedMounted] = useState(true);
const [conditionFeedExpanded, setConditionFeedExpanded] = useState(false);
const [mobileSheet, setMobileSheet] = useState<"agent" | "condition" | null>(null);
const [mobileSheetSnap, setMobileSheetSnap] = useState<MobileWorkbenchSheetSnap>("half");
const [agentCollapsedForCondition, setAgentCollapsedForCondition] = useState(false);
const prefersReducedMotion = useReducedMotion();
const [taskTickerVisible, setTaskTickerVisible] = useState(true);
const [selectedConditionId, setSelectedConditionId] = useState<string | null>(null);
const [conditionFocusRequest, setConditionFocusRequest] = useState<{
@@ -153,9 +126,8 @@ export function MapWorkbenchPage() {
const [layerVisibility, setLayerVisibility] =
useState<Record<string, boolean>>(INITIAL_LAYER_VISIBILITY);
const [agentUiResults, setAgentUiResults] = useState<AgentUiResult[]>([]);
const [scheduledConditions, setScheduledConditions] = useState<ScheduledConditionItem[]>([]);
const [scheduledConditionsLoading, setScheduledConditionsLoading] = useState(true);
const [headerDataTime, setHeaderDataTime] = useState(INITIAL_HEADER_DATA_TIME);
const { headerDataTime, scheduledConditions, scheduledConditionsLoading } =
useWorkbenchRuntimeData();
const baseLayerOptions = useMemo(
() =>
BASE_LAYER_OPTIONS.map((option) => ({
@@ -174,9 +146,9 @@ export function MapWorkbenchPage() {
const handleChange = () => {
setIsLargeScreen(mediaQuery.matches);
setViewportWidth(window.innerWidth);
setAgentPanelWidth((currentWidth) =>
currentWidth === null ? null : clampAgentPanelWidth(currentWidth, window.innerWidth)
);
if (mediaQuery.matches) {
setMobileSheet(null);
}
};
handleChange();
@@ -190,38 +162,6 @@ export function MapWorkbenchPage() {
};
}, []);
useEffect(() => () => agentPanelResizeCleanupRef.current?.(), []);
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(() => {
@@ -243,15 +183,72 @@ export function MapWorkbenchPage() {
});
function openAgentPanelForViewport() {
if (isLargeScreen) {
if (conditionFeedExpanded && viewportWidth < 1440) {
setConditionFeedExpanded(false);
setAgentCollapsedForCondition(false);
}
agent.expandPanel();
return;
}
agent.openMobilePanel();
setMobileSheet("agent");
setMobileSheetSnap("half");
}
const closeMobileSheet = useCallback(() => {
setMobileSheet(null);
}, []);
function handleConditionExpandedChange(nextExpanded: boolean) {
if (nextExpanded && isLargeScreen && viewportWidth < 1440) {
setAgentCollapsedForCondition(agent.panelOpen);
agent.collapsePanel();
} else if (!nextExpanded && agentCollapsedForCondition) {
setAgentCollapsedForCondition(false);
agent.expandPanel();
}
setConditionFeedExpanded(nextExpanded);
}
useEffect(() => {
if (!isLargeScreen || shouldShowConditionFeed || !conditionFeedExpanded) {
return;
}
setConditionFeedExpanded(false);
if (agentCollapsedForCondition) {
setAgentCollapsedForCondition(false);
agent.expandPanel();
}
}, [
agent,
agentCollapsedForCondition,
conditionFeedExpanded,
isLargeScreen,
shouldShowConditionFeed
]);
function toggleConditionFeedForViewport() {
setActiveToolId(null);
if (isLargeScreen) {
setConditionFeedVisible((current) => !current);
return;
}
if (mobileSheet === "condition") {
closeMobileSheet();
return;
}
setConditionFeedExpanded(false);
setMobileSheetSnap("half");
setMobileSheet("condition");
}
const leftPanelOpen = isLargeScreen && agent.panelOpen;
const rightPanelOpen = isLargeScreen && (devPanelOpen || shouldShowConditionFeed);
const rightPanelExpanded =
isLargeScreen && shouldShowConditionFeed && conditionFeedExpanded && !devPanelOpen;
const { mapRef, mapReady, mapError, sourceStatuses, fitNetworkBounds } = useWorkbenchMap({
containerRef: mapContainerRef,
@@ -265,7 +262,9 @@ export function MapWorkbenchPage() {
mapRef,
mapReady,
leftPanelOpen,
rightPanelOpen
rightPanelOpen,
conditionPanelExpanded: rightPanelExpanded,
agentPanelWidth
});
const activeAgentUiResults = useMemo(
@@ -299,10 +298,9 @@ export function MapWorkbenchPage() {
mode: activeMeasureModeId,
unit: activeMeasureUnitId
});
const prefersReducedMotion = useReducedMotion();
const taskTickerAvailable = runningTickerConditions.length > 0;
const shouldShowTaskTicker =
!agent.mobileOpen && !conditionFeedExpanded && taskTickerVisible && taskTickerAvailable;
mobileSheet === null && !conditionFeedExpanded && taskTickerVisible && taskTickerAvailable;
const taskTickerEnterState = prefersReducedMotion
? {
opacity: 1,
@@ -565,6 +563,10 @@ export function MapWorkbenchPage() {
setConditionFeedVisible(true);
setActiveToolId(null);
if (!isLargeScreen) {
setMobileSheet("condition");
setMobileSheetSnap("half");
}
setConditionFocusRequest((current) => ({
conditionId,
requestId: (current?.requestId ?? 0) + 1
@@ -808,6 +810,7 @@ export function MapWorkbenchPage() {
sessionHistoryLoading: agent.sessionHistoryLoading,
activeSessionId: agent.sessionId,
statusLabel: agent.statusLabel,
connectionStatus: agent.connectionStatus,
streaming: agent.streaming,
messages: agent.messages,
streamRenderState: agent.streamRenderState,
@@ -829,74 +832,18 @@ export function MapWorkbenchPage() {
onRejectQuestion: agent.rejectQuestion
};
const resizeAgentPanel = useCallback((clientX: number) => {
setAgentPanelWidth(
clampAgentPanelWidth(clientX - agentPanelResizeLeftRef.current, window.innerWidth)
);
}, []);
const handleAgentPanelResizeStart = useCallback(
(event: ReactPointerEvent<HTMLDivElement>) => {
if (event.button !== 0 || !desktopAgentPanelRef.current) {
return;
}
event.preventDefault();
agentPanelResizeLeftRef.current = desktopAgentPanelRef.current.getBoundingClientRect().left;
setAgentPanelResizing(true);
resizeAgentPanel(event.clientX);
agentPanelResizeCleanupRef.current?.();
const handlePointerMove = (pointerEvent: PointerEvent) => {
pointerEvent.preventDefault();
resizeAgentPanel(pointerEvent.clientX);
};
const stopResizing = () => {
agentPanelResizeCleanupRef.current?.();
setAgentPanelResizing(false);
};
const cleanup = () => {
window.removeEventListener("pointermove", handlePointerMove);
window.removeEventListener("pointerup", stopResizing);
window.removeEventListener("pointercancel", stopResizing);
agentPanelResizeCleanupRef.current = null;
};
agentPanelResizeCleanupRef.current = cleanup;
window.addEventListener("pointermove", handlePointerMove);
window.addEventListener("pointerup", stopResizing);
window.addEventListener("pointercancel", stopResizing);
},
[resizeAgentPanel]
);
const handleAgentPanelResizeKeyDown = useCallback((event: ReactKeyboardEvent<HTMLDivElement>) => {
const currentWidth =
desktopAgentPanelRef.current?.getBoundingClientRect().width ??
WORKBENCH_LAYOUT.desktop.agentWidth;
let nextWidth = currentWidth;
if (event.key === "ArrowLeft") {
nextWidth -= 16;
} else if (event.key === "ArrowRight") {
nextWidth += 16;
} else if (event.key === "Home") {
nextWidth = getWorkbenchViewportLayout(window.innerWidth).agentWidth;
} else if (event.key === "End") {
nextWidth = window.innerWidth * WORKBENCH_LAYOUT.maxAgentViewportRatio;
} else {
return;
}
event.preventDefault();
setAgentPanelWidth(clampAgentPanelWidth(nextWidth, window.innerWidth));
}, []);
return (
<main
className="relative h-[100dvh] min-h-[640px] overflow-hidden bg-slate-100 text-slate-900 tabular-nums"
data-basemap-tone={basemapTone}
style={WORKBENCH_LAYOUT_CSS_VARIABLES as CSSProperties}
style={
{
...WORKBENCH_LAYOUT_CSS_VARIABLES,
"--workbench-agent-current-width": `${
leftPanelOpen ? agentPanelWidth : WORKBENCH_LAYOUT.collapsedAgentWidth
}px`
} as CSSProperties
}
>
<div
ref={mapContainerRef}
@@ -907,22 +854,19 @@ export function MapWorkbenchPage() {
<WorkbenchTopBar
dataTime={headerDataTime}
modelName="DrainFlow 1.0.0"
modelName="AquaDispatch v1.0"
scenarios={WORKBENCH_SCENARIOS}
activeScenarioId={activeScenarioId}
alerts={workbenchAlerts}
user={WORKBENCH_USER}
conditionFeedVisible={shouldShowConditionFeed}
conditionFeedVisible={isLargeScreen ? shouldShowConditionFeed : mobileSheet === "condition"}
taskTickerAvailable={taskTickerAvailable}
taskTickerVisible={taskTickerVisible}
devPanelEnabled={devPanelEnabled}
devPanelOpen={devPanelOpen}
onSelectScenario={handleSelectScenario}
onSelectAlert={handleSelectAlert}
onToggleConditionFeed={() => {
setConditionFeedVisible((current) => !current);
setActiveToolId(null);
}}
onToggleConditionFeed={toggleConditionFeedForViewport}
onToggleTaskTicker={() => setTaskTickerVisible((current) => !current)}
onToggleDevPanel={() => setDevPanelOpen((current) => !current)}
onPreviewScenario={handlePreviewScenario}
@@ -935,50 +879,21 @@ export function MapWorkbenchPage() {
onExportConfig={handleExportConfig}
/>
<div
ref={desktopAgentPanelRef}
className="pointer-events-none absolute bottom-4 left-3 top-24 z-20 hidden w-[var(--workbench-agent-width)] max-w-[50vw] 2xl:w-[var(--workbench-agent-width-wide)] lg:block"
style={agentPanelWidth === null ? undefined : { width: agentPanelWidth }}
>
{agent.panelOpen ? (
<>
<AgentCommandPanel
{...agentPanelProps}
collapsing={agent.panelCollapsing}
onCollapse={agent.collapsePanel}
/>
<div
aria-label="调整 Agent 面板宽度"
aria-orientation="vertical"
aria-valuemax={Math.round(viewportWidth * WORKBENCH_LAYOUT.maxAgentViewportRatio)}
aria-valuemin={getWorkbenchViewportLayout(viewportWidth).agentWidth}
aria-valuenow={Math.round(
agentPanelWidth ?? getWorkbenchViewportLayout(viewportWidth).agentWidth
)}
className={cn(
"group pointer-events-auto absolute -right-3 top-4 bottom-4 z-10 flex w-6 cursor-ew-resize touch-none items-center justify-center outline-hidden",
agentPanelResizing && "[&>span]:bg-blue-500 [&>span]:opacity-100"
)}
role="separator"
tabIndex={0}
title="拖拽调整 Agent 面板宽度"
onKeyDown={handleAgentPanelResizeKeyDown}
onPointerDown={handleAgentPanelResizeStart}
>
<span className="h-14 w-1 rounded-full bg-slate-500/60 opacity-50 shadow-xs transition-[height,background-color,opacity] group-hover:h-20 group-hover:bg-blue-500 group-hover:opacity-100 group-focus-visible:h-20 group-focus-visible:bg-blue-500 group-focus-visible:opacity-100" />
</div>
</>
) : (
<AgentCollapsedRail
personaState={agent.personaState}
statusLabel={agent.statusLabel}
onExpand={agent.expandPanel}
/>
)}
</div>
<WorkbenchAgentPanels
panelProps={agentPanelProps}
panelOpen={agent.panelOpen}
panelCollapsing={agent.panelCollapsing}
conditionExpanded={shouldShowConditionFeed && conditionFeedExpanded}
personaState={agent.personaState}
statusLabel={agent.statusLabel}
viewportWidth={viewportWidth}
onCollapsePanel={agent.collapsePanel}
onExpandPanel={agent.expandPanel}
onWidthCommit={setAgentPanelWidth}
/>
{!devPanelOpen ? (
<div className="absolute right-2 top-24 z-20">
<div className="absolute right-2 top-24 z-20 hidden lg:block">
<MapToolbar items={toolbarItems} className="hidden lg:block" />
</div>
) : null}
@@ -986,13 +901,14 @@ export function MapWorkbenchPage() {
{conditionFeedMounted && !devPanelOpen ? (
<div className="absolute right-16 top-24 z-20 hidden lg:block 2xl:[--workbench-condition-width:var(--workbench-condition-width-wide)]">
<ScheduledConditionFeed
presentation="desktop-floating"
conditions={scheduledConditions}
expanded={conditionFeedExpanded}
focusRequest={conditionFocusRequest}
loading={scheduledConditionsLoading}
visible={shouldShowConditionFeed}
selectedConditionId={selectedConditionId}
onExpandedChange={setConditionFeedExpanded}
onExpandedChange={handleConditionExpandedChange}
onFocusRequestHandled={handleConditionFocusRequestHandled}
onSelectedConditionChange={setSelectedConditionId}
onExpandAgent={openAgentPanelForViewport}
@@ -1017,24 +933,8 @@ export function MapWorkbenchPage() {
/>
) : null}
<div className="absolute bottom-20 left-3 right-3 z-30 lg:hidden">
{agent.mobileOpen ? (
<div className="h-[calc(100dvh-8.5rem)] min-h-[420px]">
<AgentCommandPanel
{...agentPanelProps}
collapsing={agent.mobilePanelCollapsing}
onCollapse={agent.closeMobilePanel}
/>
</div>
) : null}
</div>
{!agent.mobileOpen ? (
<MobileAgentToggle personaState={agent.personaState} onOpen={agent.openMobilePanel} />
) : null}
{!agent.mobileOpen ? (
<div className="absolute bottom-20 left-3 right-3 z-30 lg:hidden">
{mobileSheet === null ? (
<div className="absolute bottom-16 left-3 right-3 z-30 lg:hidden">
<ToolbarPanel
{...toolbarPanelProps}
panelWidthClassName="w-full max-h-[52dvh] overflow-y-auto"
@@ -1072,16 +972,70 @@ export function MapWorkbenchPage() {
) : null}
</AnimatePresence>
<div className="absolute bottom-3 left-3 right-3 z-20 md:hidden">
{!agent.mobileOpen ? (
<MapToolbar
items={toolbarItems}
orientation="horizontal"
className="w-full justify-center"
/>
<div className="absolute bottom-3 left-3 right-3 z-30 lg:hidden">
{mobileSheet === null ? (
<div className="flex items-center justify-center gap-2">
<button
type="button"
aria-label="打开 Agent 面板"
title="打开 Agent 面板"
onClick={openAgentPanelForViewport}
className="acrylic-control grid h-11 w-11 shrink-0 place-items-center rounded-xl border text-violet-700"
>
<AgentPersona className="pointer-events-none h-8 w-8" state={agent.personaState} />
</button>
<MapToolbar
items={toolbarItems}
orientation="horizontal"
className="h-11 justify-center"
/>
<button
type="button"
aria-label="打开工况任务"
title="打开工况任务"
onClick={toggleConditionFeedForViewport}
className="acrylic-control grid h-11 w-11 shrink-0 place-items-center rounded-xl border text-blue-700"
>
<CalendarClock size={20} aria-hidden="true" />
</button>
</div>
) : null}
</div>
{mobileSheet ? (
<MobileWorkbenchSheet
label={mobileSheet === "agent" ? "Agent 工作台抽屉" : "工况任务抽屉"}
snap={mobileSheetSnap}
onSnapChange={setMobileSheetSnap}
onClose={closeMobileSheet}
>
{mobileSheet === "agent" ? (
<AgentCommandPanel
{...agentPanelProps}
presentation="mobile-sheet"
onCollapse={closeMobileSheet}
/>
) : (
<ScheduledConditionFeed
presentation="mobile-sheet"
conditions={scheduledConditions}
expanded={conditionFeedExpanded}
focusRequest={conditionFocusRequest}
loading={scheduledConditionsLoading}
visible
selectedConditionId={selectedConditionId}
onExpandedChange={handleConditionExpandedChange}
onFocusRequestHandled={handleConditionFocusRequestHandled}
onSelectedConditionChange={setSelectedConditionId}
onExpandAgent={openAgentPanelForViewport}
onClose={closeMobileSheet}
onLoadHistorySession={handleLoadAgentHistorySession}
onSubmitPrompt={agent.submitPrompt}
/>
)}
</MobileWorkbenchSheet>
) : null}
<FeaturePopover feature={detailFeature} onClose={() => setDetailFeature(null)} />
{!mapReady && !mapError ? (
@@ -1118,18 +1072,6 @@ function isNoticeSourceStatus(sourceStatus: {
);
}
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;
@@ -1145,67 +1087,3 @@ function getExportPresetLabel(preset: ExportViewPreset) {
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({
personaState,
onOpen
}: {
personaState: ComponentProps<typeof AgentPersona>["state"];
onOpen: () => void;
}) {
return (
<div className="absolute bottom-20 left-3 z-30 lg:hidden">
<button
type="button"
aria-label="打开 Agent 面板"
title="打开 Agent 面板"
onClick={onOpen}
className={cn(
"grid h-12 w-12 place-items-center text-violet-700",
MAP_MAJOR_PANEL_RADIUS_CLASS_NAME,
MAP_CONTROL_SURFACE_CLASS_NAME
)}
>
<AgentPersona className="pointer-events-none h-10 w-10" state={personaState} />
</button>
</div>
);
}