Przeglądaj źródła

feat: 新增流程图右键菜单ai处理方法

liaojiaxing 1 miesiąc temu
rodzic
commit
0a446754c0

+ 4 - 3
apps/designer/src/components/ai/RenderGraph.tsx

@@ -1,7 +1,7 @@
 import { useEffect, useMemo, useRef, useState } from "react";
 import { Button, message, Tooltip } from "antd";
 import { Graph } from "@antv/x6";
-import { handleParseFowlAIData } from "@/utils";
+import { handleParseFowlAIData, replaceFlowId } from "@/utils";
 import { getFlowNodeMetaByAi } from "@/utils/flow";
 import { getDefaultDataByTheme } from "@/config/data";
 import { PlusOutlined } from "@ant-design/icons";
@@ -115,11 +115,12 @@ export default function RenderGraph(props: {
           instance: messageApi,
         },
         onSuccess: (data) => {
-          parseData.current = data;
+          const result = replaceFlowId(data);
+          parseData.current = result;
           // graphRef.current?.fromJSON(data);
           const defaultDataByTheme = getDefaultDataByTheme("1");
 
-          const list = (Array.isArray(data) ? data : [data]).map((item) =>
+          const list = (Array.isArray(result) ? result : [result]).map((item) =>
             getFlowNodeMetaByAi(item, defaultDataByTheme)
           );
 

+ 1 - 0
apps/designer/src/models/flowchartModel.ts

@@ -77,6 +77,7 @@ export default function flowchartModel() {
       graphRef.current.fromJSON({ cells: list as any });
       addPageNode();
       handleGraphApiEvent(graphRef.current);
+      // graphRef.current?.centerContent();
     }
   };
 

+ 72 - 0
apps/designer/src/utils/aiRequest.ts

@@ -0,0 +1,72 @@
+import { XStream } from "@ant-design/x";
+
+
+const baseUrl = process.env.NODE_ENV === "production" ? "" : "/api";
+/**
+ * ai请求封装
+ * @param chat_query 
+ * @param options 
+ * @param chat_name 
+ */
+export const aiRequest = async (
+  chat_query: string,
+  options: {
+    onUpdate: (data: any) => void;
+    onSuccess: (data: any) => void;
+    onError: (err: Error) => void;
+  },
+  chat_name: string
+) => {
+  // const abortController = new AbortController();
+  // const signal = abortController.signal;
+  const enterpriseCode = sessionStorage.getItem("enterpriseCode");
+  const token = localStorage.getItem("token_" + enterpriseCode) || "";
+  const { onUpdate, onSuccess, onError } = options;
+
+  try {
+    const response = await fetch(baseUrl + "/api/ai/chat-message", {
+      method: "POST",
+      body: JSON.stringify({
+        app_name: 'system_design',
+        chat_query,
+        chat_name,
+        conversation_id: undefined,
+      }),
+      headers: {
+        Authorization: token,
+        "Content-Type": "application/json",
+      },
+      // signal,
+    });
+
+    // 判断当前是否流式返回
+    if (response.headers.get("content-type")?.includes("text/event-stream")) {
+      if (response.body) {
+        for await (const chunk of XStream({
+          readableStream: response.body,
+        })) {
+          const data = JSON.parse(chunk.data);
+          if (data?.event === "message") {
+            onUpdate(data);
+          } else if (data?.event === "message_end") {
+            onSuccess(data);
+          } else if (data?.event === "message_error") {
+            onError(data);
+          } else if (data?.event === "ping") {
+            console.log(">>>> stream start <<<<");
+          } else {
+            console.log(">>>> stream error <<<<");
+            onError(Error(data?.message || "请求失败"));
+          }
+        }
+      }
+    } else {
+      // 接口异常处理
+      response.json().then((res) => {
+        onError?.(Error(res?.error || "请求失败"));
+      });
+    }
+  } catch (error) {
+    onError(error as Error);
+  }
+};

+ 30 - 5
apps/designer/src/utils/contentMenu.tsx

@@ -154,16 +154,22 @@ const commonMenuData: MenuItem[] = [
   },
   { type: "divider" },
   {
-    key: "grammarFix",
+    key: "fixSyntax",
     label: "语法修复",
     icon: "icon-AI",
-    handler: menuHander.defaultStyle,
+    handler: menuHander.fixSyntax,
   },
   {
-    key: "translation",
-    label: "翻译",
+    key: "translateToEnglish",
+    label: "翻译为英文",
     icon: "icon-AI",
-    handler: menuHander.defaultStyle,
+    handler: menuHander.translateToEnglish,
+  },
+  {
+    key: "translateToChinese",
+    label: "翻译为中文",
+    icon: "icon-AI",
+    handler: menuHander.translateToChinese,
   },
   { type: "divider" },
   {
@@ -286,6 +292,25 @@ const pageMenuData: MenuItem[] = [
     label: "重置视图缩放",
     handler: menuHander.resetView,
   },
+  { type: "divider" },
+  {
+    key: "fixSyntax",
+    label: "全局语法修复",
+    icon: "icon-AI",
+    handler: menuHander.fixSyntaxAll,
+  },
+  {
+    key: "translateToEnglish",
+    label: "全局翻译为英文",
+    icon: "icon-AI",
+    handler: menuHander.translateToEnglishAll,
+  },
+  {
+    key: "translateToChinese",
+    label: "全局翻译为中文",
+    icon: "icon-AI",
+    handler: menuHander.translateToChineseAll,
+  },
   { key: "2", type: "divider" },
   {
     key: "selectAll",

+ 255 - 0
apps/designer/src/utils/flowAIHander.ts

@@ -0,0 +1,255 @@
+import { message } from "antd";
+import { Cell, Graph } from "@antv/x6";
+import { handleParseFowlAIData } from "@/utils";
+import { aiRequest } from "./aiRequest";
+
+type LabelMap = Record<
+  string,
+  { cell: string; key: string; cellType: string }[]
+>;
+
+// 从元素中提取文本 当前无选中元素时,提取所有元素
+const getLabels = (graph?: Graph, cell?: Cell) => {
+  const labelMap: LabelMap = {};
+  let cells: Cell[] | undefined;
+
+  if(cell) {
+    cells = [cell];
+  } else {
+    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,
+        });
+      }
+    }
+    // 从name中提取文本
+    if (data?.name?.trim()) {
+      if (!labelMap[data.name.trim()]) {
+        labelMap[data.name.trim()] = [
+          { cell: cell.id, key: "name", cellType: cell.shape },
+        ];
+      } else {
+        labelMap[data.name.trim()].push({
+          cell: cell.id,
+          key: "name",
+          cellType: cell.shape,
+        });
+      }
+    }
+    // 从边线中提取文本
+    if (cell.isEdge()) {
+      (cell.labels || []).forEach((label) => {
+        const labelText = (label?.attrs?.label?.text as string)?.trim();
+        if (labelText) {
+          if (!labelMap[labelText]) {
+            labelMap[labelText] = [
+              { cell: cell.id, key: "label", cellType: cell.shape },
+            ];
+          } else {
+            labelMap[labelText].push({
+              cell: cell.id,
+              key: "label",
+              cellType: cell.shape,
+            });
+          }
+        }
+      });
+    }
+  });
+  return labelMap;
+};
+
+// 替换节点文本内容
+const handleReplace = (
+  labelMap: LabelMap,
+  data: Record<string, string>,
+  graph?: Graph
+) => {
+  const keyMap: Record<string, string> = {};
+  if (Array.isArray(data)) {
+    data.forEach((item) => {
+      keyMap[item.original] = item.value;
+    });
+  }
+
+  Object.keys(keyMap).forEach((key) => {
+    if (labelMap[key]) {
+      labelMap[key].forEach((item) => {
+        const cell = graph?.getCellById(item.cell);
+        console.log(cell, item, graph);
+        if (cell && cell.shape !== "edge") {
+          cell.setData({
+            [item.key]: keyMap[key],
+          });
+          graph?.select(cell);
+        } else if (cell?.isEdge()) {
+          // 设置边线文本
+          const labels = cell.getLabels();
+          cell.setLabels(
+            labels.map((item) => {
+              return {
+                ...item,
+                attrs: {
+                  label: {
+                    text:
+                      keyMap?.[(item.attrs?.label?.text as string)?.trim()] ||
+                      item.attrs?.label?.text,
+                  },
+                },
+              };
+            })
+          );
+        }
+      });
+    }
+  });
+};
+
+// 语法修复
+export const handleGrammar = (graph?: Graph, cell?: Cell) => {
+  const labelMap = getLabels(graph, cell);
+  if (!labelMap) {
+    message.open({
+      key: "ai-handler-message",
+      type: "info",
+      content: "无可修复的数据",
+    });
+    return;
+  }
+
+  message.open({
+    key: "ai-handler-message",
+    type: "loading",
+    content: "AI正在修复语法...",
+    duration: 0,
+  });
+
+  const data = JSON.stringify(Object.keys(labelMap));
+  let result = "";
+  aiRequest(
+    `修复语法错误,需要修复的数据:${data}`,
+    {
+      onUpdate: (data) => {
+        result += data.answer;
+      },
+      onSuccess: (data) => {
+        message.open({
+          key: "ai-handler-message",
+          type: "success",
+          content: "AI修复语法完成",
+          duration: 2,
+          style: {
+            marginTop: 300,
+          },
+        });
+        handleParseFowlAIData({
+          content: result,
+          message: {
+            key: "ai-handler-message",
+            instance: message,
+          },
+          onSuccess: (data) => {
+            handleReplace(labelMap, data, graph);
+          },
+        });
+      },
+      onError: (err) => {
+        console.error(err);
+        message.open({
+          key: "ai-handler-message",
+          type: "error",
+          content: err.message || "AI创作失败",
+          duration: 2,
+          style: {
+            marginTop: 300,
+          },
+        });
+      },
+    },
+    "语法修复"
+  );
+};
+
+// 翻译
+export const handleTranslation = (lang: "en" | "zh", graph?: Graph, cell?: Cell) => {
+  const labelMap = getLabels(graph, cell);
+  if (!labelMap) {
+    message.open({
+      key: "ai-handler-message",
+      type: "info",
+      content: "无可翻译的数据",
+    });
+    return;
+  }
+
+  message.open({
+    key: "ai-handler-message",
+    type: "loading",
+    content: "AI正在翻译...",
+    duration: 0,
+  });
+
+  const data = JSON.stringify(Object.keys(labelMap));
+  let result = "";
+  aiRequest(
+    `翻译成${lang === "en" ? "英文" : "中文"},需要翻译的数据:${data}`,
+    {
+      onUpdate: (data) => {
+        result += data.answer;
+      },
+      onSuccess: (data) => {
+        message.open({
+          key: "ai-handler-message",
+          type: "success",
+          content: "AI翻译完成",
+          duration: 2,
+          style: {
+            marginTop: 300,
+          },
+        });
+        handleParseFowlAIData({
+          content: result,
+          message: {
+            key: "ai-handler-message",
+            instance: message,
+          },
+          onSuccess: (data) => {
+            handleReplace(labelMap, data, graph);
+          },
+        });
+      },
+      onError: (err) => {
+        console.error(err);
+        message.open({
+          key: "ai-handler-message",
+          type: "error",
+          content: err.message || "AI创作失败",
+          duration: 2,
+          style: {
+            marginTop: 300,
+          },
+        });
+      },
+    },
+    "翻译内容"
+  );
+};

+ 25 - 1
apps/designer/src/utils/hander.tsx

@@ -9,6 +9,7 @@ import { set, cloneDeep, get, merge } from "lodash-es";
 import { defaultData } from "@/config/data";
 import codeBlock from "@/components/basic/CodeBlock";
 import { ConnectorType } from "@/enum";
+import { handleGrammar, handleTranslation } from "@/utils/flowAIHander";
 
 /**
  * 右键菜单处理方法
@@ -123,7 +124,30 @@ export const menuHander = {
   },
   // 替换图形
   replace(tool: ContextMenuTool, e: MouseEvent) {},
-  
+  // ai语法修复
+  fixSyntax(tool: ContextMenuTool, e: MouseEvent) {
+    handleGrammar(tool.graph, tool.cell)
+  },
+  // ai翻译成英文
+  translateToEnglish(tool: ContextMenuTool, e: MouseEvent) {
+    handleTranslation("en", tool.graph, tool.cell)
+  },
+  // ai翻译成中文
+  translateToChinese(tool: ContextMenuTool, e: MouseEvent) {
+    handleTranslation("zh", tool.graph, tool.cell)
+  },
+  // 全局ai语法修复
+  fixSyntaxAll(tool: ContextMenuTool, e: MouseEvent) {
+    handleGrammar(tool.graph)
+  },
+  // 全局ai翻译成英文
+  translateToEnglishAll(tool: ContextMenuTool, e: MouseEvent) {
+    handleTranslation("en", tool.graph)
+  },
+  // 全局ai翻译成中文
+  translateToChineseAll(tool: ContextMenuTool, e: MouseEvent) {
+    handleTranslation("zh", tool.graph)
+  },
 };
 
 export const selectAll = (graph: Graph) => {

+ 4 - 5
apps/designer/src/utils/index.ts

@@ -122,7 +122,7 @@ function regexExtractJSON(markdown: string) {
 /**
  * AI生成的数据id可能有问题,重试生成id
  */
-const replaceId = (data: any) => {
+export const replaceFlowId = (data: any) => {
   if(Object.prototype.toString.call(data) === "[object Object]") {
     if(data?.id) {
       data.id = uuid();
@@ -167,7 +167,7 @@ export const handleParseFowlAIData = ({
   content,
   message,
   onSuccess,
-  onError,
+  onError
 }: {
   content: string;
   message: {
@@ -186,9 +186,8 @@ export const handleParseFowlAIData = ({
       json = JSON.parse(content);
     }
 
-    // 处理AI生成数据
-    const data = replaceId(json);
-    onSuccess?.(data);
+    onSuccess?.(json);
+    
   } catch (error) {
     message.instance.open({
       key: message.key,