feat: add project-aware agent workspace
This commit is contained in:
@@ -1,3 +1,4 @@
|
||||
import { useReadingWorkspace } from "@/shared/ui/reading-workspace-context";
|
||||
import {
|
||||
useCallback,
|
||||
useEffect,
|
||||
@@ -10,6 +11,7 @@ import {
|
||||
} from "react";
|
||||
import { AnimatePresence, motion, useReducedMotion } from "motion/react";
|
||||
import { CalendarClock, Clock3 } from "lucide-react";
|
||||
import { ReadingWorkspaceProvider } from "@/shared/ui/reading-workspace";
|
||||
import {
|
||||
AgentCommandPanel,
|
||||
AgentPersona,
|
||||
@@ -18,7 +20,8 @@ import {
|
||||
type FrontendActionRequest,
|
||||
type UIEnvelopePayload,
|
||||
type AgentUiResult,
|
||||
type AgentWorkspaceContextItem
|
||||
type AgentWorkspaceContextItem,
|
||||
type AnswerUiSelection
|
||||
} from "@/features/agent";
|
||||
import {
|
||||
MapErrorNotice,
|
||||
@@ -33,11 +36,13 @@ import {
|
||||
} from "@/features/map/core";
|
||||
import { env } from "@/shared/config/env";
|
||||
import { waitForAgentPresentation } from "./map/agent-presentation";
|
||||
import type { AccessTokenProvider } from "@/shared/auth/keycloak-auth";
|
||||
import { getWorkbenchProject, type WorkbenchAccess } from "./api/request-context";
|
||||
import type { WorkbenchProjectSelection } from "./components/workbench-top-bar-menus";
|
||||
import { AgentTaskTicker } from "./components/agent-task-ticker";
|
||||
import { AnalysisResultsPanel } from "./components/analysis-results-panel";
|
||||
import { PeripheralSamplePanel } from "./peripheral-sample/peripheral-sample-panel";
|
||||
import type { PeripheralWorkOrder } from "./peripheral-sample/client";
|
||||
import type { PeripheralTestDataRecord, PeripheralWorkOrder } from "./peripheral-sample/client";
|
||||
import { buildScadaAnomalyInvestigation } from "./peripheral-sample/scada-anomaly-agent-trigger";
|
||||
import { MapDevPanel } from "./components/map-dev-panel";
|
||||
import { InvestigationPresentationPanel } from "./components/investigation-presentation-panel";
|
||||
import { MobileWorkbenchSheet } from "./components/mobile-workbench-sheet";
|
||||
@@ -125,15 +130,22 @@ const ALERT_TIME_FORMATTER = new Intl.DateTimeFormat("zh-CN", {
|
||||
hour12: false
|
||||
});
|
||||
|
||||
export function MapWorkbenchPage({
|
||||
user = WORKBENCH_USER,
|
||||
onLogout,
|
||||
getAccessToken
|
||||
}: {
|
||||
type MapWorkbenchPageProps = {
|
||||
user?: WorkbenchUser;
|
||||
onLogout?: () => Promise<void>;
|
||||
getAccessToken?: AccessTokenProvider;
|
||||
}) {
|
||||
getAccessToken?: WorkbenchAccess;
|
||||
projectSelection?: WorkbenchProjectSelection;
|
||||
};
|
||||
export function MapWorkbenchPage(props: MapWorkbenchPageProps) {
|
||||
return <ReadingWorkspaceProvider><MapWorkbenchContent {...props} /></ReadingWorkspaceProvider>;
|
||||
}
|
||||
function MapWorkbenchContent({
|
||||
user = WORKBENCH_USER,
|
||||
onLogout,
|
||||
getAccessToken,
|
||||
projectSelection
|
||||
}: MapWorkbenchPageProps) {
|
||||
const readingWorkspace = useReadingWorkspace();
|
||||
const hasMapboxToken = Boolean(env.TJWATER_MAPBOX_ACCESS_TOKEN);
|
||||
const hasTiandituToken = Boolean(env.TJWATER_TIANDITU_TOKEN);
|
||||
const hasOnlineBasemap = hasMapboxToken || hasTiandituToken;
|
||||
@@ -145,6 +157,7 @@ export function MapWorkbenchPage({
|
||||
const [investigationFollow, setInvestigationFollow] = useState(true);
|
||||
const presentationLayerChangesRef = useRef(new Map<string, boolean>());
|
||||
const [impactVisible, setImpactVisible] = useState(false);
|
||||
const [geocodingPickActive, setGeocodingPickActive] = useState(false);
|
||||
const [activeToolId, setActiveToolId] = useState<ToolbarToolId | null>(null);
|
||||
const prefersReducedMotion = useReducedMotion();
|
||||
const [taskTickerVisible, setTaskTickerVisible] = useState(true);
|
||||
@@ -272,7 +285,8 @@ export function MapWorkbenchPage({
|
||||
const activeSpatialOverlay = artifactWorkspace.activeArtifact?.spatialOverlay ?? null;
|
||||
const activeArtifactHasSpatialView = Boolean(activeSpatialOverlay?.features.length);
|
||||
const allowDemoSimulation =
|
||||
artifactWorkspace.activeArtifact === null || artifactWorkspace.activeArtifact.origin === "demo";
|
||||
(!getWorkbenchProject(getAccessToken) || getWorkbenchProject(getAccessToken)?.gs_workspace === "tjwater_next")
|
||||
&& (artifactWorkspace.activeArtifact === null || artifactWorkspace.activeArtifact.origin === "demo");
|
||||
const latestAnalysisAvailable = Boolean(
|
||||
latestAnalysisArtifact?.blocks.some(
|
||||
(block) =>
|
||||
@@ -395,6 +409,44 @@ export function MapWorkbenchPage({
|
||||
}
|
||||
}
|
||||
|
||||
async function handleScadaAnomalyAgentTrigger(record: PeripheralTestDataRecord) {
|
||||
showMapNotice({
|
||||
id: "scada-anomaly-agent-trigger",
|
||||
tone: "loading",
|
||||
title: "SCADA 模拟异常已接收",
|
||||
message: "正在建立结构化异常调查任务。",
|
||||
duration: Infinity
|
||||
});
|
||||
|
||||
try {
|
||||
const task = await operationalTasks.createInvestigation(
|
||||
buildScadaAnomalyInvestigation(record)
|
||||
);
|
||||
setPeripheralSamplePanelOpen(false);
|
||||
openConditionWorkspaceForViewport();
|
||||
const conditionId = operationalTaskConditionId(task.task_id);
|
||||
setSelectedConditionId(conditionId);
|
||||
setConditionFocusRequest((current) => ({
|
||||
conditionId,
|
||||
requestId: (current?.requestId ?? 0) + 1
|
||||
}));
|
||||
showMapNotice({
|
||||
id: "scada-anomaly-agent-trigger",
|
||||
tone: "success",
|
||||
title: "已建立 SCADA 异常调查",
|
||||
message: `关联模拟读数 ${record.record_id.slice(0, 8)},Agent 将按任务上下文核查。`
|
||||
});
|
||||
} catch (error) {
|
||||
showMapNotice({
|
||||
id: "scada-anomaly-agent-trigger",
|
||||
tone: "error",
|
||||
title: "异常调查任务创建失败",
|
||||
message: error instanceof Error ? error.message : "Agent 事件入口当前不可用。"
|
||||
});
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
function openConditionWorkspaceForViewport() {
|
||||
openConditionFeedForViewport();
|
||||
}
|
||||
@@ -422,6 +474,7 @@ export function MapWorkbenchPage({
|
||||
scadaObservedAt,
|
||||
refreshGeoServerLayers
|
||||
} = useWorkbenchMap({
|
||||
featureInteractionsEnabled: !geocodingPickActive,
|
||||
containerRef: mapContainerRef,
|
||||
impactVisible,
|
||||
layerVisibility,
|
||||
@@ -431,6 +484,7 @@ export function MapWorkbenchPage({
|
||||
simulationOverlay: activeSpatialOverlay,
|
||||
allowDemoSimulation,
|
||||
networkView: projectPresentation.networkView,
|
||||
geoserverWorkspace: getWorkbenchProject(getAccessToken)?.gs_workspace,
|
||||
getAccessToken
|
||||
});
|
||||
const displayedDataTime =
|
||||
@@ -439,6 +493,7 @@ export function MapWorkbenchPage({
|
||||
: headerDataTime;
|
||||
const { controller: mapController, state: mapControllerState } = useWorkbenchMapController({
|
||||
mapRef,
|
||||
geoserverWorkspace: getWorkbenchProject(getAccessToken)?.gs_workspace,
|
||||
mapReady,
|
||||
leftPanelOpen: false,
|
||||
rightPanelOpen,
|
||||
@@ -464,6 +519,12 @@ export function MapWorkbenchPage({
|
||||
getPadding: () => mapRef.current ? getResponsiveWorkbenchPadding(mapRef.current, leftPanelOpen, rightPanelOpen, rightPanelExpanded, agentPanelWidth) : { top: 48, right: 48, bottom: 48, left: 48 }
|
||||
});
|
||||
const geocoding = useWorkbenchGeocoding({
|
||||
picking: geocodingPickActive,
|
||||
onPickingChange: (picking) => {
|
||||
setGeocodingPickActive(picking);
|
||||
if (picking) { setActiveDrawToolId(null); measurement.stop(); }
|
||||
},
|
||||
placeSearch: projectPresentation.placeSearch,
|
||||
active: activeToolId === "geocoding",
|
||||
projectId: projectPresentation.projectId,
|
||||
getAccessToken, mapRef, mapReady,
|
||||
@@ -479,7 +540,11 @@ export function MapWorkbenchPage({
|
||||
}
|
||||
});
|
||||
function handleLocateInvestigationTargets(targets: InvestigationMapTarget[]) {
|
||||
void presentation.locateTargets(targets).catch((error) => showMapNotice({ tone: "warning", title: "关联要素暂不可定位", message: error instanceof Error ? error.message : "地图当前不可用" }));
|
||||
if (readingWorkspace?.active) readingWorkspace.setView(businessWorkspaceWidth >= 960 ? "map_split" : "map_only");
|
||||
return presentation.locateTargets(targets).then(() => true).catch((error) => {
|
||||
showMapNotice({ tone: "warning", title: "关联要素暂不可定位", message: error instanceof Error ? error.message : "地图当前不可用" });
|
||||
return false;
|
||||
});
|
||||
}
|
||||
useEffect(() => () => {
|
||||
restorePresentationLayers();
|
||||
@@ -502,6 +567,7 @@ export function MapWorkbenchPage({
|
||||
[scheduledConditions]
|
||||
);
|
||||
const drawing = useWorkbenchDrawing({
|
||||
selectionEnabled: !geocodingPickActive,
|
||||
mapRef,
|
||||
mapReady,
|
||||
activeMode: activeDrawToolId,
|
||||
@@ -516,6 +582,7 @@ export function MapWorkbenchPage({
|
||||
});
|
||||
const taskTickerAvailable = runningTickerConditions.length > 0;
|
||||
const shouldShowTaskTicker =
|
||||
!readingWorkspace?.active &&
|
||||
!artifactActive &&
|
||||
mobileSheet === null &&
|
||||
!conditionFeedExpanded &&
|
||||
@@ -933,20 +1000,24 @@ export function MapWorkbenchPage({
|
||||
}
|
||||
|
||||
async function handleLoadAgentHistorySession(sessionId: string) {
|
||||
if (sessionId !== agent.sessionId) {
|
||||
mapController.clearScadaAnalysis();
|
||||
}
|
||||
await agent.loadHistorySession(sessionId);
|
||||
const changesSession = sessionId !== agent.sessionId;
|
||||
const loaded = await agent.loadHistorySession(sessionId);
|
||||
if (!loaded) throw new Error("会话打开已取消");
|
||||
if (changesSession) mapController.clearScadaAnalysis();
|
||||
}
|
||||
|
||||
async function handleOpenTaskCollaboration(taskId: string, discussion?: InvestigationDiscussion) {
|
||||
mapController.clearScadaAnalysis();
|
||||
await agent.openTaskCollaboration(taskId, discussion);
|
||||
const id = await agent.openTaskCollaboration(taskId, discussion);
|
||||
if (discussion?.reference && id) agent.setContentReference(discussion.reference);
|
||||
readingWorkspace?.close();
|
||||
openAgentPanelForViewport();
|
||||
}
|
||||
|
||||
async function handleDeleteAgentHistorySession(sessionId: string) {
|
||||
const clearsActiveAnalysis = sessionId === agent.sessionId;
|
||||
const deleted = await agent.deleteHistorySession(sessionId);
|
||||
if (!deleted) throw new Error("会话删除失败,请重试");
|
||||
if (deleted && clearsActiveAnalysis) {
|
||||
mapController.clearScadaAnalysis();
|
||||
}
|
||||
@@ -1134,7 +1205,7 @@ export function MapWorkbenchPage({
|
||||
? operationalTasks.tasks.find((item) => item.task_id === agent.sessionTaskId)
|
||||
: undefined;
|
||||
const promptSuggestions = useAgentPromptSuggestions({
|
||||
enabled: agent.conversationScope === "project" && (isLargeScreen ? agent.panelOpen : mobileSheet === "agent") && agent.messages.length === 0 && !agent.streaming &&
|
||||
enabled: (isLargeScreen ? agent.panelOpen : mobileSheet === "agent") && agent.messages.length === 0 && !agent.streaming &&
|
||||
agent.sessionPurpose === "ordinary_chat" && agent.connectionStatus === "online" && !agent.sessionHistoryLoading &&
|
||||
!scheduledConditionsLoading && !operationalTasks.loading,
|
||||
model: agent.selectedModel,
|
||||
@@ -1145,7 +1216,8 @@ export function MapWorkbenchPage({
|
||||
conditions: scheduledConditions,
|
||||
runtimeDataSource,
|
||||
artifact: latestAnalysisArtifact ?? null,
|
||||
sessions: agent.sessionHistory,
|
||||
sessions: agent.recentSessions,
|
||||
cacheScope: agent.cacheScope,
|
||||
});
|
||||
const agentPanelProps: Omit<
|
||||
ComponentProps<typeof AgentCommandPanel>,
|
||||
@@ -1155,6 +1227,25 @@ export function MapWorkbenchPage({
|
||||
sessionTitle: agent.sessionTitle,
|
||||
sessionHistory: agent.sessionHistory,
|
||||
sessionHistoryLoading: agent.sessionHistoryLoading,
|
||||
topicControls: agent.topicControls,
|
||||
historyTopicId: agent.history.topicId,
|
||||
onHistoryTopicChange: agent.history.setTopicId,
|
||||
onMoveSessionToTopic: agent.moveSessionToTopic,
|
||||
historyQuery: agent.history.query,
|
||||
onHistoryQueryChange: agent.history.setQuery,
|
||||
historyHasMore: agent.history.hasMore,
|
||||
historyLoadingMore: agent.history.loadingMore,
|
||||
historyError: agent.history.error ? "历史记录加载失败,请刷新重试" : undefined,
|
||||
historyPendingUpdate: agent.history.pendingUpdate,
|
||||
onHistoryOpen: agent.history.ensure,
|
||||
onHistoryClose: agent.closeHistory,
|
||||
onLoadMoreHistory: agent.history.loadMore,
|
||||
hasOlderMessages: agent.hasOlderMessages,
|
||||
loadingOlderMessages: agent.loadingOlderMessages,
|
||||
onLoadOlderMessages: agent.loadOlderMessages,
|
||||
readingPosition: agent.readingPosition,
|
||||
onReadingPositionChange: agent.onReadingPositionChange,
|
||||
backgroundStatus: agent.backgroundStatus,
|
||||
promptSuggestions: promptSuggestions.suggestions,
|
||||
promptSuggestionsLoading: promptSuggestions.loading,
|
||||
promptSuggestionsUnavailable: promptSuggestions.unavailable,
|
||||
@@ -1167,12 +1258,19 @@ export function MapWorkbenchPage({
|
||||
onInputDeliveryChange: agent.setInputDelivery,
|
||||
pendingInputs: agent.pendingInputs,
|
||||
onPendingInputDeliveryChange: agent.updateInputDelivery,
|
||||
onPendingInputEdit: (id) => agent.withdrawInput(id, true),
|
||||
onPendingInputCancel: (id) => agent.withdrawInput(id, false),
|
||||
onDownloadFile: async (path) => {
|
||||
try { await agent.apiClient.downloadFile(path); }
|
||||
catch (error) {
|
||||
showMapNotice({ tone: "error", title: "文件下载失败", message: error instanceof Error ? error.message : "请稍后重试" });
|
||||
}
|
||||
},
|
||||
messages: agent.messages,
|
||||
streamRenderState: agent.streamRenderState,
|
||||
modelOptions: agent.modelOptions,
|
||||
selectedModel: agent.selectedModel,
|
||||
conversationScope: agent.conversationScope,
|
||||
onConversationScopeChange: agent.changeConversationScope,
|
||||
currentProjectName: getWorkbenchProject(getAccessToken)?.name,
|
||||
approvalMode: agent.approvalMode,
|
||||
uiResults: activeAgentUiResults,
|
||||
reviewPanel: collaborationReviewTask ? <TaskConversationReview task={collaborationReviewTask}
|
||||
@@ -1185,6 +1283,11 @@ export function MapWorkbenchPage({
|
||||
onRefresh={operationalTasks.refresh} /> : undefined,
|
||||
workspaceContext: agentWorkspaceContext,
|
||||
prompt: agent.promptDraft,
|
||||
contentReference: agent.contentReference,
|
||||
onContentReferenceChange: (value) => {
|
||||
agent.setContentReference(value);
|
||||
if (value) readingWorkspace?.close();
|
||||
},
|
||||
promptReference: collaborationReviewTask ? {
|
||||
title: collaborationReviewTask.title,
|
||||
placeholder: "可询问调查进展、讨论报告或补充信息",
|
||||
@@ -1211,7 +1314,10 @@ export function MapWorkbenchPage({
|
||||
onRefresh: agent.agentMessageInbox.refresh
|
||||
} : undefined,
|
||||
onSubmitPrompt: agent.submitPrompt,
|
||||
onLocateAnswerFeatures: (target) => mapController.locateFeatures(target.layer, target.feature_ids, new AbortController().signal),
|
||||
onLocateAnswerFeatures: async (target) => {
|
||||
if (readingWorkspace?.active) readingWorkspace.setView(businessWorkspaceWidth >= 960 ? "map_split" : "map_only");
|
||||
return mapController.locateFeatures(target.layer, target.feature_ids, new AbortController().signal);
|
||||
},
|
||||
onStopPrompt: agent.stopPrompt,
|
||||
onReplyPermission: agent.replyPermission,
|
||||
onReplyQuestion: agent.replyQuestion,
|
||||
@@ -1241,6 +1347,7 @@ export function MapWorkbenchPage({
|
||||
alerts={workbenchAlerts}
|
||||
alertDataSource={alertDataSource}
|
||||
user={projectPresentation.user}
|
||||
projectSelection={projectSelection}
|
||||
conditionFeedVisible={isLargeScreen ? shouldShowConditionFeed : mobileSheet === "condition"}
|
||||
timelineVisible={timelineVisible}
|
||||
taskTickerAvailable={taskTickerAvailable}
|
||||
@@ -1298,113 +1405,7 @@ export function MapWorkbenchPage({
|
||||
onClose={() => setPeripheralSamplePanelOpen(false)}
|
||||
getAccessToken={getAccessToken}
|
||||
onAgentTrigger={handlePeripheralWorkOrderAgentTrigger}
|
||||
/>
|
||||
|
||||
<WorkbenchMainFrame
|
||||
activeArtifact={artifactWorkspace.activeArtifact}
|
||||
pendingArtifact={artifactWorkspace.pendingArtifact}
|
||||
surfaceMode={artifactWorkspace.surfaceMode}
|
||||
mapSplitAvailable={artifactWorkspace.mapSplitAvailable}
|
||||
onOpenPendingArtifact={artifactWorkspace.openPreview}
|
||||
onDestroyPendingArtifact={artifactWorkspace.destroyPreview}
|
||||
onSetArtifactView={artifactWorkspace.setView}
|
||||
onCollapseArtifact={artifactWorkspace.collapseArtifact}
|
||||
onDestroyArtifact={artifactWorkspace.destroyArtifact}
|
||||
timelineMode={timelineMode}
|
||||
timelineBottom={timelineBottom}
|
||||
timelineContent={
|
||||
timelineVisible ? (
|
||||
<>
|
||||
<div className="hidden lg:block">
|
||||
<OperationalTimeline
|
||||
date={workspaceContext.operational.date}
|
||||
cursorTime={workspaceContext.operational.cursorTime}
|
||||
events={operationalEvents}
|
||||
mode={timelineMode}
|
||||
selectedEventId={workspaceContext.operational.selectedEventId}
|
||||
onModeChange={setTimelineMode}
|
||||
onSelectEvent={handleSelectOperationalEvent}
|
||||
onClearSelection={handleClearOperationalSelection}
|
||||
onReturnNow={handleReturnOperationalNow}
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
role="region"
|
||||
aria-label="移动端运行时间轴摘要"
|
||||
className="acrylic-control flex h-12 w-full items-center rounded-2xl border p-1 text-xs text-slate-700 lg:hidden"
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
aria-label="打开完整运行时间轴"
|
||||
onClick={openTimelineForViewport}
|
||||
className="flex h-10 min-w-0 flex-1 items-center gap-1.5 rounded-xl px-2 text-left transition-colors hover:bg-white/55 active:bg-white/75"
|
||||
>
|
||||
<Clock3 size={15} className="shrink-0 text-blue-600" aria-hidden="true" />
|
||||
<span className="shrink-0 font-semibold">运行时间轴</span>
|
||||
<span className="flex min-w-0 items-center gap-1.5 whitespace-nowrap text-[10px]">
|
||||
<span className="text-slate-500">计划 {operationalSummary.planned}</span>
|
||||
<span className="text-slate-500">执行 {operationalSummary.active}</span>
|
||||
<span className="text-amber-700">关注 {operationalSummary.attention}</span>
|
||||
<span className="text-rose-700">异常 {operationalSummary.exceptions}</span>
|
||||
</span>
|
||||
<span className="ml-auto hidden shrink-0 text-slate-500 min-[360px]:inline">
|
||||
查看全天
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
</>
|
||||
) : null
|
||||
}
|
||||
mapContent={
|
||||
<div
|
||||
ref={mapContainerRef}
|
||||
className="map-grid absolute inset-0"
|
||||
style={{ position: "absolute", inset: 0 }}
|
||||
aria-label="供水管网地图"
|
||||
/>
|
||||
}
|
||||
mapOverlay={
|
||||
<div className="pointer-events-none absolute inset-x-0 bottom-0 top-14">
|
||||
{investigationFollow && selectedInvestigationTask && !devPanelOpen ? <InvestigationPresentationPanel
|
||||
task={selectedInvestigationTask} charts={presentation.charts} overlay={presentation.overlay} cameraPaused={presentation.cameraPaused}
|
||||
status={presentation.status} error={presentation.error}
|
||||
width={Math.min(380, Math.max(180, businessWorkspaceWidth - (rightPanelOpen ? (rightPanelExpanded ? (viewportWidth >= 1536 ? 960 : 880) : 432) + 64 : 64) - 24))}
|
||||
relatedLayers={presentationRelatedLayers} onToggleLayer={handleTogglePresentationLayer}
|
||||
onResumeCamera={presentation.resumeCamera} onCloseChart={presentation.closeChart} onToggleOverlay={presentation.toggleOverlay} onFitOverlay={presentation.fitOverlay}
|
||||
/> : null}
|
||||
{!devPanelOpen ? (
|
||||
<>
|
||||
<div className="pointer-events-auto absolute right-3 top-10 hidden lg:block">
|
||||
<MapToolbar items={toolbarItems} />
|
||||
</div>
|
||||
<div className="pointer-events-auto absolute right-16 top-10 hidden lg:block">
|
||||
<ToolbarPanel {...toolbarPanelProps} />
|
||||
</div>
|
||||
</>
|
||||
) : null}
|
||||
<div
|
||||
className="absolute right-3 flex flex-col items-end gap-2 bottom-[var(--mobile-map-control-bottom)] transition-[bottom] duration-200 ease-out motion-reduce:transition-none lg:right-0 lg:bottom-[var(--desktop-map-scale-bottom)]"
|
||||
style={
|
||||
{
|
||||
"--mobile-map-control-bottom": `${mobileOverlayLayout.mapControlBottom}px`,
|
||||
"--desktop-map-scale-bottom": `${timelineScaleLayout.scaleBottom}px`
|
||||
} as CSSProperties
|
||||
}
|
||||
>
|
||||
<div className="flex items-end gap-2 lg:pr-2"
|
||||
onPointerDownCapture={() => { if (investigationFollow) presentation.pauseCamera(); }}
|
||||
onKeyDownCapture={() => { if (investigationFollow) presentation.pauseCamera(); }}>
|
||||
<MapZoom
|
||||
mapRef={mapRef}
|
||||
mapReady={mapReady}
|
||||
onHome={fitNetworkBounds}
|
||||
reducedMotion={prefersReducedMotion ?? false}
|
||||
/>
|
||||
</div>
|
||||
<MapScaleLine className="hidden lg:inline-flex" mapRef={mapRef} mapReady={mapReady} />
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
onScadaAnomalyTrigger={handleScadaAnomalyAgentTrigger}
|
||||
/>
|
||||
|
||||
<WorkbenchAgentPanels
|
||||
@@ -1418,10 +1419,119 @@ export function MapWorkbenchPage({
|
||||
onCollapsePanel={agent.collapsePanel}
|
||||
onExpandPanel={agent.expandPanel}
|
||||
onWidthCommit={setAgentPanelWidth}
|
||||
/>
|
||||
>
|
||||
<WorkbenchMainFrame
|
||||
activeArtifact={artifactWorkspace.activeArtifact}
|
||||
pendingArtifact={artifactWorkspace.pendingArtifact}
|
||||
surfaceMode={artifactWorkspace.surfaceMode}
|
||||
mapSplitAvailable={artifactWorkspace.mapSplitAvailable}
|
||||
readingSplitAvailable={businessWorkspaceWidth >= 960}
|
||||
onOpenPendingArtifact={artifactWorkspace.openPreview}
|
||||
onDestroyPendingArtifact={artifactWorkspace.destroyPreview}
|
||||
onSetArtifactView={artifactWorkspace.setView}
|
||||
onCollapseArtifact={artifactWorkspace.collapseArtifact}
|
||||
onDestroyArtifact={artifactWorkspace.destroyArtifact}
|
||||
timelineMode={timelineMode}
|
||||
timelineBottom={timelineBottom}
|
||||
timelineContent={
|
||||
timelineVisible ? (
|
||||
<>
|
||||
<div className="hidden lg:block">
|
||||
<OperationalTimeline
|
||||
date={workspaceContext.operational.date}
|
||||
cursorTime={workspaceContext.operational.cursorTime}
|
||||
events={operationalEvents}
|
||||
mode={timelineMode}
|
||||
selectedEventId={workspaceContext.operational.selectedEventId}
|
||||
onModeChange={setTimelineMode}
|
||||
onSelectEvent={handleSelectOperationalEvent}
|
||||
onClearSelection={handleClearOperationalSelection}
|
||||
onReturnNow={handleReturnOperationalNow}
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
role="region"
|
||||
aria-label="移动端运行时间轴摘要"
|
||||
className="acrylic-control flex h-12 w-full items-center rounded-2xl border p-1 text-xs text-slate-700 lg:hidden"
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
aria-label="打开完整运行时间轴"
|
||||
onClick={openTimelineForViewport}
|
||||
className="flex h-10 min-w-0 flex-1 items-center gap-1.5 rounded-xl px-2 text-left transition-colors hover:bg-white/55 active:bg-white/75"
|
||||
>
|
||||
<Clock3 size={15} className="shrink-0 text-blue-600" aria-hidden="true" />
|
||||
<span className="shrink-0 font-semibold">运行时间轴</span>
|
||||
<span className="flex min-w-0 items-center gap-1.5 whitespace-nowrap text-[10px]">
|
||||
<span className="text-slate-500">计划 {operationalSummary.planned}</span>
|
||||
<span className="text-slate-500">执行 {operationalSummary.active}</span>
|
||||
<span className="text-amber-700">关注 {operationalSummary.attention}</span>
|
||||
<span className="text-rose-700">异常 {operationalSummary.exceptions}</span>
|
||||
</span>
|
||||
<span className="ml-auto hidden shrink-0 text-slate-500 min-[360px]:inline">
|
||||
查看全天
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
</>
|
||||
) : null
|
||||
}
|
||||
mapContent={
|
||||
<div
|
||||
ref={mapContainerRef}
|
||||
className="map-grid absolute inset-0"
|
||||
style={{ position: "absolute", inset: 0 }}
|
||||
aria-label="供水管网地图"
|
||||
/>
|
||||
}
|
||||
mapOverlay={
|
||||
<div className="pointer-events-none absolute inset-x-0 bottom-0 top-14">
|
||||
{investigationFollow && selectedInvestigationTask && !devPanelOpen ? <InvestigationPresentationPanel
|
||||
task={selectedInvestigationTask} charts={presentation.charts} overlay={presentation.overlay} cameraPaused={presentation.cameraPaused}
|
||||
status={presentation.status} error={presentation.error}
|
||||
width={Math.min(380, Math.max(180, businessWorkspaceWidth - (rightPanelOpen ? (rightPanelExpanded ? (viewportWidth >= 1536 ? 960 : 880) : 432) + 64 : 64) - 24))}
|
||||
relatedLayers={presentationRelatedLayers} onToggleLayer={handleTogglePresentationLayer}
|
||||
onResumeCamera={presentation.resumeCamera} onCloseChart={presentation.closeChart} onToggleOverlay={presentation.toggleOverlay} onFitOverlay={presentation.fitOverlay}
|
||||
/> : null}
|
||||
{!devPanelOpen ? (
|
||||
<>
|
||||
<div className="pointer-events-auto absolute right-3 top-10 hidden lg:block">
|
||||
<MapToolbar items={toolbarItems} />
|
||||
</div>
|
||||
<div className="pointer-events-auto absolute right-16 top-10 hidden lg:block">
|
||||
<ToolbarPanel {...toolbarPanelProps} />
|
||||
</div>
|
||||
</>
|
||||
) : null}
|
||||
<div
|
||||
className="absolute right-3 flex flex-col items-end gap-2 bottom-[var(--mobile-map-control-bottom)] transition-[bottom] duration-200 ease-out motion-reduce:transition-none lg:right-0 lg:bottom-[var(--desktop-map-scale-bottom)]"
|
||||
style={
|
||||
{
|
||||
"--mobile-map-control-bottom": `${mobileOverlayLayout.mapControlBottom}px`,
|
||||
"--desktop-map-scale-bottom": `${timelineScaleLayout.scaleBottom}px`
|
||||
} as CSSProperties
|
||||
}
|
||||
>
|
||||
<div className="flex items-end gap-2 lg:pr-2"
|
||||
onPointerDownCapture={() => { if (investigationFollow) presentation.pauseCamera(); }}
|
||||
onKeyDownCapture={() => { if (investigationFollow) presentation.pauseCamera(); }}>
|
||||
<MapZoom
|
||||
mapRef={mapRef}
|
||||
mapReady={mapReady}
|
||||
onHome={fitNetworkBounds}
|
||||
reducedMotion={prefersReducedMotion ?? false}
|
||||
/>
|
||||
</div>
|
||||
<MapScaleLine className="hidden lg:inline-flex" mapRef={mapRef} mapReady={mapReady} />
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
/>
|
||||
|
||||
</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 ? { display: "none" } : undefined}>
|
||||
<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}>
|
||||
<ScheduledConditionFeed
|
||||
presentation="desktop-floating"
|
||||
conditions={scheduledConditions}
|
||||
@@ -1469,7 +1579,7 @@ export function MapWorkbenchPage({
|
||||
/>
|
||||
) : null}
|
||||
|
||||
{mobileSheet === null && artifactWorkspace.surfaceMode !== "focus" ? (
|
||||
{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 }}
|
||||
@@ -1485,7 +1595,7 @@ export function MapWorkbenchPage({
|
||||
{shouldShowTaskTicker ? (
|
||||
<motion.div
|
||||
key="agent-task-ticker-shell"
|
||||
className="fixed left-1/2 right-auto top-24 z-50 w-[calc(100%-1.5rem)] [translate:-50%_0] md:absolute md:w-[var(--workbench-ticker-width)] 2xl:w-[var(--workbench-ticker-width-wide)]"
|
||||
className="fixed left-1/2 right-auto top-24 z-50 w-[calc(100%-1.5rem)] [translate:-50%_0] md:absolute md:w-[var(--workbench-ticker-width)] lg:left-[calc((100%+var(--workbench-agent-current-width))/2)] lg:max-w-[calc(100%-var(--workbench-agent-current-width)-1.5rem)] 2xl:w-[var(--workbench-ticker-width-wide)]"
|
||||
style={{ transformOrigin: "top center" }}
|
||||
initial={
|
||||
prefersReducedMotion
|
||||
@@ -1521,7 +1631,7 @@ export function MapWorkbenchPage({
|
||||
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} />
|
||||
<AgentPersona className="pointer-events-none h-10 w-10" state={agent.personaState} />
|
||||
</button>
|
||||
{artifactWorkspace.surfaceMode !== "focus" ? (
|
||||
<MapToolbar
|
||||
@@ -1545,6 +1655,8 @@ export function MapWorkbenchPage({
|
||||
|
||||
{mobileSheet ? (
|
||||
<MobileWorkbenchSheet
|
||||
className={readingWorkspace?.active ? "hidden" : undefined}
|
||||
suspended={Boolean(readingWorkspace?.active)}
|
||||
label={
|
||||
mobileSheet === "agent"
|
||||
? "Agent 工作台抽屉"
|
||||
|
||||
Reference in New Issue
Block a user