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 BuildCircleRounded from "@mui/icons-material/BuildCircleRounded";
|
||||||
|
|
||||||
import { ChatInlineChart } from "./ChatInlineChart";
|
import { ChatInlineChart } from "./ChatInlineChart";
|
||||||
import type { ChatChartSeries } from "./ChatInlineChart";
|
|
||||||
import type { AgentArtifact } from "./GlobalChatbox.types";
|
import type { AgentArtifact } from "./GlobalChatbox.types";
|
||||||
|
|
||||||
const artifactIcon = (kind: AgentArtifact["kind"]) => {
|
const artifactIcon = (kind: AgentArtifact["kind"]) => {
|
||||||
@@ -61,8 +60,13 @@ export const AgentArtifactPanel = ({ artifacts }: { artifacts: AgentArtifact[] }
|
|||||||
chart_type={
|
chart_type={
|
||||||
(artifact.params.chart_type as "line" | "bar" | "pie") ?? "line"
|
(artifact.params.chart_type as "line" | "bar" | "pie") ?? "line"
|
||||||
}
|
}
|
||||||
x_data={(artifact.params.x_data as string[]) ?? []}
|
x_data={
|
||||||
series={(artifact.params.series as ChatChartSeries[]) ?? []}
|
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}
|
x_axis_name={(artifact.params.x_axis_name as string) ?? undefined}
|
||||||
y_axis_name={(artifact.params.y_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 { stripMarkdown } from "./GlobalChatbox.utils";
|
||||||
import { AgentProgressTimeline } from "./AgentProgressTimeline";
|
import { AgentProgressTimeline } from "./AgentProgressTimeline";
|
||||||
import { ChatInlineChart } from "./ChatInlineChart";
|
import { ChatInlineChart } from "./ChatInlineChart";
|
||||||
import type { ChatChartSeries } from "./ChatInlineChart";
|
|
||||||
import { ChatToolCallBlock } from "./ChatToolCallBlock";
|
import { ChatToolCallBlock } from "./ChatToolCallBlock";
|
||||||
import { MarkdownBlock, normalizeClipboardText } from "./AgentMarkdownBlock";
|
import { MarkdownBlock, normalizeClipboardText } from "./AgentMarkdownBlock";
|
||||||
import { PermissionRequestGroup } from "./AgentPermissionRequests";
|
import { PermissionRequestGroup } from "./AgentPermissionRequests";
|
||||||
@@ -251,8 +250,8 @@ export const AgentTurn = React.memo(
|
|||||||
chart_type={
|
chart_type={
|
||||||
(p.chart_type as "line" | "bar" | "pie") ?? "line"
|
(p.chart_type as "line" | "bar" | "pie") ?? "line"
|
||||||
}
|
}
|
||||||
x_data={(p.x_data as string[]) ?? []}
|
x_data={p.x_data ?? p.xData ?? p.labels ?? p.categories}
|
||||||
series={(p.series as ChatChartSeries[]) ?? []}
|
series={p.series}
|
||||||
x_axis_name={(p.x_axis_name as string) ?? undefined}
|
x_axis_name={(p.x_axis_name as string) ?? undefined}
|
||||||
y_axis_name={(p.y_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?: "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 {
|
export interface ChatInlineChartProps {
|
||||||
title?: string;
|
title?: string;
|
||||||
chart_type?: "line" | "bar" | "pie";
|
chart_type?: "line" | "bar" | "pie";
|
||||||
x_data?: string[];
|
x_data?: unknown;
|
||||||
series?: ChatChartSeries[];
|
series?: unknown;
|
||||||
y_axis_name?: string;
|
y_axis_name?: string;
|
||||||
x_axis_name?: string;
|
x_axis_name?: string;
|
||||||
}
|
}
|
||||||
@@ -37,23 +51,148 @@ const COLORS = [
|
|||||||
"#ea7ccc",
|
"#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> = ({
|
export const ChatInlineChart: React.FC<ChatInlineChartProps> = ({
|
||||||
title,
|
title,
|
||||||
chart_type: chartType = "line",
|
chart_type: chartType = "line",
|
||||||
x_data: xData,
|
x_data,
|
||||||
series = [],
|
series,
|
||||||
y_axis_name: yAxisName,
|
y_axis_name: yAxisName,
|
||||||
x_axis_name: xAxisName,
|
x_axis_name: xAxisName,
|
||||||
}) => {
|
}) => {
|
||||||
const theme = useTheme();
|
const theme = useTheme();
|
||||||
|
const { xData, series: chartSeries } = useMemo(
|
||||||
|
() => normalizeChartData(x_data, series),
|
||||||
|
[x_data, series],
|
||||||
|
);
|
||||||
|
|
||||||
const option = useMemo(() => {
|
const option = useMemo(() => {
|
||||||
if (!series.length) return null;
|
if (!chartSeries.length) return null;
|
||||||
|
|
||||||
/* ---------- Pie chart ---------- */
|
/* ---------- Pie chart ---------- */
|
||||||
if (chartType === "pie") {
|
if (chartType === "pie") {
|
||||||
const pieData =
|
const pieData =
|
||||||
series[0]?.data.map((value, i) => ({
|
chartSeries[0]?.data.map((value, i) => ({
|
||||||
name: xData?.[i] ?? `${i}`,
|
name: xData?.[i] ?? `${i}`,
|
||||||
value,
|
value,
|
||||||
})) ?? [];
|
})) ?? [];
|
||||||
@@ -111,7 +250,7 @@ export const ChatInlineChart: React.FC<ChatInlineChartProps> = ({
|
|||||||
xData && xData.length > 20
|
xData && xData.length > 20
|
||||||
? [{ type: "inside", start: 0, end: 100 }]
|
? [{ type: "inside", start: 0, end: 100 }]
|
||||||
: undefined,
|
: undefined,
|
||||||
series: series.map((s, i) => {
|
series: chartSeries.map((s, i) => {
|
||||||
const color = COLORS[i % COLORS.length];
|
const color = COLORS[i % COLORS.length];
|
||||||
return {
|
return {
|
||||||
name: s.name,
|
name: s.name,
|
||||||
@@ -135,7 +274,7 @@ export const ChatInlineChart: React.FC<ChatInlineChartProps> = ({
|
|||||||
}),
|
}),
|
||||||
color: COLORS,
|
color: COLORS,
|
||||||
};
|
};
|
||||||
}, [chartType, xData, series, title, yAxisName, xAxisName]);
|
}, [chartType, xData, chartSeries, title, yAxisName, xAxisName]);
|
||||||
|
|
||||||
if (!option) {
|
if (!option) {
|
||||||
return (
|
return (
|
||||||
|
|||||||
Reference in New Issue
Block a user