feat(workbench): expand SCADA monitoring and investigation presentation

This commit is contained in:
2026-11-07 19:50:14 +08:00
parent 0801e161ec
commit ba71e2d529
133 changed files with 6754 additions and 1438 deletions
+56 -9
View File
@@ -116,6 +116,9 @@ import type {
} from "./types";
import { getScheduledConditionIdFromAlertId } from "./utils/scheduled-condition-alerts";
import { getOperationalNotificationSeverity } from "./utils/operational-notification-routing";
import { useScadaAlertNotices } from "./hooks/use-scada-alert-notices";
import { useDismissedWorkbenchAlerts } from "./hooks/use-dismissed-workbench-alerts";
import { ScadaAlertAtmosphere, type ScadaAlertIntensity } from "./components/scada-alert-atmosphere";
import { createAlertQueueConversationPrompt } from "./utils/scheduled-condition-prompts";
const WORKBENCH_LAYOUT_CSS_VARIABLES = getWorkbenchLayoutCssVariables();
@@ -158,6 +161,7 @@ function MapWorkbenchContent({
const [taskTickerVisible, setTaskTickerVisible] = useState(true);
const [analysisResultsPanelOpen, setAnalysisResultsPanelOpen] = useState(false);
const [peripheralSamplePanelOpen, setPeripheralSamplePanelOpen] = useState(false);
const [scadaAlertIntensity, setScadaAlertIntensity] = useState<ScadaAlertIntensity>("standard");
const [selectedConditionId, setSelectedConditionId] = useState<string | null>(null);
const [conditionFocusRequest, setConditionFocusRequest] = useState<{
conditionId: string;
@@ -225,11 +229,12 @@ function MapWorkbenchContent({
|| condition.operationalTask?.task_type === "external_work_order"
)
], [operationalTasks.conditions, runtimeScheduledConditions]);
const workbenchAlerts = useMemo<WorkbenchAlert[]>(() => {
const allWorkbenchAlerts = useMemo<WorkbenchAlert[]>(() => {
const taskNotifications = operationalTasks.notifications.flatMap((notification) => {
const severity = getOperationalNotificationSeverity(notification.kind);
return severity ? [{
id: `operational-task-notification:${notification.notification_id}`,
notificationVersion: notification.version,
conditionId: operationalTaskConditionId(notification.task_id),
title: notification.title,
description: notification.description,
@@ -240,6 +245,20 @@ function MapWorkbenchContent({
});
return [...taskNotifications, ...runtimeWorkbenchAlerts];
}, [operationalTasks.notifications, runtimeWorkbenchAlerts]);
const { alerts: workbenchAlerts, dismissAlert } = useDismissedWorkbenchAlerts(
allWorkbenchAlerts, projectPresentation.projectId, projectPresentation.user.name
);
const handleRemoveAlert = (alert: WorkbenchAlert) => {
if (!dismissAlert(alert)) {
showMapNotice({ tone: "warning", title: "通知未移除", message: "无法保存移除记录,请确认项目已加载且浏览器允许本地存储后重试。" });
}
};
const { atmosphere: scadaAlertAtmosphere, previewAtmosphere, endAtmospherePreview, dismissAtmosphere } = useScadaAlertNotices(
projectPresentation.projectId,
workbenchSync.snapshot?.runtime.alertAvailability === "ready" ? runtimeWorkbenchAlerts : null,
handleSelectAlert,
operationalTasks.loading ? workbenchSync.snapshot?.tasks ?? null : operationalTasks.tasks
);
const collapseAgentPanel = agent.collapsePanel;
const expandAgentPanel = agent.expandPanel;
const clearActiveTool = useCallback(() => {
@@ -550,6 +569,7 @@ function MapWorkbenchContent({
const taskTickerAvailable = runningTickerConditions.length > 0;
const shouldShowTaskTicker =
!readingWorkspace?.active &&
(isLargeScreen || activeToolId !== "geocoding") &&
!artifactActive &&
mobileSheet === null &&
!conditionFeedExpanded &&
@@ -853,6 +873,7 @@ function MapWorkbenchContent({
return;
}
setPeripheralSamplePanelOpen(false);
openConditionWorkspaceForViewport();
setConditionFocusRequest((current) => ({
conditionId,
@@ -1064,6 +1085,7 @@ function MapWorkbenchContent({
const toolbarPanelProps: ToolbarPanelProps = {
geocoding,
featureLocation: { commands: mapController, controllerState: mapControllerState, detailFeature, enabled: mapReady },
activeToolId,
activeMeasureModeId,
activeMeasureUnitId,
@@ -1257,9 +1279,12 @@ function MapWorkbenchContent({
onRejectQuestion: agent.sessionPurpose === "task_execution" ? undefined : agent.rejectQuestion
};
const conditionFeedSuspended = devPanelOpen || peripheralSamplePanelOpen || Boolean(readingWorkspace?.active);
const mobileSheetSuspended = Boolean(readingWorkspace?.active) || (mobileSheet === "condition" && peripheralSamplePanelOpen);
return (
<main
className="relative h-[100dvh] min-h-[640px] overflow-hidden bg-slate-100 text-slate-900 tabular-nums"
className="relative isolate h-[100dvh] min-h-[640px] overflow-hidden bg-slate-100 text-slate-900 tabular-nums"
data-basemap-tone={basemapTone}
style={
{
@@ -1269,6 +1294,7 @@ function MapWorkbenchContent({
} as CSSProperties
}
>
<ScadaAlertAtmosphere atmosphere={scadaAlertAtmosphere} onDismiss={dismissAtmosphere} onOpen={handleSelectAlert} intensity={scadaAlertIntensity} />
<WorkbenchTopBar
dataTime={displayedDataTime}
modelName={
@@ -1281,7 +1307,7 @@ function MapWorkbenchContent({
alertDataSource={alertDataSource}
user={projectPresentation.user}
projectSelection={projectSelection}
conditionFeedVisible={isLargeScreen ? shouldShowConditionFeed : mobileSheet === "condition"}
conditionFeedVisible={!conditionFeedSuspended && (isLargeScreen ? shouldShowConditionFeed : mobileSheet === "condition")}
timelineVisible={timelineVisible}
taskTickerAvailable={taskTickerAvailable}
taskTickerVisible={taskTickerVisible}
@@ -1290,6 +1316,7 @@ function MapWorkbenchContent({
devPanelEnabled={devPanelEnabled}
devPanelOpen={devPanelOpen}
onSelectAlert={handleSelectAlert}
onRemoveAlert={handleRemoveAlert}
onToggleConditionFeed={toggleConditionWorkspaceForViewport}
onToggleTimeline={toggleTimelineVisibility}
onToggleTaskTicker={() => setTaskTickerVisible((current) => !current)}
@@ -1336,7 +1363,17 @@ function MapWorkbenchContent({
open={peripheralSamplePanelOpen}
onClose={() => setPeripheralSamplePanelOpen(false)}
getAccessToken={getAccessToken}
mapReady={mapReady}
mapRef={mapRef}
onAgentTrigger={handlePeripheralWorkOrderAgentTrigger}
onOpenInvestigation={(taskId) => {
setPeripheralSamplePanelOpen(false);
openConditionWorkspaceForViewport();
const conditionId = operationalTaskConditionId(taskId);
setSelectedConditionId(conditionId);
setConditionFocusRequest((current) => ({ conditionId, requestId: (current?.requestId ?? 0) + 1 }));
operationalTasks.refresh();
}}
/>
<WorkbenchAgentPanels
@@ -1470,7 +1507,7 @@ function MapWorkbenchContent({
</WorkbenchAgentPanels>
{conditionFeedMounted ? (
<div className="absolute bottom-[calc(var(--workbench-timeline-height)+1.25rem)] right-16 top-24 z-50 hidden lg:block 2xl:[--workbench-condition-width:var(--workbench-condition-width-wide)]" style={devPanelOpen || readingWorkspace?.active ? { display: "none" } : undefined}>
<div className="pointer-events-none absolute bottom-[calc(var(--workbench-timeline-height)+1.25rem)] right-16 top-24 z-50 hidden lg:block 2xl:[--workbench-condition-width:var(--workbench-condition-width-wide)]" style={conditionFeedSuspended ? { display: "none" } : undefined}>
<ScheduledConditionFeed
presentation="desktop-floating"
conditions={scheduledConditions}
@@ -1516,10 +1553,15 @@ function MapWorkbenchContent({
flowDataSource={flowDataSource}
investigationFollow={investigationFollow}
onInvestigationFollowChange={setInvestigationFollow}
onPreviewScadaAlert={previewAtmosphere}
onEndScadaAlertPreview={endAtmospherePreview}
scadaAlertPreviewActive={scadaAlertAtmosphere?.preview === true}
scadaAlertIntensity={scadaAlertIntensity}
onScadaAlertIntensityChange={setScadaAlertIntensity}
investigationTools={{
task: selectedInvestigationTask, mapReady,
charts: previewPresentation.charts.length, overlay: previewPresentation.overlay?.title ?? null,
activity: [...presentation.activity, ...previewPresentation.activity], onSample: previewPresentation.createSample,
onSample: previewPresentation.createSample,
onPreview: previewPresentation.runPreview, onClear: previewPresentation.clearPreview
}}
onClose={() => setDevPanelOpen(false)}
@@ -1529,11 +1571,16 @@ function MapWorkbenchContent({
{mobileSheet === null && !readingWorkspace?.active && artifactWorkspace.surfaceMode !== "focus" ? (
<div
className="absolute left-3 right-3 z-30 transition-[bottom] duration-200 ease-out motion-reduce:transition-none lg:hidden"
style={{ bottom: mobileOverlayLayout.mapControlBottom }}
style={{
bottom: mobileOverlayLayout.mapControlBottom,
"--mobile-tool-available-height": `calc(100dvh - ${mobileOverlayLayout.mapControlBottom}px - 4.5rem)`
} as CSSProperties}
>
<ToolbarPanel
{...toolbarPanelProps}
panelWidthClassName="w-full max-h-[52dvh] overflow-y-auto"
panelWidthClassName={activeToolId === "geocoding"
? "w-full max-h-[var(--mobile-tool-available-height)]"
: "w-full max-h-[52dvh] overflow-y-auto"}
/>
</div>
) : null}
@@ -1602,8 +1649,8 @@ function MapWorkbenchContent({
{mobileSheet ? (
<MobileWorkbenchSheet
className={readingWorkspace?.active ? "hidden" : undefined}
suspended={Boolean(readingWorkspace?.active)}
className={mobileSheetSuspended ? "hidden" : undefined}
suspended={mobileSheetSuspended}
label={
mobileSheet === "agent"
? "Agent 工作台抽屉"