fix(chat): normalize chart tool data
Build Push and Deploy / docker-image (push) Failing after 42s
Build Push and Deploy / deploy-fallback-log (push) Successful in 1s

This commit is contained in:
2026-06-10 16:19:39 +08:00
parent d80a071987
commit 0501afaced
4 changed files with 205 additions and 14 deletions
+7 -3
View File
@@ -17,7 +17,6 @@ import SensorsRounded from "@mui/icons-material/SensorsRounded";
import BuildCircleRounded from "@mui/icons-material/BuildCircleRounded";
import { ChatInlineChart } from "./ChatInlineChart";
import type { ChatChartSeries } from "./ChatInlineChart";
import type { AgentArtifact } from "./GlobalChatbox.types";
const artifactIcon = (kind: AgentArtifact["kind"]) => {
@@ -61,8 +60,13 @@ export const AgentArtifactPanel = ({ artifacts }: { artifacts: AgentArtifact[] }
chart_type={
(artifact.params.chart_type as "line" | "bar" | "pie") ?? "line"
}
x_data={(artifact.params.x_data as string[]) ?? []}
series={(artifact.params.series as ChatChartSeries[]) ?? []}
x_data={
artifact.params.x_data ??
artifact.params.xData ??
artifact.params.labels ??
artifact.params.categories
}
series={artifact.params.series}
x_axis_name={(artifact.params.x_axis_name as string) ?? undefined}
y_axis_name={(artifact.params.y_axis_name as string) ?? undefined}
/>
+2 -3
View File
@@ -26,7 +26,6 @@ import type { Message, SpeechState } from "./GlobalChatbox.types";
import { stripMarkdown } from "./GlobalChatbox.utils";
import { AgentProgressTimeline } from "./AgentProgressTimeline";
import { ChatInlineChart } from "./ChatInlineChart";
import type { ChatChartSeries } from "./ChatInlineChart";
import { ChatToolCallBlock } from "./ChatToolCallBlock";
import { MarkdownBlock, normalizeClipboardText } from "./AgentMarkdownBlock";
import { PermissionRequestGroup } from "./AgentPermissionRequests";
@@ -251,8 +250,8 @@ export const AgentTurn = React.memo(
chart_type={
(p.chart_type as "line" | "bar" | "pie") ?? "line"
}
x_data={(p.x_data as string[]) ?? []}
series={(p.series as ChatChartSeries[]) ?? []}
x_data={p.x_data ?? p.xData ?? p.labels ?? p.categories}
series={p.series}
x_axis_name={(p.x_axis_name as string) ?? undefined}
y_axis_name={(p.y_axis_name as string) ?? undefined}
/>
@@ -0,0 +1,49 @@
import { normalizeChartData } from "./ChatInlineChart";
describe("normalizeChartData", () => {
it("keeps standard bar chart series data", () => {
const result = normalizeChartData(["A", "B"], [
{ name: "数量", data: [3, 5], type: "bar" },
]);
expect(result).toEqual({
xData: ["A", "B"],
series: [{ name: "数量", data: [3, 5], type: "bar" }],
});
});
it("normalizes line chart point arrays into x labels and y values", () => {
const result = normalizeChartData(undefined, [
{ name: "压力", data: [["10:00", 12.5], ["11:00", 13.1]] },
]);
expect(result).toEqual({
xData: ["10:00", "11:00"],
series: [{ name: "压力", data: [12.5, 13.1], type: undefined }],
});
});
it("normalizes pie chart point objects into a single series", () => {
const result = normalizeChartData(undefined, [
{ name: "低风险", value: 8 },
{ name: "高风险", value: 2 },
]);
expect(result).toEqual({
xData: ["低风险", "高风险"],
series: [{ name: "数据", data: [8, 2], type: undefined }],
});
});
it("accepts a single series object", () => {
const result = normalizeChartData(["A", "B"], {
name: "流量",
values: ["1.2", "2.4"],
});
expect(result).toEqual({
xData: ["A", "B"],
series: [{ name: "流量", data: [1.2, 2.4], type: undefined }],
});
});
});
+147 -8
View File
@@ -16,11 +16,25 @@ export interface ChatChartSeries {
type?: "line" | "bar";
}
type RawChartPoint =
| number
| string
| [unknown, unknown]
| { x?: unknown; y?: unknown; time?: unknown; timestamp?: unknown; label?: unknown; name?: unknown; value?: unknown };
type RawChartSeries = {
name?: unknown;
data?: unknown;
points?: unknown;
values?: unknown;
type?: unknown;
};
export interface ChatInlineChartProps {
title?: string;
chart_type?: "line" | "bar" | "pie";
x_data?: string[];
series?: ChatChartSeries[];
x_data?: unknown;
series?: unknown;
y_axis_name?: string;
x_axis_name?: string;
}
@@ -37,23 +51,148 @@ const COLORS = [
"#ea7ccc",
];
const toFiniteNumber = (value: unknown): number | null => {
if (typeof value === "number") {
return Number.isFinite(value) ? value : null;
}
if (typeof value === "string" && value.trim()) {
const parsed = Number(value);
return Number.isFinite(parsed) ? parsed : null;
}
return null;
};
export const pointToLabelValue = (
point: RawChartPoint,
fallbackLabel: string,
): { label: string; value: number } | null => {
const directValue = toFiniteNumber(point);
if (directValue !== null) {
return { label: fallbackLabel, value: directValue };
}
if (Array.isArray(point)) {
const value = toFiniteNumber(point[1]);
if (value === null) return null;
return { label: String(point[0] ?? fallbackLabel), value };
}
if (point && typeof point === "object") {
const value = toFiniteNumber(point.value ?? point.y);
if (value === null) return null;
const label =
point.x ?? point.time ?? point.timestamp ?? point.label ?? point.name ?? fallbackLabel;
return { label: String(label), value };
}
return null;
};
const normalizeXData = (rawXData: unknown): string[] =>
Array.isArray(rawXData)
? rawXData.map((item) => String(item ?? "")).filter((item) => item.length > 0)
: [];
const normalizeSeriesType = (type: unknown): "line" | "bar" | undefined =>
type === "line" || type === "bar" ? type : undefined;
const isRawChartPoint = (item: unknown): boolean => {
if (toFiniteNumber(item) !== null) return true;
if (Array.isArray(item)) return item.length >= 2 && toFiniteNumber(item[1]) !== null;
if (item && typeof item === "object") {
const rawItem = item as RawChartSeries & RawChartPoint;
return (
rawItem.data === undefined &&
rawItem.points === undefined &&
rawItem.values === undefined &&
toFiniteNumber(rawItem.value ?? rawItem.y) !== null
);
}
return false;
};
const normalizeRawSeriesItems = (rawSeries: unknown): unknown[] => {
if (!Array.isArray(rawSeries)) {
return rawSeries && typeof rawSeries === "object" ? [rawSeries] : [];
}
return rawSeries.length > 0 && rawSeries.every(isRawChartPoint)
? [{ name: "数据", data: rawSeries }]
: rawSeries;
};
export const normalizeChartData = (
rawXData: unknown,
rawSeries: unknown,
): { xData: string[]; series: ChatChartSeries[] } => {
const xData = normalizeXData(rawXData);
const rawSeriesItems = normalizeRawSeriesItems(rawSeries);
if (!rawSeriesItems.length) {
return { xData, series: [] };
}
const normalizedSeries = rawSeriesItems
.map((rawItem, seriesIndex): ChatChartSeries | null => {
const item =
rawItem && typeof rawItem === "object" && !Array.isArray(rawItem)
? (rawItem as RawChartSeries)
: ({ data: rawItem } satisfies RawChartSeries);
const rawData = item.data ?? item.points ?? item.values;
if (!Array.isArray(rawData)) return null;
const labelsFromPoints: string[] = [];
const data = rawData
.map((point, index) => {
const parsed = pointToLabelValue(
point as RawChartPoint,
xData[index] ?? `${index + 1}`,
);
if (!parsed) return null;
labelsFromPoints[index] = parsed.label;
return parsed.value;
})
.filter((value): value is number => value !== null);
if (!data.length) return null;
if (!xData.length && labelsFromPoints.length) {
xData.push(...labelsFromPoints);
}
return {
name:
typeof item.name === "string" && item.name.trim()
? item.name
: `系列 ${seriesIndex + 1}`,
data,
type: normalizeSeriesType(item.type),
};
})
.filter((item): item is ChatChartSeries => Boolean(item));
return { xData, series: normalizedSeries };
};
export const ChatInlineChart: React.FC<ChatInlineChartProps> = ({
title,
chart_type: chartType = "line",
x_data: xData,
series = [],
x_data,
series,
y_axis_name: yAxisName,
x_axis_name: xAxisName,
}) => {
const theme = useTheme();
const { xData, series: chartSeries } = useMemo(
() => normalizeChartData(x_data, series),
[x_data, series],
);
const option = useMemo(() => {
if (!series.length) return null;
if (!chartSeries.length) return null;
/* ---------- Pie chart ---------- */
if (chartType === "pie") {
const pieData =
series[0]?.data.map((value, i) => ({
chartSeries[0]?.data.map((value, i) => ({
name: xData?.[i] ?? `${i}`,
value,
})) ?? [];
@@ -111,7 +250,7 @@ export const ChatInlineChart: React.FC<ChatInlineChartProps> = ({
xData && xData.length > 20
? [{ type: "inside", start: 0, end: 100 }]
: undefined,
series: series.map((s, i) => {
series: chartSeries.map((s, i) => {
const color = COLORS[i % COLORS.length];
return {
name: s.name,
@@ -135,7 +274,7 @@ export const ChatInlineChart: React.FC<ChatInlineChartProps> = ({
}),
color: COLORS,
};
}, [chartType, xData, series, title, yAxisName, xAxisName]);
}, [chartType, xData, chartSeries, title, yAxisName, xAxisName]);
if (!option) {
return (