feat: refresh workbench visual layout

This commit is contained in:
2026-07-27 11:50:29 +08:00
parent b9bc9c3d39
commit f0485ca86e
33 changed files with 1624 additions and 353 deletions
+179 -28
View File
@@ -8,8 +8,10 @@ import {
type CSSProperties
} from "react";
import { AnimatePresence, motion, useReducedMotion } from "motion/react";
import { CalendarClock } from "lucide-react";
import {
AgentCommandPanel,
AgentPersona,
toTrustedMapAction,
type FrontendActionRequest,
type UIEnvelopePayload,
@@ -29,6 +31,10 @@ import {
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";
@@ -92,10 +98,18 @@ export function MapWorkbenchPage() {
const [impactVisible, setImpactVisible] = useState(false);
const [isLargeScreen, setIsLargeScreen] = useState(false);
const [viewportWidth, setViewportWidth] = useState<number>(WORKBENCH_LAYOUT.desktopMinWidth);
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 [mobileSheetClosing, setMobileSheetClosing] = useState(false);
const [agentCollapsedForCondition, setAgentCollapsedForCondition] = useState(false);
const prefersReducedMotion = useReducedMotion();
const [taskTickerVisible, setTaskTickerVisible] = useState(true);
const [selectedConditionId, setSelectedConditionId] = useState<string | null>(null);
const [conditionFocusRequest, setConditionFocusRequest] = useState<{
@@ -133,6 +147,10 @@ export function MapWorkbenchPage() {
const handleChange = () => {
setIsLargeScreen(mediaQuery.matches);
setViewportWidth(window.innerWidth);
if (mediaQuery.matches) {
setMobileSheet(null);
setMobileSheetClosing(false);
}
};
handleChange();
@@ -167,20 +185,88 @@ export function MapWorkbenchPage() {
});
function openAgentPanelForViewport() {
if (isLargeScreen) {
if (conditionFeedExpanded && viewportWidth < 1440) {
setConditionFeedExpanded(false);
setAgentCollapsedForCondition(false);
}
agent.expandPanel();
return;
}
agent.openMobilePanel();
setMobileSheetClosing(false);
setMobileSheet("agent");
setMobileSheetSnap("half");
}
const closeMobileSheet = useCallback(() => {
if (prefersReducedMotion) {
setMobileSheet(null);
setMobileSheetClosing(false);
return;
}
setMobileSheetClosing(true);
}, [prefersReducedMotion]);
const completeMobileSheetClose = useCallback(() => {
setMobileSheet(null);
setMobileSheetClosing(false);
}, []);
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);
setMobileSheetClosing(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,
leftPanelOpen,
rightPanelOpen,
impactVisible,
onSelectFeature: handleSelectFeature,
selectedFeature: detailFeature
@@ -189,7 +275,9 @@ export function MapWorkbenchPage() {
mapRef,
mapReady,
leftPanelOpen,
rightPanelOpen
rightPanelOpen,
conditionPanelExpanded: rightPanelExpanded,
agentPanelWidth
});
const activeAgentUiResults = useMemo(
@@ -223,10 +311,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,
@@ -489,6 +576,11 @@ export function MapWorkbenchPage() {
setConditionFeedVisible(true);
setActiveToolId(null);
if (!isLargeScreen) {
setMobileSheetClosing(false);
setMobileSheet("condition");
setMobileSheetSnap("half");
}
setConditionFocusRequest((current) => ({
conditionId,
requestId: (current?.requestId ?? 0) + 1
@@ -608,7 +700,13 @@ export function MapWorkbenchPage() {
map.easeTo({
center: trustedAction.center,
zoom: trustedAction.zoom ?? Math.max(map.getZoom(), 14),
padding: getResponsiveWorkbenchPadding(map, leftPanelOpen, rightPanelOpen),
padding: getResponsiveWorkbenchPadding(
map,
leftPanelOpen,
rightPanelOpen,
rightPanelExpanded,
agentPanelWidth
),
duration: trustedAction.durationMs ?? 500
});
showMapNotice({
@@ -757,7 +855,14 @@ export function MapWorkbenchPage() {
<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}
@@ -773,17 +878,14 @@ export function MapWorkbenchPage() {
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}
@@ -800,19 +902,16 @@ export function MapWorkbenchPage() {
panelProps={agentPanelProps}
panelOpen={agent.panelOpen}
panelCollapsing={agent.panelCollapsing}
mobileOpen={agent.mobileOpen}
mobilePanelCollapsing={agent.mobilePanelCollapsing}
personaState={agent.personaState}
statusLabel={agent.statusLabel}
viewportWidth={viewportWidth}
onCollapsePanel={agent.collapsePanel}
onCloseMobilePanel={agent.closeMobilePanel}
onExpandPanel={agent.expandPanel}
onOpenMobilePanel={agent.openMobilePanel}
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}
@@ -820,13 +919,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}
@@ -851,8 +951,8 @@ export function MapWorkbenchPage() {
/>
) : 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"
@@ -890,16 +990,67 @@ 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-12 w-12 shrink-0 place-items-center rounded-xl border text-violet-700 active:scale-95"
>
<AgentPersona className="pointer-events-none h-9 w-9" state={agent.personaState} />
</button>
<MapToolbar items={toolbarItems} orientation="horizontal" className="justify-center" />
<button
type="button"
aria-label="打开工况任务"
title="打开工况任务"
onClick={toggleConditionFeedForViewport}
className="acrylic-control grid h-12 w-12 shrink-0 place-items-center rounded-xl border text-blue-700 active:scale-95"
>
<CalendarClock size={20} aria-hidden="true" />
</button>
</div>
) : null}
</div>
{mobileSheet ? (
<MobileWorkbenchSheet
label={mobileSheet === "agent" ? "Agent 工作台抽屉" : "工况任务抽屉"}
snap={mobileSheetSnap}
onSnapChange={setMobileSheetSnap}
onClose={closeMobileSheet}
closing={mobileSheetClosing}
onCloseComplete={completeMobileSheetClose}
>
{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}
onLoadHistorySession={handleLoadAgentHistorySession}
onSubmitPrompt={agent.submitPrompt}
/>
)}
</MobileWorkbenchSheet>
) : null}
<FeaturePopover feature={detailFeature} onClose={() => setDetailFeature(null)} />
{!mapReady && !mapError ? (