diff --git a/src/features/agent/answer-ui/answer-display-time.test.ts b/src/features/agent/answer-ui/answer-display-time.test.ts index d71d6bb..088f621 100644 --- a/src/features/agent/answer-ui/answer-display-time.test.ts +++ b/src/features/agent/answer-ui/answer-display-time.test.ts @@ -17,7 +17,7 @@ describe("answer presentation time zone", () => { expect(result.elements.table.props.rows).toEqual([["2026/7/16 08:00:00(UTC+8)"]]); expect(result.elements.evidence.props.observed_at).toBe("2026/7/16 08:00:00(UTC+8)"); expect(result.elements.evidence.props.evidence_id).toBe("2026-07-16T00:00:00Z"); - expect(result.elements.chart.props.x_axis_name).toBe("时间(UTC+8)"); + expect(result.elements.chart.props.x_axis_name).toBe("时间"); expect(result.elements.chart.props.x_data).toEqual(["08:00"]); expect(JSON.stringify(spec)).toBe(original); expect(withAnswerDisplayTimes(result)).toEqual(result); @@ -36,4 +36,23 @@ describe("answer presentation time zone", () => { expect(result.elements.table.props.rows).toEqual([["2027/1/1 01:00:00(UTC+8)", "00:00"]]); expect(withAnswerDisplayTimes(result)).toEqual(result); }); + it("removes chart timezone wording while preserving the window, data and sample instants", () => { + const samples = ["2026-07-15T23:30:00Z", "2026-07-15T23:45:00Z"]; + const spec: Spec = { root: "chart", elements: { + chart: { type: "Chart", props: { + title: "回放流量:487434(m³/h,上海时间 07-16 04:45–07:45)", + x_axis_name: "上海时间", x_data: ["07:30", "07:45"], + series: [{ name: "当前回放(2026-07-16,UTC+8)", data: [420, 380], sample_times: samples }] + }, children: [] } + } }; + const result = withAnswerDisplayTimes(spec); + expect(result.elements.chart.props.title).toBe("回放流量:487434(m³/h,07-16 04:45–07:45)"); + expect(result.elements.chart.props.x_axis_name).toBe("时间"); + expect(result.elements.chart.props.x_data).toEqual(["07:30", "07:45"]); + expect(result.elements.chart.props.series).toEqual([ + { name: "当前回放(2026-07-16)", data: [420, 380], sample_times: samples } + ]); + expect(withAnswerDisplayTimes(result)).toEqual(result); + }); + }); diff --git a/src/features/agent/answer-ui/answer-display-time.ts b/src/features/agent/answer-ui/answer-display-time.ts index f0debfd..c2e4f66 100644 --- a/src/features/agent/answer-ui/answer-display-time.ts +++ b/src/features/agent/answer-ui/answer-display-time.ts @@ -1,5 +1,5 @@ import type { Spec } from "@json-render/core"; -import { formatDisplayText } from "@/shared/utils/date-time-text"; +import { formatDisplayText, withChartDisplayTimes } from "@/shared/utils/date-time-text"; const textFields = new Set(["title", "description", "summary", "text", "label", "value", "source", "observed_at"]); const utcLabel = /UTC(?:\s*\+?0(?::00)?)?(?![\w+:-])/; @@ -23,14 +23,7 @@ export function withAnswerDisplayTimes(spec: Spec): Spec { props.columns = columns.map(timeAxis); } if (node.type === "Chart") { - const axis = typeof props.x_axis_name === "string" ? props.x_axis_name : ""; - props.x_data = (props.x_data as string[]).map((label) => { - const displayed = displayCell(label, axis); - return timeAxis(axis).includes("UTC+8") ? displayed.replace(/(UTC\+8)$/, "") : displayed; - }); - if (typeof props.x_axis_name === "string") props.x_axis_name = timeAxis(props.x_axis_name); - props.series = (props.series as Array>).map((series) => ({ ...series, - name: typeof series.name === "string" ? formatDisplayText(series.name) : series.name })); + Object.assign(props, withChartDisplayTimes(props as Parameters[0])); } return [id, { ...node, props }]; })) }; diff --git a/src/features/agent/components/agent-ui-envelope-renderer.tsx b/src/features/agent/components/agent-ui-envelope-renderer.tsx index 2c7ce04..f6e2fd6 100644 --- a/src/features/agent/components/agent-ui-envelope-renderer.tsx +++ b/src/features/agent/components/agent-ui-envelope-renderer.tsx @@ -1,6 +1,7 @@ import { Activity, BarChart3, Database, History, LineChart, MonitorCog } from "lucide-react"; import { AnimatePresence, MotionConfig, motion } from "motion/react"; import { memo, type ReactNode } from "react"; +import { formatChartDisplayText, withChartDisplayTimes } from "@/shared/utils/date-time-text"; import { StatusIcon } from "@/shared/ui/status"; import { StandaloneInteractiveChart } from "../answer-ui/interactive-chart"; import { normalizeSafeChartData, parseChartSpec } from "../chart-data"; @@ -78,8 +79,12 @@ export const AgentChartView = memo(function AgentChartView({ chart, embedded = f }); function SafeChartEnvelope({ envelope, embedded = false, expanded = false }: { envelope: Extract; embedded?: boolean; expanded?: boolean }) { - const spec = parseChartSpec(envelope.spec); - const data = normalizeSafeChartData(envelope.data, 100); + const parsed = parseChartSpec(envelope.spec); + const raw = normalizeSafeChartData(envelope.data, 100); + const displayed = withChartDisplayTimes({ title: parsed.title ?? "Agent 图表", x_axis_name: parsed.xAxisName, + x_data: raw?.xData ?? [], series: raw?.series ?? [] }); + const spec = { ...parsed, title: displayed.title, xAxisName: displayed.x_axis_name }; + const data = raw ? { ...raw, xData: displayed.x_data, series: displayed.series } : null; if (!data || data.series.length === 0 || data.xData.length === 0) { return ; @@ -206,7 +211,7 @@ function getEnvelopeIcon(envelope: UIEnvelope) { function getEnvelopeTitle(envelope: UIEnvelope) { if (envelope.kind === "chart") { const spec = parseChartSpec(envelope.spec); - return spec.title ?? "Agent 图表"; + return formatChartDisplayText(spec.title ?? "Agent 图表"); } if (envelope.kind === "registered_component") { diff --git a/src/features/workbench/components/investigation-presentation-panel.tsx b/src/features/workbench/components/investigation-presentation-panel.tsx index 34d8115..543b6c8 100644 --- a/src/features/workbench/components/investigation-presentation-panel.tsx +++ b/src/features/workbench/components/investigation-presentation-panel.tsx @@ -2,6 +2,7 @@ import { useId, useState } from "react"; import { AnimatePresence, motion, useReducedMotion } from "motion/react"; import { ChevronDown, Eye, EyeOff, LocateFixed, MapPinned, Pause, X } from "lucide-react"; import { AgentChartView, type AnswerUiBlock } from "@/features/agent"; +import { formatChartDisplayText } from "@/shared/utils/date-time-text"; import { Badge } from "@/shared/ui/badge"; import { Button } from "@/shared/ui/button"; import { Card, CardContent, CardHeader } from "@/shared/ui/card"; @@ -295,13 +296,13 @@ export function InvestigationPresentationPanel({

- {item.chart.title} + {formatChartDisplayText(item.chart.title)}

{item.chart.series.some((series) => series.type === "scatter") ? "折线与历史散点" : item.chart.chart_type === "bar" ? "柱状图" : "折线图"} ·{" "} @@ -310,7 +311,7 @@ export function InvestigationPresentationPanel({

- {() =>
@@ -323,7 +324,7 @@ export function InvestigationPresentationPanel({ size="icon-sm" className="shrink-0" disabled={reviewing} - aria-label={`关闭图表 ${item.chart.title}`} + aria-label={`关闭图表 ${formatChartDisplayText(item.chart.title)}`} onClick={() => onCloseChart(item.chart_id)} > diff --git a/src/features/workbench/utils/presentation-command-label.ts b/src/features/workbench/utils/presentation-command-label.ts index f68d588..de26ea7 100644 --- a/src/features/workbench/utils/presentation-command-label.ts +++ b/src/features/workbench/utils/presentation-command-label.ts @@ -1,3 +1,4 @@ +import { formatChartDisplayText } from "@/shared/utils/date-time-text"; import type { InvestigationPresentationCommand } from "../api/investigation-presentation"; import { investigationActionLabels } from "./investigation-dev-preview"; import type { PresentationView } from "./presentation-view-history"; @@ -8,11 +9,11 @@ export function presentationCommandLabel( ) { const detail = command.action === "open_chart" - ? command.chart.title + ? formatChartDisplayText(command.chart.title) : command.action === "set_overlay" ? command.title : command.action === "close_chart" - ? view.charts.find((chart) => chart.chart_id === command.chart_id)?.chart.title + ? formatChartDisplayText(view.charts.find((chart) => chart.chart_id === command.chart_id)?.chart.title ?? "") : "feature_ids" in command ? command.feature_ids.slice(0, 3).join("、") : null; diff --git a/src/shared/utils/date-time-text.ts b/src/shared/utils/date-time-text.ts index 628a722..d77103a 100644 --- a/src/shared/utils/date-time-text.ts +++ b/src/shared/utils/date-time-text.ts @@ -41,3 +41,32 @@ export function formatDisplayText(text: string | null | undefined): string { }); }).join(""); } + +/** Chart labels use the display clock without repeating its zone to readers. */ +export function formatChartDisplayText(value: string): string { + return formatDisplayText(value) + .replace(/[((]\s*(?:上海时间|Asia\/Shanghai|UTC\s*\+\s*8(?::00)?)\s*[))]/g, "") + .replace(/(?:上海时间|Asia\/Shanghai|UTC\s*\+\s*8(?::00)?)(?![\w:+-])\s*/g, "") + .replace(/[,,]\s*([))])/g, "$1") + .trim(); +} + +type ChartDisplayFields = { + title: string; + x_axis_name?: string; + y_axis_name?: string; + x_data: string[]; + series: Array<{ name: string }>; +}; + +/** Preserve values and original sample timestamps; convert only visible labels. */ +export function withChartDisplayTimes(chart: T): T { + const utcAxis = /UTC(?:\s*\+?0(?::00)?)?(?![\w+:-])/.test(chart.x_axis_name ?? ""); + const bareTime = /^(?:\d{4}-\d{2}-\d{2}[T ])?\d{2}:\d{2}(?::\d{2}(?:\.\d{1,9})?)?$/; + return { ...chart, + title: formatChartDisplayText(chart.title), + ...(chart.x_axis_name ? { x_axis_name: formatChartDisplayText(chart.x_axis_name.replace(/UTC(?:\s*\+?0(?::00)?)?(?![\w+:-])/g, "UTC+8")) || "时间" } : {}), + x_data: chart.x_data.map((label) => formatChartDisplayText(utcAxis && bareTime.test(label) ? `${label} UTC` : label)), + series: chart.series.map((series) => ({ ...series, name: formatChartDisplayText(series.name) })) + }; +} diff --git a/tests/browser/investigation-chart-windows.e2e.ts b/tests/browser/investigation-chart-windows.e2e.ts index bacb312..c77a6a2 100644 --- a/tests/browser/investigation-chart-windows.e2e.ts +++ b/tests/browser/investigation-chart-windows.e2e.ts @@ -156,7 +156,7 @@ test("SCADA chart axes and legends fit every nested container", async ({ page }, series: [{ name: "测点 123748(实测回放)", data: [26.2, 25.7, 23.5] }, { name: "测点 159673(实测回放)", data: [24.9, 24.87, 21.6] }] } }); - const popup = page.getByRole("dialog", { name: "SCADA 回放压力:异常前后(2026-07-16,UTC+8)", exact: true }); + const popup = page.getByRole("dialog", { name: "SCADA 回放压力:异常前后(2026-07-16)", exact: true }); await expect(popup).toBeVisible(); await expect.poll(() => popup.evaluate((node) => Array.from(node.querySelectorAll("*")) .filter((element) => { @@ -173,3 +173,31 @@ test("SCADA chart axes and legends fit every nested container", async ({ page }, body: await popup.screenshot({ path: testInfo.outputPath("scada-chart-layout.png") }), contentType: "image/png" }); }); + + +test("SCADA pressure and flow keep full comparison data without timezone wording", async ({ page }, testInfo) => { + const task = investigationFixture(1, "SCADA 三小时对照核查"); + const mock = await mockInvestigationPresentation(page, [task]); + await selectInvestigation(page, task.title); + const labels = Array.from({ length: 13 }, (_, i) => `${String(4 + Math.floor((45 + i * 15) / 60)).padStart(2, "0")}:${String((45 + i * 15) % 60).padStart(2, "0")}`); + for (const [metric, device, unit] of [["压力", "123748", "m"], ["流量", "487434", "m³/h"]]) { + await mock.emit(task, { action: "open_chart", chart_id: `scada-${device}`, evidence_refs: ["synthetic:pressure"], chart: { + title: `回放${metric}:${device}(${unit},上海时间 07-16 04:45–07:45)`, chart_type: "line", + x_axis_name: "上海时间", y_axis_name: `${metric}(${unit})`, x_data: labels, + series: [{ name: "当前回放(2026-07-16)", type: "line", data: labels.map((_, i) => 30 + i) }, + ...Array.from({ length: 7 }, (_, day) => ({ name: `参考 07-${String(9 + day).padStart(2, "0")}`, type: "scatter", + data: labels.map((_, i) => i < 10 ? null : 28 + day) }))] + } }); + const popup = page.getByRole("dialog", { name: `回放${metric}:${device}(${unit},07-16 04:45–07:45)`, exact: true }); + await expect(popup).toBeVisible(); + await expect(popup.getByText(/上海时间|UTC\+8|Asia\/Shanghai/)).toHaveCount(0); + await expect(popup.getByRole("img", { name: `回放${metric}:${device}(${unit},07-16 04:45–07:45)`, exact: true })).toBeVisible(); + await testInfo.attach(`scada-${device}-comparison`, { body: await popup.screenshot(), contentType: "image/png" }); + await popup.getByRole("tab", { name: "数据", exact: true }).click(); + const table = popup.getByRole("table"); + await expect(table).toBeVisible(); + await expect(table.getByRole("row")).toHaveCount(14); + await expect(table.getByRole("columnheader")).toHaveCount(9); + await popup.getByRole("button", { name: "关闭窗口", exact: true }).click(); + } +});