From 7a304a91a801d8785be27d01025f1dfe9dab018b Mon Sep 17 00:00:00 2001 From: Huarch Date: Thu, 12 Nov 2026 11:42:25 +0800 Subject: [PATCH] feat(workbench): dock task and chart workspaces --- docs/agent-answer-ui.md | 2 +- docs/investigation-dev-tools.md | 8 +- .../investigation-chart-manager.tsx | 6 +- .../components/scheduled-condition-feed.tsx | 91 ++++++------- .../scheduled-condition-workspace.tsx | 37 ++++++ .../hooks/use-investigation-presentation.ts | 17 ++- .../use-workbench-responsive-layout.test.ts | 6 + .../hooks/use-workbench-responsive-layout.ts | 43 +++--- src/features/workbench/map-workbench-page.tsx | 82 ++++++++++-- .../workspace/chart-workspace-context.ts | 5 + .../workspace/chart-workspace-provider.tsx | 17 +++ .../workspace/workbench-content-split.tsx | 6 +- .../workspace/workbench-main-frame.tsx | 56 ++++++-- .../workbench/workspace/workspace-model.ts | 2 + src/shared/ui/floating-window.test.tsx | 60 +++++++++ src/shared/ui/floating-window.tsx | 85 +++++++++--- .../browser/dynamic-investigation-flow.e2e.ts | 5 +- .../investigation-chart-docking.e2e.ts | 118 +++++++++++++++++ .../browser/investigation-chart-layout.e2e.ts | 9 +- .../investigation-chart-manager.e2e.ts | 13 +- .../browser/investigation-chart-resize.e2e.ts | 100 +++++++++----- tests/browser/map-option-interaction.e2e.ts | 8 +- tests/browser/mobile-workbench-sheet.e2e.ts | 4 + .../scheduled-condition-acrylic.e2e.ts | 28 ++-- .../scheduled-condition-docking.e2e.ts | 125 ++++++++++++++++++ .../mock-investigation-presentation.ts | 12 ++ tests/browser/workbench-window-menu.e2e.ts | 22 +-- 27 files changed, 766 insertions(+), 201 deletions(-) create mode 100644 src/features/workbench/components/scheduled-condition-workspace.tsx create mode 100644 src/features/workbench/workspace/chart-workspace-context.ts create mode 100644 src/features/workbench/workspace/chart-workspace-provider.tsx create mode 100644 tests/browser/investigation-chart-docking.e2e.ts create mode 100644 tests/browser/scheduled-condition-docking.e2e.ts diff --git a/docs/agent-answer-ui.md b/docs/agent-answer-ui.md index f785f59..8a00dfa 100644 --- a/docs/agent-answer-ui.md +++ b/docs/agent-answer-ui.md @@ -70,7 +70,7 @@ SCADA 的压力、水头(m)及流量、管流(m³/h)数值轴使用对 图表发布及 Dev 示例的对象聚合、同期校验和历史读取边界统一见 [调查展示 Dev 测试](investigation-dev-tools.md#对象复合图与验证规则)。结构化 Chart 保持单图语义,对象聚合通过 open_chart 的独立对象数据集实现。 -[图表和窗口管理](investigation-dev-tools.md#图表和窗口管理)由浏览器宿主负责:总窗口收纳过程图组与报告 Chart,子图表保留独立弹窗放大。总窗口提供单图、左右、四宫格固定槽位,全部图表沿纵向连续排列,末行按列数补齐空槽;整条位置栏提供拖拽和悬停反馈,拖放到目标位置交换内容。窄窗口使用单图,快速定位会滚动到目标位置。同一图表更新保留位置,删图留空,新图先填空槽。顶部窗口列表可定位、置顶已打开的总窗口、子图表和表格,并统一关闭。窗口缩放期间,图表按窗口实际尺寸适配并保留图表实例与筛选状态;松开后确认最终尺寸,Escape 或指针取消恢复原尺寸。 +[图表和窗口管理](investigation-dev-tools.md#图表和窗口管理)由浏览器宿主负责:管理区占用右侧分析区域,收纳过程图组与报告 Chart,子图表保留独立弹窗放大。管理区提供单图、左右、四宫格固定槽位,全部图表沿纵向连续排列,末行按列数补齐空槽;整条位置栏提供拖拽和悬停反馈,拖放到目标位置交换内容。窄区域使用单图,快速定位会滚动到目标位置。同一图表更新保留位置,删图留空,新图先填空槽。顶部窗口列表仅定位、置顶和统一关闭独立子图表与表格浮窗,不包含图表管理区。拖动分隔条或缩放独立浮窗时,图表按实际尺寸适配并保留图表实例与筛选状态;独立浮窗缩放支持 Escape 或指针取消恢复原尺寸。 任务打开、切换、刷新、已完成任务载入和历史恢复只更新内容,不弹窗;运行中新收到的 open_chart 由总窗口统一处理,关闭后的实时更新不重新弹出。排列、布局和窗口操作不新增模型输出或 wire 协议字段,也不改变图表校验与证据规则。具体自动弹出与关闭边界以上述管理说明为准。 diff --git a/docs/investigation-dev-tools.md b/docs/investigation-dev-tools.md index 873195a..4bbe9aa 100644 --- a/docs/investigation-dev-tools.md +++ b/docs/investigation-dev-tools.md @@ -22,7 +22,9 @@ Dev 与工况详情共用右侧工具位置,切换调查时先关闭 Dev 回 ## 图表和窗口管理 -调查、本地预览与对话展示面板提供“图表窗口”入口,在一个可拖动、缩放、最大化的总窗口中集中查看所有子图表,包括 open_chart 形成的对象图组与报告中的 Chart。每张子图表保留自身的独立弹窗放大入口,可同时打开多个放大窗口;关闭总窗口收起其内部开启的放大窗口,关闭子窗口只收起该放大视图。普通趋势总览、柱状图和报告 Chart 保持各自的图表语义。 +调查、本地预览与对话展示面板提供“图表窗口”入口,在右侧分析区域打开图表管理区,集中查看所有子图表,包括 open_chart 形成的对象图组与报告中的 Chart。管理区不提供浮窗拖动、边缘缩放或最大化按钮,通过工作区底部入口切换地图对照与专注图表。每张子图表保留自身的独立弹窗放大入口,可同时打开多个放大窗口;关闭管理区收起其内部开启的放大窗口,关闭子窗口只收起该放大视图。普通趋势总览、柱状图和报告 Chart 保持各自的图表语义。 + +图表管理区与地图分栏时,两侧最小宽度均为 600px;空间不足时显示专注图表。工况任务沿用展开按钮,展开后占用右侧分析区域,任务区最小宽度为 880px;普通打开或重新打开时默认不选择条目。切换工况任务与图表管理区保留已加载的图表、布局和筛选状态。 总窗口使用固定槽位布局,提供“单图布局”“左右布局”“四宫格布局”,每行分别显示 1、2、2 个固定位置,单图与左右布局每行约占一个可视区高度,四宫格每个可视区约显示两行。全部图表沿纵向连续排列,通过滚动查看;图表随槽位尺寸适配,末行按列数补齐空槽。窄窗口使用单图布局,调宽后恢复先前选择的布局。头部定位、布局和重置控件置于同一条浅底工具栏,控件保持等高,窄窗口允许自然换行。“快速定位图表”列出全部图表,选择后滚动到对应位置并聚焦目标;目标的位置栏短暂显示淡色背景和“已定位”,不使用整卡外框,避免高亮被滚动区域裁切。 @@ -32,9 +34,9 @@ Dev 与工况详情共用右侧工具位置,切换调查时先关闭 Dev 回 槽位映射依据稳定的子图表标识:同一图表实时更新时保留位置,删除图表后保留中间空槽并移除末尾空行,新图优先填补空槽,再追加位置与行。同一次任务查看期间,关闭并重新打开总窗口保留槽位与布局;任务、项目或执行代次切换后重新建立当前内容。交换位置不改变图表数据、对象选择或服务端的展示事件顺序。 -工作台顶部的窗口列表入口列出已打开的总图表窗口、独立子图表和表格窗口,选择条目可将对应窗口置顶并定位到标题栏。列表中的“关闭全部窗口”统一收起当前打开的窗口,不删除已加载的图表、表格或任务结果;图表窗口可从原展示入口重新打开。 +工作台顶部的窗口列表入口仅列出已打开的独立子图表和表格浮窗,图表管理区不计入列表和窗口数量。选择条目可将对应浮窗置顶并定位到标题栏。“关闭全部窗口”收起独立浮窗,保留图表管理区和已加载结果;图表管理区可从原展示入口重新打开。 -总窗口与子图表使用同一缩放交互:指针拖动窗口边缘时,图表按窗口实际尺寸适配,保持已有图表实例与筛选、序列显隐和对象选择;松开后确认最终尺寸。拖动期间按 Escape 或发生 pointercancel 时取消调整、恢复原尺寸;标题栏的 Shift 加方向键继续用于逐步调整大小。 +拖动地图与分析区域的分隔条时,管理区图表随实际尺寸适配,保持已有图表实例与筛选、序列显隐和对象选择。独立子图表仍支持拖动窗口边缘缩放;松开后确认最终尺寸,拖动期间按 Escape 或发生 pointercancel 时取消调整、恢复原尺寸,标题栏的 Shift 加方向键用于逐步调整大小。 只有正在跟随的运行中调查新收到 open_chart 时,才自动弹出一次总窗口,不自动弹出各子图表,不抢走键盘焦点;后续指令按 chart_id 更新其中的数据。打开、切换、刷新任务、SSE 重连补载、历史回看和回到当前都只恢复内容,已完成任务也不弹窗。用户关闭总窗口后,本次任务查看期间的后续更新不使其再次弹出;手动收起展示面板期间的新指令也不请求弹窗。总窗口已经打开时,切换历史动作或播放回看会同步更新窗口内的快照;窗口关闭时不自动打开,手动打开后显示当前回看位置。实时数据继续记录,但不覆盖正在查看的历史;“回到当前”显示最新结果。切换任务、项目或执行代次关闭原窗口并重新静默载入,返回之前看过的任务也不重放自动弹出意图。Chat 与本地 Dev 预览使用手动入口。 diff --git a/src/features/workbench/components/investigation-chart-manager.tsx b/src/features/workbench/components/investigation-chart-manager.tsx index a190afd..52afdad 100644 --- a/src/features/workbench/components/investigation-chart-manager.tsx +++ b/src/features/workbench/components/investigation-chart-manager.tsx @@ -11,6 +11,7 @@ import type { InvestigationChartGroup } from "../utils/investigation-chart-group import { presentationChartBlocks } from "../utils/presentation-chart-blocks"; import { InvestigationChartGroup as ChartGroup } from "./investigation-chart-group"; import { ChartDropSlot, ManagedChartSlot } from "./investigation-chart-slot"; +import { useChartWorkspace } from "../workspace/chart-workspace-context"; type ManagedChart = { id: string; title: string } & ({ group: InvestigationChartGroup; block?: never } | { block: AnswerUiBlock; group?: never }); const layouts = [{ id: "one", label: "单图布局", icon: Square }, { id: "two", label: "左右布局", icon: Columns2 }, { id: "four", label: "四宫格布局", icon: LayoutGrid }] as const; @@ -25,6 +26,7 @@ export function InvestigationChartManager({ groups, blocks, autoOpen, taskTitle, onCloseChart: (id: string) => void; references: (ids: string[]) => ReactNode; }) { + const dock = useChartWorkspace(); const entries = useMemo(() => [ ...groups.map((group) => ({ id: `command:${group.id}`, title: formatChartDisplayText(group.title), group })), ...presentationChartBlocks(blocks).map((block) => ({ id: `report:${block.block_id}`, title: formatChartDisplayText(String(block.spec.elements[block.spec.root].props.title)), block })) @@ -54,13 +56,13 @@ export function InvestigationChartManager({ groups, blocks, autoOpen, taskTitle, const count = entries.length; return 图表窗口 {count}} - initialSize={{ width: 1120, height: 760 }} fillContent + initialSize={{ width: 1120, height: 760 }} fillContent dock={dock} // A session gets one automatic request. Updates never revoke a user's dismissal. openRequestKey={autoOpen ? "live-charts" : undefined}> {() =>
-

{taskTitle}

{reviewing ? "历史回看" : "当前结果"} · 拖到固定位置交换,支持单图放大

+

{taskTitle}

{reviewing ? "历史回看" : "当前结果"} · 拖动交换位置,支持单图放大

{count} 张图表
diff --git a/src/features/workbench/components/scheduled-condition-feed.tsx b/src/features/workbench/components/scheduled-condition-feed.tsx index 44985a8..f32fd2c 100644 --- a/src/features/workbench/components/scheduled-condition-feed.tsx +++ b/src/features/workbench/components/scheduled-condition-feed.tsx @@ -4,6 +4,8 @@ import { ChevronRight, ClipboardList, History, + Maximize2, + Minimize2, RefreshCw, X, XCircle @@ -61,6 +63,7 @@ const CONDITION_CONTROL_SURFACE_CLASS_NAME = const MINIMUM_REFRESH_FEEDBACK_MS = 3_000; export type ScheduledConditionFeedPresentation = + | "workspace" | "desktop-dock" | "desktop-floating" | "mobile-sheet"; @@ -82,6 +85,7 @@ type ScheduledConditionFeedProps = { onSelectedConditionChange?: (conditionId: string | null) => void; onExpandAgent: () => void; onClose?: () => void; + reserveAgentRail?: boolean; onLoadHistorySession: (sessionId: string) => void | Promise; onSubmitPrompt: (prompt: string) => void | Promise; onOpenTaskCollaboration?: (taskId: string, discussion?: InvestigationDiscussion) => Promise; @@ -124,6 +128,7 @@ export function ScheduledConditionFeed({ onSelectedConditionChange, onExpandAgent, onClose, + reserveAgentRail = false, onLoadHistorySession, onSubmitPrompt, onOpenTaskCollaboration, @@ -160,8 +165,10 @@ export function ScheduledConditionFeed({ const desktopDock = presentation === "desktop-dock"; const compactPresentation = mobileSheet || desktopDock; const desktopFloating = presentation === "desktop-floating"; + const workspace = presentation === "workspace"; const expanded = controlledExpanded ?? uncontrolledExpanded; - const selectedConditionId = controlledSelectedConditionId ?? uncontrolledSelectedConditionId; + const selectedConditionId = controlledSelectedConditionId === undefined + ? uncontrolledSelectedConditionId : controlledSelectedConditionId; const conditionsByCategory = useMemo( () => groupConditionsByTaskCategory(conditions), [conditions] @@ -219,17 +226,14 @@ export function ScheduledConditionFeed({ : `定时 ${conditionsByCategory.scheduled.length} · 调查 ${conditionsByCategory.investigation.length} · 预约 ${conditionsByCategory.appointment.length}`; const selectedCondition = timelineConditions.find((condition) => condition.id === selectedConditionId) ?? - timelineConditions[0] ?? null; + const showCompactDetail = expanded && Boolean(selectedCondition); const focusConditionId = focusRequest?.conditionId; const focusRequestId = focusRequest?.requestId; const [focusedAction, setFocusedAction] = useState<{ conditionId: string; actionId: string } | null>(null); const updateExpanded = useCallback( (nextExpanded: boolean) => { - if (controlledExpanded === undefined) { - setUncontrolledExpanded(nextExpanded); - } - + if (controlledExpanded === undefined) setUncontrolledExpanded(nextExpanded); onExpandedChange?.(nextExpanded); }, [controlledExpanded, onExpandedChange] @@ -256,10 +260,7 @@ export function ScheduledConditionFeed({ }, [taskFilter, taskFilterOptions]); useEffect(() => { - if (!expanded) { - return; - } - + if (!expanded) return; // A controlled task selection survives panel remounts and detail-loading windows. const selected = conditions.find((condition) => condition.id === controlledSelectedConditionId); if (selected) { @@ -308,9 +309,6 @@ export function ScheduledConditionFeed({ function handleToggleExpanded() { updateExpanded(!expanded); - updateSelectedConditionId( - selectedConditionId ?? timelineConditions[0]?.id ?? null - ); } function handleTaskCategoryChange(nextCategory: WorkbenchTaskCategory) { @@ -318,9 +316,7 @@ export function ScheduledConditionFeed({ setTaskFilter(CONDITION_FILTER_ALL); setStatusFilter(CONDITION_FILTER_ALL); setRetainedScheduledCondition(null); - updateSelectedConditionId( - sortConditionsNewestFirst(conditionsByCategory[nextCategory])[0]?.id ?? null - ); + updateSelectedConditionId(null); } async function handleRefresh() { @@ -360,9 +356,7 @@ export function ScheduledConditionFeed({ ); } updateSelectedConditionId(conditionId); - if (!expanded) { - updateExpanded(true); - } + if (!expanded) updateExpanded(true); } function handleContinueConversation(condition: ScheduledConditionItem, discussion?: InvestigationDiscussion) { @@ -427,22 +421,20 @@ export function ScheduledConditionFeed({
-