跳至主要內容

BI 智能分析业务

holic-x...大约 22 分钟项目bi-platform

智能分析业务

需求分析

业务流程分析

【1】用户输入

  • 分析目标
  • 上传原始数据(excel)
  • 更精细化地控制图表:比如图表类型、图表名称等

【2】后端校验

  • 校验用户的输入否合法(此如长度)
  • 成本控制(次数统计和校验、鉴权等)

【3】把处理后的数据输入给Al模型(调用Al接口),让AI模型给系统提供图表信息、结论文本

【4】图表信息(是一段json配置、是一段代码)、结论文本在前端进行展示

接口开发

根据用户的输入(文本和文件),最后返回图表信息和结论文本

​ 需要在接收用户上传的excel文件时,在请求的参数中对接文件,能够接收到文件(可以仿照模板中文件上传FileController的接口进行编写)

image-20240417224659270

​ 将model/dto/file/UploadFileRequest复制到model/dto/chart/重命名为GenChartByAiRequest.java,此处GenChartByAiRequest字段设计可以参考图表信息表

/**
 * 文件上传请求
 */
@Data
public class GenChartByAiRequest implements Serializable {

    /**
     * 名称
     */
    private String name;

    /**
     * 分析目标
     */
    private String goal;

    /**
     * 图表类型
     */
    private String chartType;

    private static final long serialVersionUID = 1L;
}

原始数据压缩

ChartController文件上传分析

/**
     * 智能分析(同步)
     *
     * @param multipartFile
     * @param genChartByAiRequest
     * @param request
     * @return
     */
    @PostMapping("/upload")
    public BaseResponse<String> genChartByAi(@RequestPart("file") MultipartFile multipartFile, GenChartByAiRequest genChartByAiRequest, HttpServletRequest request) {
        String name = genChartByAiRequest.getName();
        String goal = genChartByAiRequest.getGoal();
        String chartType = genChartByAiRequest.getChartType();

        // 校验
        ThrowUtils.throwIf(StringUtils.isBlank(goal), ErrorCode.PARAMS_ERROR, "目标为空");
        ThrowUtils.throwIf(StringUtils.isNotBlank(name) && name.length() > 100, ErrorCode.PARAMS_ERROR, "名称过长");

        // 读取用户上传的excel文件,进行处理
        User loginUser = userService.getLoginUser(request);
        // 文件目录:根据业务、用户划分
        String uuid = RandomStringUtils.randomAlphanumeric(8);
        String filename = uuid + "-" + multipartFile.getOriginalFilename();
        File file = null;
        try {
            // 返回可访问地址
            return ResultUtils.success("");
        } catch (Exception e) {
//            log.error("file upload error, filepath = " + filepath, e);
            throw new BusinessException(ErrorCode.SYSTEM_ERROR, "上传失败");
        } finally {
            if (file != null) {
                // 删除临时文件
                boolean delete = file.delete();
                if (!delete) {
//                    log.error("file delete error, filepath = {}", filepath);
                }
            }
        }
    }

​ 读取到excel文件,需要进行处理。AI接口普遍都有输入字数限制,因此要尽可能压缩数据,使得允许多传一些数据。ExcelUtils设计如下(读取excel文件需要转化为csv:参考ExcelUtils实现)

/**
 * Excel 相关工具类
 */
@Slf4j
public class ExcelUtils {

    /**
     * excel 转 csv
     *
     * @param multipartFile
     * @return
     */
    public static String excelToCsv(MultipartFile multipartFile) {
//        File file = null;
//        try {
//            file = ResourceUtils.getFile("classpath:网站数据.xlsx");
//        } catch (FileNotFoundException e) {
//            e.printStackTrace();
//        }
        // 读取数据
        List<Map<Integer, String>> list = null;
        try {
            list = EasyExcel.read(multipartFile.getInputStream())
                    .excelType(ExcelTypeEnum.XLSX)
                    .sheet()
                    .headRowNumber(0)
                    .doReadSync();
        } catch (IOException e) {
            log.error("表格处理错误", e);
        }
        if (CollUtil.isEmpty(list)) {
            return "";
        }
        // 转换为 csv
        StringBuilder stringBuilder = new StringBuilder();
        // 读取表头
        LinkedHashMap<Integer, String> headerMap = (LinkedHashMap) list.get(0);
        List<String> headerList = headerMap.values().stream().filter(ObjectUtils::isNotEmpty).collect(Collectors.toList());
        stringBuilder.append(StringUtils.join(headerList, ",")).append("\n");
        // 读取数据
        for (int i = 1; i < list.size(); i++) {
            LinkedHashMap<Integer, String> dataMap = (LinkedHashMap) list.get(i);
            List<String> dataList = dataMap.values().stream().filter(ObjectUtils::isNotEmpty).collect(Collectors.toList());
            stringBuilder.append(StringUtils.join(dataList, ",")).append("\n");
        }
        return stringBuilder.toString();
    }

    public static void main(String[] args) {
        excelToCsv(null);
    }
}

文件上传压缩接口测试

		@PostMapping("/upload")
    public BaseResponse<String> genChartByAi(@RequestPart("file") MultipartFile multipartFile, GenChartByAiRequest genChartByAiRequest, HttpServletRequest request) {
        String name = genChartByAiRequest.getName();
        String goal = genChartByAiRequest.getGoal();
        String chartType = genChartByAiRequest.getChartType();

        // 校验
        ThrowUtils.throwIf(StringUtils.isBlank(goal), ErrorCode.PARAMS_ERROR, "目标为空");
        ThrowUtils.throwIf(StringUtils.isNotBlank(name) && name.length() > 100, ErrorCode.PARAMS_ERROR, "名称过长");

        // 测试读取文件
        String result = ExcelUtils.excelToCsv(multipartFile);
        return ResultUtils.success(result);
    }

​ 本地测试:启动项目访问接口文档测试

image-20240417231232953

AI调用

如何使用AI生成结论、图表?

​ 将数据给到AI,然后拼接用户的分析目标

输入:
系统预设(提前告诉他职责、功能回复格式要求) +分析目标+压缩后的数据
  
最简单的系统预设:
你是一个数据分析师,接下来我会给你我的分析目标和原始数据,请告诉我分析结论。
  
AI提词技巧3:
在系统(模型)层面做预设效果一般来说,蚍直接拼接在用户消息里效果更好一些。
  
AI提词技巧4:
除了系统预设外,额外关联一问一答两条消息,相当于给A1一个提示。

参考AI接口open in new window,此处将预设文本传入,查看AI响应效果

​ 将excel解析的数据给AI,然后拼接用户的分析目标。将系统预设目标以文本方式拼接,然后调用AI

​ 调整ChartController实现压缩文本拼接,将拼接文本用于后续和AI接口对接

	 /**
     * 智能分析(同步)
     *
     * @param multipartFile
     * @param genChartByAiRequest
     * @param request
     * @return
     */
    @PostMapping("/upload")
    public BaseResponse<String> genChartByAi(@RequestPart("file") MultipartFile multipartFile, GenChartByAiRequest genChartByAiRequest, HttpServletRequest request) {
        String name = genChartByAiRequest.getName();
        String goal = genChartByAiRequest.getGoal();
        String chartType = genChartByAiRequest.getChartType();

        // 校验
        ThrowUtils.throwIf(StringUtils.isBlank(goal), ErrorCode.PARAMS_ERROR, "目标为空");
        ThrowUtils.throwIf(StringUtils.isNotBlank(name) && name.length() > 100, ErrorCode.PARAMS_ERROR, "名称过长");

        // 用户输入
        StringBuilder userInput = new StringBuilder();
        userInput.append("你是一个数据分析师,接下来我会给你我的分析目标和原始数据,请告诉我分析结论。");
        userInput.append("分析目标:").append(goal).append("\n");

        // 压缩后的数据
        String result = ExcelUtils.excelToCsv(multipartFile);
        userInput.append("数据:").append(result).append("\n");
        return ResultUtils.success(userInput.toString());
    }

​ 启动后台访问接口文档,测试响应结果如下

{
  "code": 0,
  "data": "你是一个数据分析师,接下来我会给你我的分析目标和原始数据,请告诉我分析结论。分析目标:分析网站用户\n数据:日期,用户数\n1号,10\n2号,20\n3号,30\n4号,90\n5号,0\n6号,10\n7号,20\n\n",
  "message": "ok"
}

AI预设问答效果:

image-20240418205409021

后端开发

1.三种AI数据分析应用方式

AI生成结论

示例问法:

你是一个数据分析师,请帮我分析网站用户的增长趋势
原始数据如下:
日期,用户数
1,10
2,20
3,30

AI生成图表

​ AI虽然无法生成现成的图表,但是可以生成图表代码,利用前端组件库(Echarts)将生成代码进行渲染展示(Echarts在线展示open in new window

预期生成代码参考:

option = {
  xAxis: {
    type: 'category',
    data: ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun']
  },
  yAxis: {
    type: 'value'
  },
  series: [
    {
      data: [150, 230, 224, 218, 135, 147, 260],
      type: 'line'
    }
  ]
};

image-20240418205840523

AI提问技巧

提问技巧参考

​ 为了让AI更好地理解输入,给出预期精确的输出,需要严格控制提问词

【1】使用系统预设+控制输入格式(便于AI精确理解需求)

  • 助手设定
你是一个数据分析师和前端开发专家,接下来我会按照以下固定格式给你提供内容:
分析需求:
{数据分析的需求或者目标}
原始数据:
{csv格式的原始数据,用,作为分隔符}
请根据以上内容,帮我生成数据分析结论和可视化图表代码
  • 用户提问
分析需求:
分析网站用户的增长情况
原始数据:
日期,用户数
1,10
2,20
3,30

【2】控制输出格式(便于AI返回内容能够更好地为系统所用)

  • Prompt预设
你是一个数据分析师和前端开发专家,接下来我会按照以下固定格式给你提供内容:
分析需求:
{数据分析的需求或者目标}
原始数据:
{csv格式的原始数据,用,作为分隔符}
请根据这两部分内容,按照以下指定格式生成内容(此外不要输出任何多余的开头、结尾、注释)
【【【【【
{前端Echarts V5的option 配置对象js代码,合理地将数据进行可视化,不要生成任何多余的内容,比如注释
【【【【【
{明确的数据分析结论、越详细越好,不要生成多余的注释}
  • 生成内容
【【【【【
{
	title: {
    text: '网站用户增 长情况',
    subtext :
  },
  tooltip: {
    trigger: ' axis',
    axisPointer: {
      type:‘shadow
    },
  legend: {
    data: ['用户数']
    xAxis: {
      data: ['1号,'2号,,'3号']
    },
  yAxis: {},
  series: [{
  name :
    '用户数',
    type: 'bar',
    data: [10, 2030]
  }]
}
【【【【【
根据数据分析可得,该网站用户数量逐日增长,时间越长,用户数量增长越多。
  • 指定一个示例问答,one-shot或者few-shot

one-shot:给AI一轮示例问答

few-shot:给AI多轮示例问答

AI助手提问示例

​ 创建AI助手open in new window

image-20240418210901623

image-20240418211258599

创建完成,可在【我的助手】模块查看助手信息

image-20240418211357106

【1】点击AI助手,对话框输入【用户提问】
分析需求:
分析网站用户的增长情况
原始数据:
日期,用户数
1,10
2,20
3,30

image-20240418211610858

【2】控制输出格式

​ 编辑助手信息,修改为【2】中的Prompt预设

image-20240418211913902

​ 还是问同一个问题,确认问答信息

image-20240418212101811

【【【【【
{
"title": {
"text": "用户增长情况"
},
"tooltip": {
"trigger": "axis"
},
"legend": {
"data": ["用户数"]
},
"grid": {
"left": "3%",
"right": "4%",
"bottom": "3%",
"containLabel": true
},
"toolbox": {
"feature": {
"saveAsImage": {}
}
},
"xAxis": {
"type": "category",
"boundaryGap": false,
"data": ["1号", "2号", "3号"]
},
"yAxis": {
"type": "value"
},
"series": [
{
"name": "用户数",
"type": "line",
"stack": "总量",
"data": [10, 20, 30]
}
]
}
【【【【【
根据提供的数据,我们可以得出以下结论:

从1号到3号,网站用户数呈持续增长趋势,从10增加到30,增长了200%。
每天的用户增长数是:1号到2号增长了10个用户,2号到3号增长了10个用户。
如果这个增长趋势持续,可以预期未来网站用户数将继续上升。需要进一步关注用户增长的速度以及可能影响增长的因素,以制定相应的市场策略。
【3】指定示例问答

修改模型信息,点击【高级设置】=》【导入示例】=【更新AI助手】

【用户】:将【1】中的提问填充进去

【助手】:将【2】中的回答填充进去

image-20240418212606199

然后保存,继续问同一个问题

image-20240418212652543

【【【【【
{
"tooltip": {
"trigger": 'axis'
},
"legend": {
"data": ['用户数']
},
"xAxis": {
"type": 'category',
"data": ['1号', '2号', '3号']
},
"yAxis": {
"type": 'value'
},
"series": [
{
"name": '用户数',
"type": 'line',
"data": [10, 20, 30]
}
]
}
【【【【【
根据提供的数据,我们可以得出以下结论:

网站用户数从1号到3号呈现线性增长,每天增长10个用户。
在这三天内,用户数从10增长到30,增长了200%。
平均每日增长率约为66.67%。
如果这种增长趋势持续下去,可以预见网站用户数将稳步上升。

2.三种调用AI的方式

​ 如果在程序中想传递参数给AI,并且得到AI返回值,则可通过调用接口实现

(1)调用官方接口

​ 比如OpenAI或者其他Al原始大模型官网的接口官方文档open in new window

  • 优点:不经封装,最灵活,最原始

  • 缺点:要钱、要魔法

    本质上OpenAI就是提供了HTTP接口,可以用任何语言去调用

调用步骤参考

【1】在请求头指定OPEN_API_KEY => Authorization:Bearer OPENAI_API_KEY,否则提示无权限调用

【2】找到要使用的接口(例如AI对话接口open in new window

【3】按照接口文档示例,构造HTTP请求(借助hutool工具类、httpclient工具类)

public class OpenAiApi {
    public static void main(String[] args) {

        /**
         * AI 对话(需要自己创建请求响应对象)
         * @param request
         * @param openAiApiKey
         * @return
         */
        public CreateChatCompletionResponse createChatCompletion(CreateChatCompletionRequest request, String openAiApiKey) {
            if (StringUtils.isBlank(openAiApiKey)) {
                throw new BusinessException(ErrorCode.PARAMS_ERROR, "未传 openAiApiKey");
            }
            String url = "https://api.openai.com/v1/chat/completions";
            String json = JSONUtil.toJsonStr(request);
            String result = HttpRequest.post(url)
                    .header("Authorization", "Bearer " + openAiApiKey)
                    .body(json)
                    .execute()
                    .body();
            return JSONUtil.toBean(result, CreateChatCompletionResponse.class);
        }
    }
}

​ 然后获取到响应数据

(2)使用云服务商提供的封装接口

例如:Azure云(微软官方)

  • 优点:本地可用
  • 缺点:要钱,比直接调用原始接口更贵

(3)鱼AI

  • 优点:一定额度免费,有很多现成的模型(prompt系统预设)
  • 缺点:不完全灵活,可自定义模型

​ 鼠标移动到头像=》开放平台=》得到开发者秘钥,下载SDK(SDK项目文档open in new window

SDK依赖配置

<dependency>
    <groupId>com.yucongming</groupId>
    <artifactId>yucongming-java-sdk</artifactId>
    <version>0.0.3</version>
</dependency>

构建YuCongMingClient对象(配置application.yml)

# 鱼聪明 AI 配置(https://yucongming.com/)
yuapi:
  client:
    access-key: 替换为你自己的 access-key
    secret-key: 替换为你自己的 secret-key

在manager包下创建AiManager.java(对接第三方接口)

@Service
public class AiManager {
    @Resource
    private YuCongMingClient yuCongMingClient;

    /**
     * AI 对话
     * @param modelId
     * @param message
     * @return
     */
    public String doChat(long modelId, String message) {
        DevChatRequest devChatRequest = new DevChatRequest();
        devChatRequest.setModelId(modelId);
        devChatRequest.setMessage(message);
        BaseResponse<DevChatResponse> response = yuCongMingClient.doChat(devChatRequest);
        if (response == null) {
            throw new BusinessException(ErrorCode.SYSTEM_ERROR, "AI 响应错误");
        }
        return response.getData().getContent();
    }
}

​ 分享助手,然后选择复制链接(我发现了一个 AI 对话助手,点击链接进行聊天:https://www.yucongming.com/model/1780947646559985666?inviteUser=1765656249917460482),从连接中获取到模型id(1780947646559985666)

image-20240418214545735

方法名: doChat
请求参数:
  modelId:使用的会话模型id
  message :要发送的消息,不超过1024 字
响应结果:
  code :响应状态码
  data :
    content:对话结果内容
  message :响应信息

image-20240418215019617

image-20240418215200204

测试类构建

@SpringBootTest
class AiManagerTest {

    @Resource
    private AiManager aiManager;

    @Test
    void doChat() {
        String answer = aiManager.doChat(1780947646559985666L, "分析需求:\n" +
                "分析网站用户的增长情况\n" +
                "原始数据:\n" +
                "日期,用户数\n" +
                "1号,10\n" +
                "2号,20\n" +
                "3号,30");
        System.out.println(answer);
    }
}

image-20240418220016964

3.业务逻辑编写

​ 创建model/vo/BiResponse(Bi返回结果定义)

@Data
public class BiResponse {

    private String genChart;

    private String genResult;

    private Long chartId;
}

​ 优化ChartController中的实现、CommonConstant(定义ModelId)

public interface CommonConstant {
    /**
     * AI modelId
     */
    long BI_MODEL_ID = 1780947646559985666L;
}
		@PostMapping("/upload")
    public BaseResponse<BiResponse> genChartByAi(@RequestPart("file") MultipartFile multipartFile, GenChartByAiRequest genChartByAiRequest, HttpServletRequest request) {
        String name = genChartByAiRequest.getName();
        String goal = genChartByAiRequest.getGoal();
        String chartType = genChartByAiRequest.getChartType();

        // 校验
        ThrowUtils.throwIf(StringUtils.isBlank(goal), ErrorCode.PARAMS_ERROR, "目标为空");
        ThrowUtils.throwIf(StringUtils.isNotBlank(name) && name.length() > 100, ErrorCode.PARAMS_ERROR, "名称过长");

        User loginUser = userService.getLoginUser(request);
        long biModelId = CommonConstant.BI_MODEL_ID;

        // 构造用户输入
        StringBuilder userInput = new StringBuilder();
        userInput.append("分析需求:").append("\n");

        // 拼接分析目标
        String userGoal = goal;
        if (StringUtils.isNotBlank(chartType)) {
            userGoal += ",请使用" + chartType;
        }
        userInput.append(userGoal).append("\n");
        userInput.append("原始数据:").append("\n");
        // 压缩后的数据
        String csvData = ExcelUtils.excelToCsv(multipartFile);
        userInput.append(csvData).append("\n");

        String result = aiManager.doChat(biModelId, userInput.toString());
        // 此处分隔符以设定为参考
        String[] splits = result.split("【【【【【"); // 【【【【【
        if (splits.length < 3) {
            throw new BusinessException(ErrorCode.SYSTEM_ERROR, "AI 生成错误");
        }
        String genChart = splits[1].trim();
        String genResult = splits[2].trim();
        // 插入到数据库
        Chart chart = new Chart();
        chart.setName(name);
        chart.setGoal(goal);
        chart.setChartData(csvData);
        chart.setChartType(chartType);
        chart.setGenChart(genChart);
        chart.setGenResult(genResult);
        chart.setUserId(loginUser.getId());
        boolean saveResult = chartService.save(chart);
        ThrowUtils.throwIf(!saveResult, ErrorCode.SYSTEM_ERROR, "图表保存失败");
        BiResponse biResponse = new BiResponse();
        biResponse.setGenChart(genChart);
        biResponse.setGenResult(genResult);
        biResponse.setChartId(chart.getId());
        return ResultUtils.success(biResponse);
    }

image-20240418221552253

image-20240418222011139

image-20240418222350183

将从控制台获取到的图表信息,通过Echarts在线调试,确认是否可以生成图表open in new window

==> Parameters: 详细分析网站用户增长情况(String), 用户增长表(String), 日期,用户数
1号,10
2号,20
3号,30
4号,90
5号,0
6号,10
7号,20
(String), {
  "title": {
    "text": "网站用户增长情况"
  },
  "tooltip": {
    "trigger": "axis"
  },
  "legend": {
    "data": ["用户数"]
  },
  "grid": {
    "left": "3%",
    "right": "4%",
    "bottom": "3%",
    "containLabel": true
  },
  "toolbox": {
    "feature": {
      "saveAsImage": {}
    }
  },
  "xAxis": {
    "type": "category",
    "boundaryGap": false,
    "data": ["1号", "2号", "3号", "4号", "5号", "6号", "7号"]
  },
  "yAxis": {
    "type": "value"
  },
  "series": [
    {
      "name": "用户数",
      "type": "line",
      "data": [10, 20, 30, 90, 0, 10, 20],
      "markPoint": {
        "data": [
          { "type": "max", "name": "最大值" },
          { "type": "min", "name": "最小值" }
        ]
      },
      "markLine": {
        "data": [
          { "type": "average", "name": "平均值" }
        ]
      }
    }
  ]
}(String), 根据提供的数据,我们可以进行以下详细分析:

- 用户增长趋势:从1号到3号,用户数呈现稳步增长,从10增加到30。然而,4号用户数大幅增长至90,可能是由于某些推广活动或特殊情况。5号用户数突然下降至0,可能是数据错误或网站维护等原因。从6号开始,用户数又恢复到10,并在7号增长到20。

- 用户增长波动:从1号到7号,用户数的平均值为30(计算平均时排除5号的0,因为可能是异常数据)。最大值为90(出现在4号),最小值为0(出现在5号)。

- 用户增长幅度:从1号到4号,用户增长了800%。但从4号到5号,用户数下降了100%,这是一个显著的波动。从5号到7号,用户数增长了100%。

- 异常情况:5号的数据明显异常,需要进一步调查原因,可能是数据录入错误或网站问题。

- 结论:整体来看,除了5号的异常数据外,网站用户数在观察期间呈现增长趋势,但增长幅度存在较大波动,需要进一步分析影响增长的具体因素。特别是4号的显著增长和5号的显著下降,需要重点关注和调查。(String), 1780252549874569217(Long)

image-20240418222301105

​ 进一步检查数据库数据是否插入成功:

image-20240418222520875

​ 至此,完成BI智能接口的后端接口开发

4.后端接口开发总结

后端接口开发流程

【1】梳理AI接口接入思路(定义交互格式)

【2】构造用户请求(用户消息、CSV数据、图表类型)

【3】调用AI接口,获取并处理AI响应结果(获取对应图表信息)

【4】将响应数据(图表信息)保存到数据库中(前端可渲染数据信息)

前端开发

1.用户表单开发

组件页面构建

​ 创建AddChart组件(pages/AddChart/index.tsx:可以从User下copy一份),配置routes.ts路由

routes.ts路由配置

  // { path: '/', redirect: '/welcome' },

  { path: '/', redirect: '/add_chart' },
  { name: '添加图表', icon: 'table', path: '/add_chart', component: './AddChart' },

AddChart.tsx组件

import { Footer } from '@/components';

import { listChartByPageUsingPost } from '@/services/noob-bi/chartController';

import { useModel } from '@umijs/max';
import React, { useEffect,useState } from 'react';


const AddChart: React.FC = () => {
  const [type, setType] = useState<string>('account');
  const { setInitialState } = useModel('@@initialState');

  useEffect(()=>{
    listChartByPageUsingPost({}).then(res=>{
      console.error('res',res)
    })
  });

  return (
    
    // 页面信息定义(add-chart)
    <div className = "add-chart">
      hello my chart
    </div>
  
  );
};
export default AddChart;

image-20240418224412793

image-20240418224430390

生成效果

image-20240418224543686

表单开发

参考Ant Design 组件库open in new window

image-20240418224947696

选择参考表单代码

import React from 'react';
import { InboxOutlined, UploadOutlined } from '@ant-design/icons';
import {
  Button,
  Checkbox,
  Col,
  ColorPicker,
  Form,
  InputNumber,
  Radio,
  Rate,
  Row,
  Select,
  Slider,
  Space,
  Switch,
  Upload,
} from 'antd';

const { Option } = Select;

const formItemLayout = {
  labelCol: { span: 6 },
  wrapperCol: { span: 14 },
};

const normFile = (e: any) => {
  console.log('Upload event:', e);
  if (Array.isArray(e)) {
    return e;
  }
  return e?.fileList;
};

const onFinish = (values: any) => {
  console.log('Received values of form: ', values);
};

const App: React.FC = () => (
  <Form
    name="validate_other"
    {...formItemLayout}
    onFinish={onFinish}
    initialValues={{
      'input-number': 3,
      'checkbox-group': ['A', 'B'],
      rate: 3.5,
      'color-picker': null,
    }}
    style={{ maxWidth: 600 }}
  >
    <Form.Item label="Plain Text">
      <span className="ant-form-text">China</span>
    </Form.Item>
    <Form.Item
      name="select"
      label="Select"
      hasFeedback
      rules={[{ required: true, message: 'Please select your country!' }]}
    >
      <Select placeholder="Please select a country">
        <Option value="china">China</Option>
        <Option value="usa">U.S.A</Option>
      </Select>
    </Form.Item>

    <Form.Item
      name="select-multiple"
      label="Select[multiple]"
      rules={[{ required: true, message: 'Please select your favourite colors!', type: 'array' }]}
    >
      <Select mode="multiple" placeholder="Please select favourite colors">
        <Option value="red">Red</Option>
        <Option value="green">Green</Option>
        <Option value="blue">Blue</Option>
      </Select>
    </Form.Item>

    <Form.Item label="InputNumber">
      <Form.Item name="input-number" noStyle>
        <InputNumber min={1} max={10} />
      </Form.Item>
      <span className="ant-form-text" style={{ marginLeft: 8 }}>
        machines
      </span>
    </Form.Item>

    <Form.Item name="switch" label="Switch" valuePropName="checked">
      <Switch />
    </Form.Item>

    <Form.Item name="slider" label="Slider">
      <Slider
        marks={{
          0: 'A',
          20: 'B',
          40: 'C',
          60: 'D',
          80: 'E',
          100: 'F',
        }}
      />
    </Form.Item>

    <Form.Item name="radio-group" label="Radio.Group">
      <Radio.Group>
        <Radio value="a">item 1</Radio>
        <Radio value="b">item 2</Radio>
        <Radio value="c">item 3</Radio>
      </Radio.Group>
    </Form.Item>

    <Form.Item
      name="radio-button"
      label="Radio.Button"
      rules={[{ required: true, message: 'Please pick an item!' }]}
    >
      <Radio.Group>
        <Radio.Button value="a">item 1</Radio.Button>
        <Radio.Button value="b">item 2</Radio.Button>
        <Radio.Button value="c">item 3</Radio.Button>
      </Radio.Group>
    </Form.Item>

    <Form.Item name="checkbox-group" label="Checkbox.Group">
      <Checkbox.Group>
        <Row>
          <Col span={8}>
            <Checkbox value="A" style={{ lineHeight: '32px' }}>
              A
            </Checkbox>
          </Col>
          <Col span={8}>
            <Checkbox value="B" style={{ lineHeight: '32px' }} disabled>
              B
            </Checkbox>
          </Col>
          <Col span={8}>
            <Checkbox value="C" style={{ lineHeight: '32px' }}>
              C
            </Checkbox>
          </Col>
          <Col span={8}>
            <Checkbox value="D" style={{ lineHeight: '32px' }}>
              D
            </Checkbox>
          </Col>
          <Col span={8}>
            <Checkbox value="E" style={{ lineHeight: '32px' }}>
              E
            </Checkbox>
          </Col>
          <Col span={8}>
            <Checkbox value="F" style={{ lineHeight: '32px' }}>
              F
            </Checkbox>
          </Col>
        </Row>
      </Checkbox.Group>
    </Form.Item>

    <Form.Item name="rate" label="Rate">
      <Rate />
    </Form.Item>

    <Form.Item
      name="upload"
      label="Upload"
      valuePropName="fileList"
      getValueFromEvent={normFile}
      extra="longgggggggggggggggggggggggggggggggggg"
    >
      <Upload name="logo" action="/upload.do" listType="picture">
        <Button icon={<UploadOutlined />}>Click to upload</Button>
      </Upload>
    </Form.Item>
    <Form.Item label="Dragger">
      <Form.Item name="dragger" valuePropName="fileList" getValueFromEvent={normFile} noStyle>
        <Upload.Dragger name="files" action="/upload.do">
          <p className="ant-upload-drag-icon">
            <InboxOutlined />
          </p>
          <p className="ant-upload-text">Click or drag file to this area to upload</p>
          <p className="ant-upload-hint">Support for a single or bulk upload.</p>
        </Upload.Dragger>
      </Form.Item>
    </Form.Item>
    <Form.Item
      name="color-picker"
      label="ColorPicker"
      rules={[{ required: true, message: 'color is required!' }]}
    >
      <ColorPicker />
    </Form.Item>

    <Form.Item wrapperCol={{ span: 12, offset: 6 }}>
      <Space>
        <Button type="primary" htmlType="submit">
          Submit
        </Button>
        <Button htmlType="reset">reset</Button>
      </Space>
    </Form.Item>
  </Form>
);

export default App;

image-20240418225304763

AddChart雏形

​ 填充Form表单(清理无用代码)、引入onFinish方法、导入所需依赖,简化后的代码参考如下

import { Footer } from '@/components';

import { listChartByPageUsingPost } from '@/services/noob-bi/chartController';

import { useModel } from '@umijs/max';
import React, { useEffect,useState } from 'react';

import { InboxOutlined, UploadOutlined } from '@ant-design/icons';
import {
  Button,
  Form,
  Select,
  Space,
  Upload,
} from 'antd';
import TextArea from 'antd/es/input/TextArea';



const AddChart: React.FC = () => {
  const [type, setType] = useState<string>('account');
  const { setInitialState } = useModel('@@initialState');

  useEffect(()=>{
    listChartByPageUsingPost({}).then(res=>{
      console.error('res',res)
    })
  });

  const onFinish = (values: any) => {
    console.log('Received values of form: ', values);
  };

  return (
    // 页面信息定义(add-chart)
    <div className = "add-chart">
      <Form
        // 设定表单名称
        name="addChart"
        onFinish={onFinish}
        // 初始化数据为空
        initialValues={{ }}
        style={{ maxWidth: 600 }}
      >

        <Form.Item name="rate" label="Rate">
          <TextArea />
        </Form.Item>

        <Form.Item
          name="select"
          label="Select"
          hasFeedback
          rules={[{ required: true, message: 'Please select your country!' }]}
        >
          <Select placeholder="Please select a country">
            <Option value="china">China</Option>
            <Option value="usa">U.S.A</Option>
          </Select>
        </Form.Item>

        <Form.Item
          name="upload"
          label="Upload"
          valuePropName="fileList"
          extra="longgggggggggggggggggggggggggggggggggg"
        >
          <Upload name="logo" action="/upload.do" listType="picture">
            <Button icon={<UploadOutlined />}>Click to upload</Button>
          </Upload>
        </Form.Item>

        <Form.Item wrapperCol={{ span: 12, offset: 6 }}>
          <Space>
            <Button type="primary" htmlType="submit">
              Submit
            </Button>
            <Button htmlType="reset">reset</Button>
          </Space>
        </Form.Item>
      </Form>
    </div>
  
  );
};
export default AddChart;

image-20240418230300768

AddChart细化
import { Footer } from '@/components';

import { listChartByPageUsingPost } from '@/services/noob-bi/chartController';

import { useModel } from '@umijs/max';
import React, { useEffect,useState } from 'react';

import { InboxOutlined, UploadOutlined } from '@ant-design/icons';
import {
  Button,
  Form,
  Input,
  Select,
  Space,
  Upload,
} from 'antd';
import TextArea from 'antd/es/input/TextArea';



const AddChart: React.FC = () => {
  const [type, setType] = useState<string>('account');
  const { setInitialState } = useModel('@@initialState');

  useEffect(()=>{
    listChartByPageUsingPost({}).then(res=>{
      console.error('res',res)
    })
  });

  const onFinish = (values: any) => {
    console.log('Received values of form: ', values);
  };

  return (
    // 页面信息定义(add-chart)
    <div className = "add-chart">
      <Form
        // 设定表单名称
        name="addChart"
        onFinish={onFinish}
        // 初始化数据为空
        initialValues={{ }}
        style={{ maxWidth: 600 }}
      >

        {/* 前端表单的name属性对应后端接口请求参数的字段,name对应goal,label为左侧提示文本,rules=....是必填项提示 */}

        <Form.Item name="goal" label="分析目标" rules={[{required:true,message:"请输入分析目标"}]}>
          {/* placeholder文本框提示语 */}
          <TextArea placeholder="请输入分析需求,例如:分析网站用户的增长情况"/>
        </Form.Item>

        {/* 图表名称 */}
        <Form.Item name="name" label="图表名称">
          {/* placeholder文本框提示语 */}
          <Input placeholder="请输入分析需求,例如:分析网站用户的增长情况"/>
        </Form.Item>


        {/* 图表类型非必填,不做校验 */}
        <Form.Item
          name="selchartTypeect"
          label="图表类型"
        >
          <Select placeholder="请选择图表类型"
            options={[
              {value:'折线图',label:'折线图'},
              {value:'柱状图',label:'柱状图'},
              {value:'堆叠图',label:'堆叠图'},
              {value:'饼图',label:'饼图'},
              {value:'雷达图',label:'雷达图'},
            ]}
          />
        </Form.Item>

        {/* 文件上传 */}
        <Form.Item
          name="file"
          label="原始数据"
        >
          {/* action:文件上传之后 调用后台接口  action="/upload.do" */}
          <Upload name="file">
            <Button icon={<UploadOutlined />}>上传CSV文件</Button>
          </Upload>
        </Form.Item>

        <Form.Item wrapperCol={{ span: 12, offset: 6 }}>
          <Space>
            <Button type="primary" htmlType="submit">
              提交
            </Button>
            <Button htmlType="reset">重置</Button>
          </Space>
        </Form.Item>
      </Form>
    </div>
  
  );
};
export default AddChart;

image-20240418231756207

2.后端对接

​ 利用openapi自动生成前端调用代码,启动npm run openapi ,生成接口调用代码

​ 简化前端模板:将src/services目录下的ant-design-pro删除(测试接口),并且去除相关引用(后续调整为后端对接相关接口)

image-20240418232632182

​ 删除pages/TableList文件夹,并在routes.ts中去除相关路由

image-20240418233352315

​ 完善onFinish方法,完成后端接口调用


​ 接口调用通过断点调试,或者F12查看响应结果(接口调用AI响应可能有点慢,耐心蹲)

image-20240418233805072

3.图表渲染

​ 图表渲染选择Echart库(在API接口平台已经了解过使用)

【1】引入echarts组件库

npm install echarts-for-react

【2】参考官方示例open in new window,编写示例代码看能否接入Echarts

【3】联调测试(完整代码测试)


import { genChartByAiUsingPost, listChartByPageUsingPost } from '@/services/noob-bi/chartController';

import { useModel } from '@umijs/max';
import React, { useEffect, useState } from 'react';

import { UploadOutlined } from '@ant-design/icons';
import {
  Button,
  Form,
  Input,
  Select,
  Space,
  Upload,
  message,
  Card,
  Col,
  Row,
  Spin,
  Divider
} from 'antd';
import TextArea from 'antd/es/input/TextArea';
import EChartsReact from 'echarts-for-react';



const AddChart: React.FC = () => {
  // 定义状态,接收后端返回值,实时展示在页面上
  const [chart,setChart] = useState<API.BiResponse>();
  const [option,setOption] = useState<any>();
  // 提交中的状态,默认未提交
  const [submitting,setSubmitting] = useState<boolean>(false);

  /**
   * 提交
   * @param values 
   */
  const onFinish = async (values: any) => {
    console.log('Received values of form: ', values);

    // 如果已经提交中的状态(加载),则直接返回避免重复提交
    if(submitting){
      return;
    }

    // 开始提交,将submitting设置为true
    setSubmitting(true);

    // 如果提交了,则将图表数据和图表代码清空(避免和之前提交的图表堆叠);如果option清空了则组件会触发重新渲染,不会保留之前的历史记录
    setChart(undefined);
    setOption(undefined);
  
    // 对接后端,上传数据
    const params = {
      ...values,
      file: undefined,
    };
    try {
      // 获取到上传的原始数据并传入后端接口
      const res = await genChartByAiUsingPost(params, {}, values.file.file.originFileObj);
      // 一般情况下没有返回值为分析失败,有则认为成功
      if (!res?.data) {
        message.error('分析失败');
      } else {
        message.success('分析成功');
        // 解析成对象,为空则设置为空字符串
        const chartOption = JSON.parse(res.data.genChart??'');
        // 如果为空,则抛出异常,提示图表代码解析错误
        if(!chartOption){
          throw new Error('图表代码解析错误');
        }else{
          // 解析成功,则将响应结果设置到图表中进行渲染
          setChart(res.data);
          setOption(chartOption);
        }
      }
    } catch (e: any) {
      // 异常情况下,提示分析失败和具体的原因说明
      message.error('分析失败,' + e.message);
    }

    // 提交结束,将setSubmitting设置为false
    setSubmitting(false);
  };

  return (
    // 页面展示美化


    // 页面信息定义(add-chart)
    <div className = "add-chart">
      <Row gutter={24}>
        <Col span={12}>
          <Card title="智能分析">
            <Form
              // 设定表单名称
              name="addChart"
              onFinish={onFinish}
              // 初始化数据为空
              initialValues={{ }}
              style={{ maxWidth: 600 }}
            >

              {/* 前端表单的name属性对应后端接口请求参数的字段,name对应goal,label为左侧提示文本,rules=....是必填项提示 */}

              <Form.Item name="goal" label="分析目标" rules={[{required:true,message:"请输入分析目标"}]}>
                {/* placeholder文本框提示语 */}
                <TextArea placeholder="请输入分析需求,例如:分析网站用户的增长情况"/>
              </Form.Item>

              {/* 图表名称 */}
              <Form.Item name="name" label="图表名称">
                {/* placeholder文本框提示语 */}
                <Input placeholder="请输入分析需求,例如:分析网站用户的增长情况"/>
              </Form.Item>


              {/* 图表类型非必填,不做校验 */}
              <Form.Item
                name="selchartTypeect"
                label="图表类型"
              >
                <Select placeholder="请选择图表类型"
                  options={[
                    {value:'折线图',label:'折线图'},
                    {value:'柱状图',label:'柱状图'},
                    {value:'堆叠图',label:'堆叠图'},
                    {value:'饼图',label:'饼图'},
                    {value:'雷达图',label:'雷达图'},
                  ]}
                />
              </Form.Item>

              {/* 文件上传 */}
              <Form.Item
                name="file"
                label="原始数据"
              >
                {/* action:文件上传之后 调用后台接口  action="/upload.do" */}
                <Upload name="file">
                  <Button icon={<UploadOutlined />}>上传CSV文件</Button>
                </Upload>
              </Form.Item>

              <Form.Item wrapperCol={{ span: 12, offset: 6 }}>
                <Space>
                  <Button type="primary" htmlType="submit">
                    提交
                  </Button>
                  <Button htmlType="reset">重置</Button>
                </Space>
              </Form.Item>
            </Form>
            </Card>
        </Col>

        <Col span={12}>
          <Card title="分析结论">
            {chart?.genResult ?? <div>请先在左侧进行提交</div>}
            <Spin spinning={submitting}/>
          </Card>
          <Divider />
          <Card title="可视化图表">
            {
              option ? <EChartsReact option={option} /> : <div>请先在左侧进行提交</div>
            }
            <Spin spinning={submitting}/>
          </Card>
        </Col>
      </Row>
      
    </div>
  );
};
export default AddChart;

image-20240419001631238

待优化

【1】网站安全性考虑

​ 如果用户上传一个超级大的文件怎么办

​ 如果用户疯狂点击提交怎么办

​ 如果AI生成太慢,但是用户访问量比较大,给系统造成压力,如何兼顾用户体验和系统的可用性

评论
  • 按正序
  • 按倒序
  • 按热度
Powered by Waline v3.1.3