AiCreator.tsx 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444
  1. import React, { useRef, useState } from "react";
  2. import { CloseOutlined, LoadingOutlined, SendOutlined } from "@ant-design/icons";
  3. import { Button, Tooltip, Input, Form, message } from "antd";
  4. import { useChat } from "@/hooks/useChat";
  5. import { Cell } from "@antv/x6";
  6. import { handleParseFowlAIData } from "@/utils";
  7. import { useModel } from "umi";
  8. import { traverseNode } from "@/utils/mindmapHander";
  9. export default function AICreator(props: {
  10. type: "mindmap" | "flow";
  11. onClose?: () => void;
  12. onChange?: (data: any) => void;
  13. onError?: (err: Error) => void;
  14. }) {
  15. const [focused, setFocused] = React.useState(false);
  16. const [input, setInput] = useState("");
  17. const [messageApi, contextHolder] = message.useMessage();
  18. const messageKey = "ailoading";
  19. const msgContent = useRef<string>("");
  20. const { mindProjectInfo, setMindProjectInfo, graph } = useModel("mindMapModel");
  21. const { loading, onRequest, cancel } = useChat({
  22. app_name: "system_design",
  23. onUpdate: (msg) => {
  24. setInput("");
  25. msgContent.current += msg.answer;
  26. },
  27. onSuccess: (msg) => {
  28. console.log("加载完毕!", msgContent.current);
  29. messageApi.open({
  30. key: messageKey,
  31. type: "success",
  32. content: "AI创作完成",
  33. duration: 2,
  34. style: {
  35. marginTop: 300,
  36. },
  37. });
  38. handleParseFowlAIData({
  39. content: msgContent.current,
  40. message: {
  41. key: messageKey,
  42. instance: messageApi,
  43. },
  44. onSuccess: props.onChange,
  45. onError: props.onError,
  46. });
  47. },
  48. onError: (err) => {
  49. messageApi.open({
  50. key: messageKey,
  51. type: "error",
  52. content: err.message || "AI创作失败",
  53. duration: 2,
  54. style: {
  55. marginTop: 300,
  56. },
  57. });
  58. },
  59. });
  60. const handleStop = () => {
  61. cancel();
  62. messageApi.open({
  63. key: messageKey,
  64. type: "error",
  65. content: "AI创作已取消",
  66. duration: 2,
  67. style: {
  68. marginTop: 300,
  69. },
  70. });
  71. };
  72. // 处理提交
  73. const onSubmit = () => {
  74. if (input.trim()) {
  75. onRequest(`#角色:你是一个思维导图设计助手,根据需求部分的描述为用户使用设计思维导图工具生成对应的json数据
  76. #需求:${input}`, undefined, input);
  77. messageApi.open({
  78. key: messageKey,
  79. type: "loading",
  80. content: "AI创作中...",
  81. duration: 0,
  82. style: {
  83. marginTop: 300,
  84. },
  85. });
  86. }
  87. };
  88. React.useEffect(() => {
  89. return () => {
  90. // 取消所有进行中的请求
  91. const controller = new AbortController();
  92. controller.abort();
  93. };
  94. }, []);
  95. const handleList = [
  96. {
  97. key: "style",
  98. label: "风格美化",
  99. icon: "icon-yijianmeihua",
  100. color: "#a171f2",
  101. },
  102. {
  103. key: "grammar",
  104. label: "语法修复",
  105. icon: "icon-tubiao_yufajiucuo",
  106. color: "#00c4ad",
  107. },
  108. {
  109. key: "translation_en",
  110. label: "翻译为英文",
  111. icon: "icon-fanyiweiyingwen",
  112. color: "#8c4ff0",
  113. },
  114. {
  115. key: "translation_zh",
  116. label: "翻译为中文",
  117. icon: "icon-fanyiweizhongwen",
  118. color: "#3d72fb",
  119. },
  120. ];
  121. type LabelMap = Record<
  122. string,
  123. { cell: string; key: string; cellType: string }[]
  124. >;
  125. // 从元素中提取文本 当前无选中元素时,提取所有元素
  126. const getLabels = () => {
  127. const labelMap: LabelMap = {};
  128. let cells: Cell[] | undefined;
  129. cells = graph?.getSelectedCells();
  130. if (!cells || cells.length === 0) {
  131. cells = graph?.getCells();
  132. }
  133. if (!cells || !cells.length) return;
  134. cells.forEach((cell) => {
  135. const data = cell.getData();
  136. // 从label中提取文本
  137. if (data?.label?.trim()) {
  138. if (!labelMap[data.label.trim()]) {
  139. labelMap[data.label.trim()] = [
  140. { cell: cell.id, key: "label", cellType: cell.shape },
  141. ];
  142. } else {
  143. labelMap[data.label.trim()].push({
  144. cell: cell.id,
  145. key: "label",
  146. cellType: cell.shape,
  147. });
  148. }
  149. }
  150. });
  151. return labelMap;
  152. };
  153. // 替换节点文本内容
  154. const handleReplace = (labelMap: LabelMap, data: Record<string, string>) => {
  155. const keyMap: Record<string, string> = data;
  156. if(Array.isArray(data)) {
  157. data.forEach(item => {
  158. keyMap[item.original] = item.value
  159. })
  160. }
  161. mindProjectInfo && setMindProjectInfo({
  162. ...mindProjectInfo,
  163. topics: traverseNode(mindProjectInfo?.topics || [], (topic) => {
  164. // 判断当前节点是否需要替换
  165. const ids = labelMap[topic.label?.trim()]?.map(item => item.cell);
  166. if (keyMap[topic.label?.trim()] && ids?.includes(topic.id)) {
  167. topic.label = keyMap[topic.label?.trim()];
  168. }
  169. return topic
  170. })
  171. });
  172. };
  173. // 风格美化
  174. const handleStyle = () => {
  175. onRequest("生成一个美化风格的配置", {
  176. onUpdate: (data) => {
  177. console.log("style update:", data);
  178. },
  179. onSuccess: (data) => {
  180. console.log(data);
  181. },
  182. onError: (err) => {
  183. console.error(err);
  184. },
  185. });
  186. };
  187. // 语法修复
  188. const handleGrammar = () => {
  189. const labelMap = getLabels();
  190. if (!labelMap) {
  191. messageApi.open({
  192. key: messageKey,
  193. type: "info",
  194. content: "无可修复的数据",
  195. });
  196. return;
  197. }
  198. messageApi.open({
  199. key: messageKey,
  200. type: "loading",
  201. content: "AI正在修复语法...",
  202. duration: 0,
  203. style: {
  204. marginTop: 300,
  205. },
  206. });
  207. const data = JSON.stringify(Object.keys(labelMap));
  208. let result = "";
  209. onRequest(
  210. `修复语法错误,需要修复的数据:${data}`,
  211. {
  212. onUpdate: (data) => {
  213. result += data.answer;
  214. },
  215. onSuccess: (data) => {
  216. messageApi.open({
  217. key: messageKey,
  218. type: "success",
  219. content: "AI修复语法完成",
  220. duration: 2,
  221. style: {
  222. marginTop: 300,
  223. },
  224. });
  225. handleParseFowlAIData({
  226. content: result,
  227. message: {
  228. key: messageKey,
  229. instance: messageApi,
  230. },
  231. onSuccess: (data) => {
  232. handleReplace(labelMap, data);
  233. },
  234. });
  235. },
  236. onError: (err) => {
  237. console.error(err);
  238. messageApi.open({
  239. key: messageKey,
  240. type: "error",
  241. content: err.message || "AI创作失败",
  242. duration: 2,
  243. style: {
  244. marginTop: 300,
  245. },
  246. });
  247. },
  248. },
  249. "语法修复"
  250. );
  251. };
  252. // 翻译
  253. const handleTranslation = (lang: "en" | "zh") => {
  254. const labelMap = getLabels();
  255. if (!labelMap) {
  256. messageApi.open({
  257. key: messageKey,
  258. type: "info",
  259. content: "无可翻译的数据",
  260. });
  261. return;
  262. }
  263. messageApi.open({
  264. key: messageKey,
  265. type: "loading",
  266. content: "AI正在翻译...",
  267. duration: 0,
  268. style: {
  269. marginTop: 300,
  270. },
  271. });
  272. const data = JSON.stringify(Object.keys(labelMap));
  273. let result = "";
  274. onRequest(
  275. `翻译成${lang === "en" ? "英文" : "中文"},需要翻译的数据:${data}`,
  276. {
  277. onUpdate: (data) => {
  278. result += data.answer;
  279. },
  280. onSuccess: (data) => {
  281. messageApi.open({
  282. key: messageKey,
  283. type: "success",
  284. content: "AI翻译完成",
  285. duration: 2,
  286. style: {
  287. marginTop: 300,
  288. },
  289. });
  290. handleParseFowlAIData({
  291. content: result,
  292. message: {
  293. key: messageKey,
  294. instance: messageApi,
  295. },
  296. onSuccess: (data) => {
  297. handleReplace(labelMap, data);
  298. },
  299. });
  300. },
  301. onError: (err) => {
  302. console.error(err);
  303. messageApi.open({
  304. key: messageKey,
  305. type: "error",
  306. content: err.message || "AI创作失败",
  307. duration: 2,
  308. style: {
  309. marginTop: 300,
  310. },
  311. });
  312. },
  313. },
  314. "翻译内容"
  315. );
  316. };
  317. const handleAiFeature = (feature: string) => {
  318. switch (feature) {
  319. case "style":
  320. handleStyle();
  321. break;
  322. case "grammar":
  323. handleGrammar();
  324. break;
  325. case "translation_en":
  326. handleTranslation("en");
  327. break;
  328. case "translation_zh":
  329. handleTranslation("zh");
  330. break;
  331. default:
  332. break;
  333. }
  334. };
  335. return (
  336. <div
  337. className="flex-1 h-full"
  338. style={{
  339. backgroundImage: "linear-gradient(137deg, #e5f4ff 0%, #efe7ff 100%)",
  340. }}
  341. >
  342. {contextHolder}
  343. <div className="chat-head w-full h-40px px-10px color-#333 flex items-center justify-between">
  344. <span className="text-14px">
  345. <svg className="icon h-32px w-32px" aria-hidden="true">
  346. <use xlinkHref="#icon-AI1"></use>
  347. </svg>
  348. <span>AI创作</span>
  349. </span>
  350. <span>
  351. <Button
  352. type="text"
  353. size="small"
  354. icon={<CloseOutlined />}
  355. onClick={() => props.onClose?.()}
  356. ></Button>
  357. </span>
  358. </div>
  359. <div className="text-14px pl-12px text-#333">一句话生成思维导图~</div>
  360. <div className="chat-content px-10px overflow-y-auto mt-12px">
  361. <div
  362. style={{
  363. borderColor: focused ? "#1890ff" : "#ddd",
  364. }}
  365. className="chat-foot bg-#fff rounded-10px border border-solid border-1px shadow-sm"
  366. >
  367. <Form onFinish={onSubmit}>
  368. <Input.TextArea
  369. rows={3}
  370. autoSize={{ maxRows: 3, minRows: 3 }}
  371. placeholder="输入你的主题或问题"
  372. variant="borderless"
  373. onFocus={() => setFocused(true)}
  374. onBlur={() => setFocused(false)}
  375. value={input}
  376. onChange={(e) => setInput(e.target.value)}
  377. disabled={loading}
  378. onPressEnter={onSubmit}
  379. />
  380. <div className="text-right p-10px">
  381. {loading ? (
  382. <Tooltip title="停止生成">
  383. <Button
  384. type="primary"
  385. shape="circle"
  386. icon={<LoadingOutlined />}
  387. onClick={handleStop}
  388. ></Button>
  389. </Tooltip>
  390. ) : (
  391. <Button
  392. type="text"
  393. icon={<SendOutlined />}
  394. disabled={!input.trim()}
  395. htmlType="submit"
  396. ></Button>
  397. )}
  398. </div>
  399. </Form>
  400. </div>
  401. </div>
  402. <div className="text-14px pl-12px text-#333 mt-32px">图形处理</div>
  403. <div className="flex flex-wrap gap-10px p-10px">
  404. {handleList.map((item) => (
  405. <div
  406. key={item.key}
  407. className="flex-[40%] h-50px bg-#fff rounded-10px shadow-sm flex items-center pl-10px text-12px cursor-pointer"
  408. onClick={() => handleAiFeature(item.key)}
  409. >
  410. <i
  411. className={`iconfont ${item.icon} text-16px`}
  412. style={{ color: item.color }}
  413. ></i>
  414. <span className="ml-10px">{item.label}</span>
  415. </div>
  416. ))}
  417. </div>
  418. </div>
  419. );
  420. }