Browse Source

feat: 思维导图添加ai业务功能

liaojiaxing 1 month ago
parent
commit
88cec6b1a8

+ 7 - 6
apps/designer/src/hooks/useChat.ts

@@ -194,16 +194,17 @@ export function useChat({ app_name, onStart, onSuccess, onUpdate, onError }: Cha
               const data = JSON.parse(chunk.data);
               if (data?.event === "message") {
                 onUpdate(data);
-              }
-              if (data?.event === "message_end") {
+              } else if (data?.event === "message_end") {
                 onSuccess(data);
-              }
-              if (data?.event === "message_error") {
+              } else if (data?.event === "message_error") {
                 onError(data);
-              }
-              if (data?.event === "ping") {
+              } else if (data?.event === "ping") {
                 console.log(">>>> stream start <<<<");
                 onStart?.(data);
+              } else {
+                console.log(">>>> stream error <<<<");
+                console.log(data);
+                onError?.(Error(data?.message || 'AI调用失败'));
               }
             }
           }

+ 3 - 3
apps/designer/src/pages/flow/components/Config/AiCreator.tsx

@@ -291,9 +291,9 @@ export default function AICreator(props: {
     let result = "";
     onRequest(
       `修复语法错误,并返回修复后的键对值,要求:
-        1、仅返回修复后的JSON数据,不展示其他内容
-        2、以键值对展示 如: {'大有做为': '大有作为'}
-      需要修复的数据:${data}`,
+1、仅返回修复后的JSON数据,不展示其他内容
+2、以键值对展示 如: {'大有做为': '大有作为'}
+需要修复的数据:${data}`,
       {
         onUpdate: (data) => {
           result += data.answer;

+ 249 - 93
apps/designer/src/pages/mindmap/components/Config/AiCreator.tsx

@@ -1,23 +1,11 @@
 import React, { useRef, useState } from "react";
-import {
-  CloseOutlined,
-  SendOutlined,
-  EditOutlined,
-  DeleteOutlined,
-  CaretDownOutlined,
-} from "@ant-design/icons";
-import { Button, Tooltip, Input, Form, Dropdown, message } from "antd";
-import type { DropDownProps } from "antd";
+import { CloseOutlined, SendOutlined } from "@ant-design/icons";
+import { Button, Tooltip, Input, Form, message } from "antd";
 import { useChat } from "@/hooks/useChat";
-import {} from "react-markdown";
-
-interface ChatHistoryItem {
-  id: string;
-  messages: any[];
-  createdAt: number;
-  updatedAt: number;
-  title: string;
-}
+import { Cell } from "@antv/x6";
+import { handleParseAIData } from "@/utils";
+import { useModel } from "umi";
+import { traverseNode } from "@/utils/mindmapHander";
 
 export default function AICreator(props: {
   type: "mindmap" | "flow";
@@ -30,6 +18,7 @@ export default function AICreator(props: {
   const [messageApi, contextHolder] = message.useMessage();
   const messageKey = "ailoading";
   const msgContent = useRef<string>("");
+  const { mindProjectInfo, setMindProjectInfo, graph } = useModel("mindMapModel");
 
   const { loading, onRequest, cancel } = useChat({
     app_name: "system_design",
@@ -48,7 +37,15 @@ export default function AICreator(props: {
           marginTop: 300,
         },
       });
-      handleParse();
+      handleParseAIData({
+        content: msgContent.current,
+        message: {
+          key: messageKey,
+          instance: messageApi,
+        },
+        onSuccess: props.onChange,
+        onError: props.onError,
+      });
     },
     onError: (err) => {
       messageApi.open({
@@ -62,51 +59,6 @@ export default function AICreator(props: {
       });
     },
   });
-
-  function regexExtractJSON(markdown: string) {
-    const jsonRegex = /```(?:json)?\n([\s\S]*?)\n```/g;
-    const matches = [];
-    let match;
-
-    while ((match = jsonRegex.exec(markdown)) !== null) {
-      try {
-        const jsonObj = JSON.parse(match[1]);
-        matches.push(jsonObj);
-      } catch (e) {
-        console.warn("无效JSON:", match[0]);
-      }
-    }
-    return matches;
-  }
-
-  const handleParse = () => {
-    try {
-      // 根据markdown格式取出json部分数据
-      const md = msgContent.current;
-      let json: string;
-      if (md.includes("```json")) {
-        json = regexExtractJSON(msgContent.current)?.[0];
-      } else {
-        json = JSON.parse(msgContent.current);
-      }
-
-      console.log("解析结果:", json);
-      props.onChange?.(json);
-    } catch (error) {
-      messageApi.open({
-        key: messageKey,
-        type: "error",
-        content: "AI创作失败",
-        duration: 2,
-        style: {
-          marginTop: 300,
-        },
-      });
-      console.error(error);
-      props.onError?.(new Error("AI创作失败"));
-    }
-  };
-
   const handleStop = () => {
     cancel();
     messageApi.open({
@@ -123,11 +75,7 @@ export default function AICreator(props: {
   // 处理提交
   const onSubmit = () => {
     if (input.trim()) {
-      onRequest(
-        `设计思维导图, 具体需求描述:${input}`,
-        undefined,
-        input
-      );
+      onRequest(`设计思维导图, 具体需求描述:${input}`, undefined, input);
 
       messageApi.open({
         key: messageKey,
@@ -148,7 +96,7 @@ export default function AICreator(props: {
       controller.abort();
     };
   }, []);
-  
+
   const handleList = [
     {
       key: "style",
@@ -176,19 +124,241 @@ export default function AICreator(props: {
     },
   ];
 
+  type LabelMap = Record<
+    string,
+    { cell: string; key: string; cellType: string }[]
+  >;
+
+  // 从元素中提取文本 当前无选中元素时,提取所有元素
+  const getLabels = () => {
+    const labelMap: LabelMap = {};
+    let cells: Cell[] | undefined;
+
+    cells = graph?.getSelectedCells();
+    if (!cells || cells.length === 0) {
+      cells = graph?.getCells();
+    }
+
+    if (!cells || !cells.length) return;
+
+    cells.forEach((cell) => {
+      const data = cell.getData();
+      // 从label中提取文本
+      if (data?.label?.trim()) {
+        if (!labelMap[data.label.trim()]) {
+          labelMap[data.label.trim()] = [
+            { cell: cell.id, key: "label", cellType: cell.shape },
+          ];
+        } else {
+          labelMap[data.label.trim()].push({
+            cell: cell.id,
+            key: "label",
+            cellType: cell.shape,
+          });
+        }
+      }
+    });
+    return labelMap;
+  };
+
+  // 替换节点文本内容
+  const handleReplace = (labelMap: LabelMap, data: Record<string, string>) => {
+    mindProjectInfo && setMindProjectInfo({
+      ...mindProjectInfo,
+      topics: traverseNode(mindProjectInfo?.topics || [], (topic) => {
+        // 判断当前节点是否需要替换
+        const ids = labelMap[topic.label?.trim()]?.map(item => item.cell);
+        if (data[topic.label?.trim()] && ids?.includes(topic.id)) {
+          topic.label = data[topic.label?.trim()];
+        }
+        return topic
+      })
+    });
+  };
+
+  // 风格美化
+  const handleStyle = () => {
+    onRequest("生成一个美化风格的配置", {
+      onUpdate: (data) => {
+        console.log("style update:", data);
+      },
+      onSuccess: (data) => {
+        console.log(data);
+      },
+      onError: (err) => {
+        console.error(err);
+      },
+    });
+  };
+
+  // 语法修复
+  const handleGrammar = () => {
+    const labelMap = getLabels();
+    if (!labelMap) {
+      messageApi.open({
+        key: messageKey,
+        type: "info",
+        content: "无可修复的数据",
+      });
+      return;
+    }
+
+    messageApi.open({
+      key: messageKey,
+      type: "loading",
+      content: "AI正在修复语法...",
+      duration: 0,
+      style: {
+        marginTop: 300,
+      },
+    });
+
+    const data = JSON.stringify(Object.keys(labelMap));
+    let result = "";
+    onRequest(
+      `修复语法错误,并返回修复后的键对值,要求:
+1、仅返回修复后的JSON数据,不展示其他内容
+2、以键值对展示 如: {'大有做为': '大有作为'}
+ 需要修复的数据:${data}`,
+      {
+        onUpdate: (data) => {
+          result += data.answer;
+        },
+        onSuccess: (data) => {
+          messageApi.open({
+            key: messageKey,
+            type: "success",
+            content: "AI修复语法完成",
+            duration: 2,
+            style: {
+              marginTop: 300,
+            },
+          });
+          handleParseAIData({
+            content: result,
+            message: {
+              key: messageKey,
+              instance: messageApi,
+            },
+            onSuccess: (data) => {
+              handleReplace(labelMap, data);
+            },
+          });
+        },
+        onError: (err) => {
+          console.error(err);
+          messageApi.open({
+            key: messageKey,
+            type: "error",
+            content: err.message || "AI创作失败",
+            duration: 2,
+            style: {
+              marginTop: 300,
+            },
+          });
+        },
+      },
+      "语法修复"
+    );
+  };
+
+  // 翻译
+  const handleTranslation = (lang: "en" | "zh") => {
+    const labelMap = getLabels();
+    if (!labelMap) {
+      messageApi.open({
+        key: messageKey,
+        type: "info",
+        content: "无可翻译的数据",
+      });
+      return;
+    }
+
+    messageApi.open({
+      key: messageKey,
+      type: "loading",
+      content: "AI正在翻译...",
+      duration: 0,
+      style: {
+        marginTop: 300,
+      },
+    });
+
+    const data = JSON.stringify(Object.keys(labelMap));
+    let result = "";
+    onRequest(
+      `翻译成${lang === "en" ? "英文" : "中文"},并返回翻译后的键对值,要求:
+          1、仅返回修复后的JSON数据,不展示其他内容
+          2、以键值对展示 如: {'中文': 'Chinese'}
+        需要翻译的数据:${data}`,
+      {
+        onUpdate: (data) => {
+          result += data.answer;
+        },
+        onSuccess: (data) => {
+          messageApi.open({
+            key: messageKey,
+            type: "success",
+            content: "AI翻译完成",
+            duration: 2,
+            style: {
+              marginTop: 300,
+            },
+          });
+          handleParseAIData({
+            content: result,
+            message: {
+              key: messageKey,
+              instance: messageApi,
+            },
+            onSuccess: (data) => {
+              handleReplace(labelMap, data);
+            },
+          });
+        },
+        onError: (err) => {
+          console.error(err);
+          messageApi.open({
+            key: messageKey,
+            type: "error",
+            content: err.message || "AI创作失败",
+            duration: 2,
+            style: {
+              marginTop: 300,
+            },
+          });
+        },
+      },
+      "翻译内容"
+    );
+  };
+
   const handleAiFeature = (feature: string) => {
-    // onRequest('', {
-    //   onUpdata: (data) => {
-    //   },
-    //   onSuccess: (data) => {
-    //   },
-    //   onError: (err) => {
-    //   }
-    // })
+    switch (feature) {
+      case "style":
+        handleStyle();
+        break;
+      case "grammar":
+        handleGrammar();
+        break;
+      case "translation_en":
+        handleTranslation("en");
+        break;
+      case "translation_zh":
+        handleTranslation("zh");
+        break;
+      default:
+        break;
+    }
   };
 
   return (
-    <div className="flex-1 h-full" style={{backgroundImage: 'linear-gradient(137deg, #e5f4ff 0%, #efe7ff 100%)'}}>
+    <div
+      className="flex-1 h-full"
+      style={{
+        backgroundImage: "linear-gradient(137deg, #e5f4ff 0%, #efe7ff 100%)",
+      }}
+    >
       {contextHolder}
       <div className="chat-head w-full h-40px px-10px color-#333 flex items-center justify-between">
         <span className="text-14px">
@@ -198,20 +368,6 @@ export default function AICreator(props: {
           <span>AI创作</span>
         </span>
         <span>
-          {/* <Dropdown
-            menu={{  }}
-            trigger={["click"]}
-            placement="bottomLeft"
-            arrow
-          >
-            <Tooltip title="历史记录">
-              <Button
-                type="text"
-                size="small"
-                icon={<FieldTimeOutlined />}
-              ></Button>
-            </Tooltip>
-          </Dropdown> */}
           <Button
             type="text"
             size="small"

+ 4 - 0
apps/designer/src/utils/mindmapHander.tsx

@@ -315,6 +315,10 @@ export const traverseNode = (
     if (topic.children?.length) {
       topic.children = traverseNode(topic.children, callback);
     }
+    // 遍历概要
+    if (topic?.summary?.topic) {
+      topic.summary.topic = traverseNode([topic.summary.topic], callback)[0];
+    }
     return topic;
   });
 };