AiCreator.tsx 13 KB

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