fix(chat): normalize chart tool data
This commit is contained in:
@@ -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}
|
||||
/>
|
||||
|
||||
@@ -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 }],
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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 (
|
||||
|
||||
Reference in New Issue
Block a user