feat: 优化分析中心图表生成&markdown内容显示

This commit is contained in:
Shu Guang 2025-04-19 01:46:42 +08:00
parent a191d88ef2
commit 7c2752d074
2 changed files with 287 additions and 204 deletions

View File

@ -85,6 +85,12 @@
border-radius: 12px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06);
:global {
.ant-list-item-meta {
width: 40vw !important;
}
}
.avatar {
background: #1890ff;
}
@ -92,6 +98,7 @@
.chartContainer {
margin-top: 16px;
padding: 16px;
width: 40vw;
background: #fff;
border-radius: 8px;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.04);

View File

@ -29,6 +29,7 @@ import type { UploadFile } from 'antd/es/upload/interface';
import ReactECharts from 'echarts-for-react';
import styles from './index.less';
import * as XLSX from 'xlsx';
import { marked } from 'marked';
const { Option } = Select;
const { Dragger } = Upload;
@ -107,79 +108,7 @@ const AnalysisCenter: React.FC = () => {
messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' });
};
const handleFileAnalysis = async (file: File) => {
try {
const reader = new FileReader();
reader.onload = async (e) => {
const data = e.target?.result;
let textContent = '';
if (file.name.toLowerCase().endsWith('.csv')) {
textContent = data as string;
} else {
const workbook = XLSX.read(data, { type: 'array' });
const firstSheet = workbook.Sheets[workbook.SheetNames[0]];
const jsonData = XLSX.utils.sheet_to_json(firstSheet, { header: 1 });
textContent = jsonData.map(row => row.join('\t')).join('\n');
}
const response = await fetch('https://aizex.top/v1/chat/completions', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': 'Bearer sk-Bp4AtAw19a6lENrPUQeqfiS9KP46Z5A43j4QkNeX4NRnGKMU'
},
body: JSON.stringify({
model: 'gpt-4o',
messages: [
{
role: 'user',
content: [
{
type: 'text',
text: `请对以下数据进行${analysisOptions.find(opt => opt.value === analysisType)?.label},并给出专业的分析见解:\n${textContent}`
}
]
}
],
max_tokens: 2000
})
});
const result = await response.json();
const userMessage: Message = {
type: 'user',
content: `已上传文件:${file.name}`,
timestamp: Date.now(),
};
const assistantMessage: Message = {
type: 'assistant',
content: result.choices[0].message.content,
timestamp: Date.now(),
charts: generateMockChart(analysisType),
};
setMessages(prev => [...prev, userMessage, assistantMessage]);
setLoading(false);
scrollToBottom();
};
if (file.name.toLowerCase().endsWith('.csv')) {
reader.readAsText(file);
} else {
reader.readAsArrayBuffer(file);
}
} catch (error) {
console.error('文件处理失败:', error);
message.error('文件处理失败');
setLoading(false);
}
};
const handleSend = async () => {
const handleSend = async () => {
if (!inputValue.trim()) return;
const userMessage: Message = {
@ -193,148 +122,295 @@ const AnalysisCenter: React.FC = () => {
setLoading(true);
try {
// 这里应该调用后端API进行分析
// const response = await analyzeData({ type: analysisType, message: inputValue });
const response = await fetch('https://aizex.top/v1/chat/completions', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': 'Bearer sk-Bp4AtAw19a6lENrPUQeqfiS9KP46Z5A43j4QkNeX4NRnGKMU'
},
body: JSON.stringify({
model: 'gpt-4o',
messages: [
{
role: 'system',
content: '你是一个数据分析专家,请根据用户输入进行分析并生成分析报告和 ECharts 图表配置。图表配置需要包含在 ```json 代码块中。'
},
{
role: 'user',
content: [
{
type: 'text',
text: `请对以下内容进行${analysisOptions.find(opt => opt.value === analysisType)?.label},并给出专业的分析见解。
ECharts 使 \`\`\`json 包裹),配置中需要包含:
1.
2. 线
3.
4.
5.
// 模拟API响应
setTimeout(() => {
const assistantMessage: Message = {
type: 'assistant',
content: generateMockResponse(analysisType),
timestamp: Date.now(),
charts: generateMockChart(analysisType),
};
setMessages(prev => [...prev, assistantMessage]);
setLoading(false);
scrollToBottom();
}, 1500);
${inputValue}`
}
]
}
],
max_tokens: 2000
})
});
const result = await response.json();
let chartOption;
try {
const matches = result.choices[0].message.content.match(/```json\n([\s\S]*?)\n```/);
if (matches && matches[1]) {
chartOption = JSON.parse(matches[1]);
}
} catch (error) {
console.error('解析图表配置失败:', error);
chartOption = generateMockChart(analysisType);
}
const assistantMessage: Message = {
type: 'assistant',
content: result.choices[0].message.content.replace(/```json\n[\s\S]*?\n```/g, '').trim(),
timestamp: Date.now(),
charts: chartOption,
};
setMessages(prev => [...prev, assistantMessage]);
setLoading(false);
scrollToBottom();
} catch (error) {
message.error('分析请求失败');
setLoading(false);
}
}
const handleFileAnalysis = async (file: File) => {
try {
const reader = new FileReader();
reader.onload = async (e) => {
const data = e.target?.result;
let textContent = '';
if (file.name.toLowerCase().endsWith('.csv')) {
textContent = data as string;
} else {
const workbook = XLSX.read(data, { type: 'array' });
const firstSheet = workbook.Sheets[workbook.SheetNames[0]];
const jsonData = XLSX.utils.sheet_to_json(firstSheet, { header: 1 });
textContent = jsonData.map(row => row.join('\t')).join('\n');
}
const response = await fetch('https://aizex.top/v1/chat/completions', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': 'Bearer sk-Bp4AtAw19a6lENrPUQeqfiS9KP46Z5A43j4QkNeX4NRnGKMU'
},
body: JSON.stringify({
model: 'gpt-4o',
messages: [
{
role: 'system',
content: '你是一个数据分析专家,请根据用户输入的数据进行分析并生成分析报告和 ECharts 图表配置。图表配置需要包含在 ```json 代码块中。'
},
{
role: 'user',
content: [
{
type: 'text',
text: `请对以下数据进行${analysisOptions.find(opt => opt.value === analysisType)?.label},并给出专业的分析见解。
ECharts 使 \`\`\`json 包裹),配置中需要包含:
1.
2. 线
3.
4.
5.
\n${textContent}`
}
]
}
],
max_tokens: 2000
})
});
const result = await response.json();
let chartOption;
try {
const matches = result.choices[0].message.content.match(/```json\n([\s\S]*?)\n```/);
if (matches && matches[1]) {
chartOption = JSON.parse(matches[1]);
}
} catch (error) {
console.error('解析图表配置失败:', error);
message.error('图表生成失败,使用默认图表');
chartOption = generateMockChart(analysisType);
}
const userMessage: Message = {
type: 'user',
content: `已上传文件:${file.name}`,
timestamp: Date.now(),
};
const assistantMessage: Message = {
type: 'assistant',
content: marked(result.choices[0].message.content
.replace(/```json\n[\s\S]*?\n```/g, '') // 先移除 JSON 代码块
.trim()
),
timestamp: Date.now(),
charts: chartOption,
};
setMessages(prev => [...prev, userMessage, assistantMessage]);
setLoading(false);
scrollToBottom();
};
if (file.name.toLowerCase().endsWith('.csv')) {
reader.readAsText(file);
} else {
reader.readAsArrayBuffer(file);
}
} catch (error) {
console.error('文件处理失败:', error);
message.error('文件处理失败');
setLoading(false);
}
};
const generateMockResponse = (type: string) => {
const responses: { [key: string]: string } = {
predictive: '根据历史数据分析预计未来三个月的销售增长率将达到15%,主要增长点来自新市场的开拓。',
descriptive: '数据集中包含1000条记录平均值为45.6标准差为12.3,分布呈现正态分布特征。',
anomaly: '检测到3个异常值点主要出现在数据的边缘区域建议进行进一步核实。',
quality: '数据完整性为98.5%,存在少量缺失值,建议对缺失数据进行适当的填充处理。',
const generateMockResponse = (type: string) => {
const responses: { [key: string]: string } = {
predictive: '根据历史数据分析预计未来三个月的销售增长率将达到15%,主要增长点来自新市场的开拓。',
descriptive: '数据集中包含1000条记录平均值为45.6标准差为12.3,分布呈现正态分布特征。',
anomaly: '检测到3个异常值点主要出现在数据的边缘区域建议进行进一步核实。',
quality: '数据完整性为98.5%,存在少量缺失值,建议对缺失数据进行适当的填充处理。',
};
return responses[type] || '分析完成';
};
return responses[type] || '分析完成';
return (
<PageContainer
className={styles.container}
title="智能预测分析"
subTitle="上传数据,获取专业的数据分析见解"
>
<Card bordered={false} className={styles.mainCard}>
<Space direction="vertical" style={{ width: '100%' }} size="large">
<div className={styles.analysisTypeSelector}>
{analysisOptions.map(option => (
<Tooltip key={option.value} title={option.label}>
<Tag
className={styles.analysisTag}
color={analysisType === option.value ? option.color : 'default'}
icon={option.icon}
onClick={() => setAnalysisType(option.value)}
>
{option.label}
</Tag>
</Tooltip>
))}
</div>
<Card className={styles.uploadCard}>
<Dragger
fileList={fileList}
onChange={({ fileList }) => setFileList(fileList)}
beforeUpload={(file) => {
const isExcelOrCsv = /\.(xlsx|xls|csv)$/.test(file.name.toLowerCase());
if (!isExcelOrCsv) {
message.error('只支持 Excel 或 CSV 文件!');
return false;
}
setFileList([file]);
setLoading(true);
handleFileAnalysis(file);
return false;
}}
>
<p className="ant-upload-drag-icon">
<InboxOutlined className={styles.uploadIcon} />
</p>
<p className="ant-upload-text"></p>
<p className="ant-upload-hint"> Excel (.xlsx, .xls) CSV </p>
</Dragger>
</Card>
<div className={styles.chatContainer}>
<List
className={styles.messageList}
itemLayout="horizontal"
dataSource={messages}
// 在 List 组件的 renderItem 部分修改
renderItem={item => (
<List.Item className={item.type === 'user' ? styles.userMessage : styles.assistantMessage}>
<Card className={styles.messageCard}>
<List.Item.Meta
avatar={
<Avatar
icon={item.type === 'user' ? <UserOutlined /> : <RobotOutlined />}
className={styles.avatar}
style={{ padding: '8px' }}
/>
}
title={item.type === 'user' ? '你' : 'AI 助手'}
description={
<div
style={{ whiteSpace: 'pre-wrap', wordBreak: 'break-all' }}
dangerouslySetInnerHTML={{ __html: item.content }}
/>
}
/>
{item.charts && (
<div className={styles.chartContainer}>
<ReactECharts option={item.charts} style={{ height: 300 }} />
</div>
)}
</Card>
</List.Item>
)}
/>
<div ref={messagesEndRef} />
</div>
<div className={styles.inputContainer}>
<TextArea
value={inputValue}
onChange={(e) => setInputValue(e.target.value)}
placeholder="请描述您的分析需求..."
autoSize={{ minRows: 2, maxRows: 6 }}
className={styles.input}
onPressEnter={(e) => {
if (!e.shiftKey) {
e.preventDefault();
handleSend();
}
}}
/>
<Button
type="primary"
icon={<SendOutlined />}
onClick={handleSend}
loading={loading}
className={styles.sendButton}
>
</Button>
</div>
</Space>
</Card>
</PageContainer>
);
};
return (
<PageContainer
className={styles.container}
title="智能预测分析"
subTitle="上传数据,获取专业的数据分析见解"
>
<Card bordered={false} className={styles.mainCard}>
<Space direction="vertical" style={{ width: '100%' }} size="large">
<div className={styles.analysisTypeSelector}>
{analysisOptions.map(option => (
<Tooltip key={option.value} title={option.label}>
<Tag
className={styles.analysisTag}
color={analysisType === option.value ? option.color : 'default'}
icon={option.icon}
onClick={() => setAnalysisType(option.value)}
>
{option.label}
</Tag>
</Tooltip>
))}
</div>
<Card className={styles.uploadCard}>
<Dragger
fileList={fileList}
onChange={({ fileList }) => setFileList(fileList)}
beforeUpload={(file) => {
const isExcelOrCsv = /\.(xlsx|xls|csv)$/.test(file.name.toLowerCase());
if (!isExcelOrCsv) {
message.error('只支持 Excel 或 CSV 文件!');
return false;
}
setFileList([file]);
setLoading(true);
handleFileAnalysis(file);
return false;
}}
>
<p className="ant-upload-drag-icon">
<InboxOutlined className={styles.uploadIcon} />
</p>
<p className="ant-upload-text"></p>
<p className="ant-upload-hint"> Excel (.xlsx, .xls) CSV </p>
</Dragger>
</Card>
<div className={styles.chatContainer}>
<List
className={styles.messageList}
itemLayout="horizontal"
dataSource={messages}
renderItem={item => (
<List.Item className={item.type === 'user' ? styles.userMessage : styles.assistantMessage}>
<Card className={styles.messageCard}>
<List.Item.Meta
avatar={
<Avatar
icon={item.type === 'user' ? <UserOutlined /> : <RobotOutlined />}
className={styles.avatar}
style={{ padding: '8px' }}
/>
}
title={item.type === 'user' ? '你' : 'AI 助手'}
description={
<div style={{ whiteSpace: 'pre-wrap', wordBreak: 'break-all' }}>
{item.content}
</div>
}
/>
{item.charts && (
<div className={styles.chartContainer}>
<ReactECharts option={item.charts} style={{ height: 300 }} />
</div>
)}
</Card>
</List.Item>
)}
/>
<div ref={messagesEndRef} />
</div>
<div className={styles.inputContainer}>
<TextArea
value={inputValue}
onChange={(e) => setInputValue(e.target.value)}
placeholder="请描述您的分析需求..."
autoSize={{ minRows: 2, maxRows: 6 }}
className={styles.input}
onPressEnter={(e) => {
if (!e.shiftKey) {
e.preventDefault();
handleSend();
}
}}
/>
<Button
type="primary"
icon={<SendOutlined />}
onClick={handleSend}
loading={loading}
className={styles.sendButton}
>
</Button>
</div>
</Space>
</Card>
</PageContainer>
);
};