feat: AI 对话支持 A2UI 表单/审查/图表与 Agent 工具

This commit is contained in:
2026-08-05 17:11:00 +08:00
parent 644c35ce53
commit 0e6e3e2d96
64 changed files with 8395 additions and 6434 deletions

View File

@@ -1,12 +1,14 @@
import React, { useEffect, useMemo, useRef, useState } from 'react';
import { XCard, registerCatalog } from '@ant-design/x-card';
import type { XAgentCommand_v0_9 } from '@ant-design/x-card';
import { Button, Tag, Tooltip, Typography } from 'antd';
import React, { lazy, Suspense, useEffect, useMemo, useRef, useState } from 'react';
import { XCard, registerCatalog, type XAgentCommand_v0_9 } from '@ant-design/x-card';
import { Button, Spin, Tag, Tooltip, Typography } from 'antd';
import { DownloadOutlined } from '@ant-design/icons';
import type { EChartsType } from 'echarts/core';
import ReactECharts, { type EChartsOption } from '../../components/ECharts';
import type { EChartsOption } from '../../components/ECharts';
import type { AiChartSchema } from './types';
// echarts 体积较大,仅在真正渲染图表时加载,避免打开 AI 抽屉就拉取
const ReactECharts = lazy(() => import('../../components/ECharts'));
const CHART_CATALOG_ID = 'gongxue-chart-catalog';
registerCatalog({
@@ -41,117 +43,129 @@ const CHART_TYPE_LABELS: Record<string, string> = {
funnel: '漏斗图',
};
function buildOption(chart: AiChartSchema): EChartsOption {
function buildNameValueRows(chart: AiChartSchema): { name: string; value: number }[] {
const nameField = chart.columns[0]?.key ?? '';
const valueField = chart.columns[1]?.key ?? '';
return chart.rows.map((row) => ({
name: String(row[nameField] ?? ''),
value: numberValue(row[valueField]),
}));
}
function buildScatterOption(chart: AiChartSchema): EChartsOption {
const columns = chart.columns;
if (chart.chartType === 'scatter') {
const nameField = columns[0]?.key ?? '';
const xField = columns[1]?.key ?? '';
const yField = columns[2]?.key ?? '';
const data = chart.rows.map((row) => ({
name: String(row[nameField] ?? ''),
value: [numberValue(row[xField]), numberValue(row[yField])],
}));
return {
tooltip: {
trigger: 'item',
formatter: (params: unknown) => {
const item = params as { name?: string; value?: number[] };
const [x, y] = item.value ?? [];
return `${item.name ?? ''}: (${x ?? 0}, ${y ?? 0})`;
},
const nameField = columns[0]?.key ?? '';
const xField = columns[1]?.key ?? '';
const yField = columns[2]?.key ?? '';
const data = chart.rows.map((row) => ({
name: String(row[nameField] ?? ''),
value: [numberValue(row[xField]), numberValue(row[yField])],
}));
return {
tooltip: {
trigger: 'item',
formatter: (params: unknown) => {
const item = params as { name?: string; value?: number[] };
const [x, y] = item.value ?? [];
return `${item.name ?? ''}: (${x ?? 0}, ${y ?? 0})`;
},
grid: { left: 8, right: 16, top: 32, bottom: 48, containLabel: true },
xAxis: { type: 'value', name: columns[1]?.title },
yAxis: { type: 'value', name: columns[2]?.title },
series: [{ type: 'scatter', symbolSize: 10, data }],
};
}
if (chart.chartType === 'radar') {
const seriesNameField = columns[0]?.key ?? '';
const indicatorColumns = columns.slice(1);
const indicators = indicatorColumns.map((column) => {
const values = chart.rows.map((row) => numberValue(row[column.key]));
const max = Math.max(1, ...values);
return { name: column.title, max: Math.ceil(max * 1.1) };
});
const seriesData = chart.rows.map((row) => ({
name: String(row[seriesNameField] ?? ''),
value: indicatorColumns.map((column) => numberValue(row[column.key])),
}));
return {
tooltip: { trigger: 'item' },
legend: { bottom: 0, type: 'scroll' },
radar: { indicator: indicators, radius: '65%' },
series: [{ type: 'radar', data: seriesData }],
};
}
if (chart.chartType === 'gauge') {
const nameField = columns[0]?.key ?? '';
const valueField = columns[1]?.key ?? '';
const maxField = columns[2]?.key;
const gauges = chart.rows.map((row) => ({
name: String(row[nameField] ?? ''),
value: numberValue(row[valueField]),
max: maxField ? Math.max(1, numberValue(row[maxField])) : 100,
}));
return {
series: gauges.map((gauge, index) => ({
type: 'gauge',
center: [`${((index + 0.5) * 100) / gauges.length}%`, '58%'],
radius: '75%',
min: 0,
max: gauge.max,
title: { show: true, offsetCenter: [0, '82%'], fontSize: 12 },
detail: { formatter: '{value}', fontSize: 14, offsetCenter: [0, '20%'] },
data: [{ value: gauge.value, name: gauge.name }],
})),
};
}
if (chart.chartType === 'funnel') {
const nameField = columns[0]?.key ?? '';
const valueField = columns[1]?.key ?? '';
const data = chart.rows.map((row) => ({
name: String(row[nameField] ?? ''),
value: numberValue(row[valueField]),
}));
return {
tooltip: { trigger: 'item', formatter: '{b}: {c}' },
legend: { bottom: 0, type: 'scroll' },
series: [
{
type: 'funnel',
left: '10%',
top: 20,
bottom: 40,
width: '80%',
minSize: '20%',
label: { formatter: '{b}: {c}' },
data,
},
],
};
}
if (chart.chartType === 'pie') {
const nameField = columns[0]?.key ?? '';
const valueField = columns[1]?.key ?? '';
const data = chart.rows.map((row) => ({
name: String(row[nameField] ?? ''),
value: numberValue(row[valueField]),
}));
return {
tooltip: { trigger: 'item' },
legend: { bottom: 0, type: 'scroll' },
series: [
{
type: 'pie',
radius: ['35%', '68%'],
center: ['50%', '45%'],
data,
label: { formatter: '{b}: {c}' },
},
],
};
}
},
grid: { left: 8, right: 16, top: 32, bottom: 48, containLabel: true },
xAxis: { type: 'value', name: columns[1]?.title },
yAxis: { type: 'value', name: columns[2]?.title },
series: [{ type: 'scatter', symbolSize: 10, data }],
};
}
function buildRadarOption(chart: AiChartSchema): EChartsOption {
const columns = chart.columns;
const seriesNameField = columns[0]?.key ?? '';
const indicatorColumns = columns.slice(1);
const indicators = indicatorColumns.map((column) => {
const values = chart.rows.map((row) => numberValue(row[column.key]));
const max = Math.max(1, ...values);
return { name: column.title, max: Math.ceil(max * 1.1) };
});
const seriesData = chart.rows.map((row) => ({
name: String(row[seriesNameField] ?? ''),
value: indicatorColumns.map((column) => numberValue(row[column.key])),
}));
return {
tooltip: { trigger: 'item' },
legend: { bottom: 0, type: 'scroll' },
radar: { indicator: indicators, radius: '65%' },
series: [{ type: 'radar', data: seriesData }],
};
}
function buildGaugeOption(chart: AiChartSchema): EChartsOption {
const columns = chart.columns;
const nameField = columns[0]?.key ?? '';
const valueField = columns[1]?.key ?? '';
const maxField = columns[2]?.key;
const gauges = chart.rows.map((row) => ({
name: String(row[nameField] ?? ''),
value: numberValue(row[valueField]),
max: maxField ? Math.max(1, numberValue(row[maxField])) : 100,
}));
return {
series: gauges.map((gauge, index) => ({
type: 'gauge',
center: [`${((index + 0.5) * 100) / gauges.length}%`, '58%'],
radius: '75%',
min: 0,
max: gauge.max,
title: { show: true, offsetCenter: [0, '82%'], fontSize: 12 },
detail: { formatter: '{value}', fontSize: 14, offsetCenter: [0, '20%'] },
data: [{ value: gauge.value, name: gauge.name }],
})),
};
}
function buildNameValueOption(chart: AiChartSchema): EChartsOption {
const data = buildNameValueRows(chart);
return chart.chartType === 'funnel'
? {
tooltip: { trigger: 'item', formatter: '{b}: {c}' },
legend: { bottom: 0, type: 'scroll' },
series: [
{
type: 'funnel',
left: '10%',
top: 20,
bottom: 40,
width: '80%',
minSize: '20%',
label: { formatter: '{b}: {c}' },
data,
},
],
}
: {
tooltip: { trigger: 'item' },
legend: { bottom: 0, type: 'scroll' },
series: [
{
type: 'pie',
radius: ['35%', '68%'],
center: ['50%', '45%'],
data,
label: { formatter: '{b}: {c}' },
},
],
};
}
function buildOption(chart: AiChartSchema): EChartsOption {
if (chart.chartType === 'scatter') return buildScatterOption(chart);
if (chart.chartType === 'radar') return buildRadarOption(chart);
if (chart.chartType === 'gauge') return buildGaugeOption(chart);
if (chart.chartType === 'funnel' || chart.chartType === 'pie') return buildNameValueOption(chart);
return buildCategoryOption(chart);
}
function buildCategoryOption(chart: AiChartSchema): EChartsOption {
const columns = chart.columns;
const categoryField = columns[0]?.key ?? '';
const categories = chart.rows.map((row) => String(row[categoryField] ?? ''));
const series = columns.slice(1).map((column) => ({
@@ -223,11 +237,9 @@ const ChartPreview: React.FC<ChartPreviewProps> = ({ chart }) => {
</Tooltip>
</span>
</div>
<ReactECharts
option={option}
style={{ width: '100%', height: 260 }}
onReady={setInstance}
/>
<Suspense fallback={<Spin size="small" />}>
<ReactECharts option={option} style={{ width: '100%', height: 260 }} onReady={setInstance} />
</Suspense>
</div>
);
};
@@ -291,5 +303,3 @@ export const DynamicChart: React.FC<DynamicChartProps> = ({ chart }) => {
</div>
);
};
export default DynamicChart;