feat: add project-aware agent workspace
Generic Container CI/CD / test-build-publish (push) Successful in 2m9s
Frontend CI/CD / build-test-publish-and-deploy (push) Successful in 2m39s

This commit is contained in:
2026-10-22 18:52:18 +08:00
parent c6a0e74e72
commit 2d8506fea0
191 changed files with 12373 additions and 3556 deletions
+246 -134
View File
@@ -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 工作台抽屉"