|
@@ -0,0 +1,278 @@
|
|
|
+import React, { useMemo, useRef, useState } from "react";
|
|
|
+import { Modal, message } from "antd";
|
|
|
+import type { DraggableData, DraggableEvent } from "react-draggable";
|
|
|
+import Draggable from "react-draggable";
|
|
|
+import { Sender, Welcome, Prompts } from "@ant-design/x";
|
|
|
+import { PromptsProps } from "@ant-design/x";
|
|
|
+import aiLogo from "@/assets/icon-ai-3.png";
|
|
|
+import { CoffeeOutlined, FireOutlined, SmileOutlined } from "@ant-design/icons";
|
|
|
+import { useChat } from "@/hooks/useChat";
|
|
|
+
|
|
|
+type AICteatorProps = {
|
|
|
+ trigger: JSX.Element;
|
|
|
+ onChange?: (data: any) => void;
|
|
|
+ onError?: (err: Error) => void;
|
|
|
+ position?: {
|
|
|
+ top?: number | string;
|
|
|
+ left?: number | string;
|
|
|
+ bottom?: number | string;
|
|
|
+ right?: number | string;
|
|
|
+ };
|
|
|
+};
|
|
|
+
|
|
|
+const items: PromptsProps["items"] = [
|
|
|
+ {
|
|
|
+ key: "6",
|
|
|
+ icon: <CoffeeOutlined style={{ color: "#964B00" }} />,
|
|
|
+ description: "帮我创建一个用户表",
|
|
|
+ disabled: false,
|
|
|
+ },
|
|
|
+ {
|
|
|
+ key: "7",
|
|
|
+ icon: <SmileOutlined style={{ color: "#FAAD14" }} />,
|
|
|
+ description: "创建一个订单表",
|
|
|
+ disabled: false,
|
|
|
+ },
|
|
|
+ // {
|
|
|
+ // key: "8",
|
|
|
+ // icon: <FireOutlined style={{ color: "#FF4D4F" }} />,
|
|
|
+ // description: "创建一个商品表",
|
|
|
+ // disabled: false,
|
|
|
+ // },
|
|
|
+];
|
|
|
+
|
|
|
+export default (props: AICteatorProps) => {
|
|
|
+ const [open, setOpen] = useState(false);
|
|
|
+ const [disabled, setDisabled] = useState(true);
|
|
|
+ const [bounds, setBounds] = useState({
|
|
|
+ left: 0,
|
|
|
+ top: 0,
|
|
|
+ bottom: 0,
|
|
|
+ right: 0,
|
|
|
+ });
|
|
|
+ const [input, setInput] = useState("");
|
|
|
+ const draggleRef = useRef<HTMLDivElement>(null!);
|
|
|
+ const [messageApi, contextHolder] = message.useMessage();
|
|
|
+ const msgContent = useRef<string>("");
|
|
|
+ const messageKey = "data-model";
|
|
|
+
|
|
|
+ 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 { loading, onRequest, cancel } = useChat({
|
|
|
+ app_name: "data_model",
|
|
|
+ onUpdate: (msg) => {
|
|
|
+ setInput("");
|
|
|
+ msgContent.current += msg.answer;
|
|
|
+ },
|
|
|
+ onSuccess: (msg) => {
|
|
|
+ console.log("加载完毕!", msgContent.current);
|
|
|
+ messageApi.open({
|
|
|
+ key: messageKey,
|
|
|
+ type: "success",
|
|
|
+ content: "AI创作完成",
|
|
|
+ duration: 2,
|
|
|
+ style: {
|
|
|
+ marginTop: 300,
|
|
|
+ },
|
|
|
+ });
|
|
|
+ handleParse();
|
|
|
+ },
|
|
|
+ onError: (err) => {
|
|
|
+ messageApi.open({
|
|
|
+ key: messageKey,
|
|
|
+ type: "error",
|
|
|
+ content: err.message || "AI创作失败",
|
|
|
+ duration: 2,
|
|
|
+ style: {
|
|
|
+ marginTop: 300,
|
|
|
+ },
|
|
|
+ });
|
|
|
+ },
|
|
|
+ });
|
|
|
+
|
|
|
+ const triggerDom = React.cloneElement(props.trigger, {
|
|
|
+ ...props.trigger.props,
|
|
|
+ onClick: () => {
|
|
|
+ setOpen(!open);
|
|
|
+ },
|
|
|
+ });
|
|
|
+
|
|
|
+ const onStart = (_event: DraggableEvent, uiData: DraggableData) => {
|
|
|
+ const { clientWidth, clientHeight } = window.document.documentElement;
|
|
|
+ const targetRect = draggleRef.current?.getBoundingClientRect();
|
|
|
+ if (!targetRect) {
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ setBounds({
|
|
|
+ left: -targetRect.left + uiData.x,
|
|
|
+ right: clientWidth - (targetRect.right - uiData.x),
|
|
|
+ top: -targetRect.top + uiData.y,
|
|
|
+ bottom: clientHeight - (targetRect.bottom - uiData.y),
|
|
|
+ });
|
|
|
+ };
|
|
|
+
|
|
|
+ const onSubmit = (value: string) => {
|
|
|
+ if (value.trim()) {
|
|
|
+ const query = `设计一个数据模型内容,需求如下:${value.trim()}`;
|
|
|
+ onRequest(query, undefined, value);
|
|
|
+
|
|
|
+ messageApi.open({
|
|
|
+ key: messageKey,
|
|
|
+ type: "loading",
|
|
|
+ content: (
|
|
|
+ <span>
|
|
|
+ <svg className="icon mr-4px color-#666" aria-hidden="true">
|
|
|
+ <use xlinkHref="#icon-AI1"></use>
|
|
|
+ </svg>
|
|
|
+ <span>AI创作中...</span>
|
|
|
+ </span>
|
|
|
+ ),
|
|
|
+ duration: 0,
|
|
|
+ style: {
|
|
|
+ marginTop: 300,
|
|
|
+ },
|
|
|
+ });
|
|
|
+ }
|
|
|
+ };
|
|
|
+
|
|
|
+ const onStop = () => {
|
|
|
+ cancel();
|
|
|
+ msgContent.current = "";
|
|
|
+ messageApi.open({
|
|
|
+ key: messageKey,
|
|
|
+ type: "error",
|
|
|
+ content: "AI创作已取消",
|
|
|
+ duration: 2,
|
|
|
+ style: {
|
|
|
+ marginTop: 300,
|
|
|
+ },
|
|
|
+ });
|
|
|
+ };
|
|
|
+
|
|
|
+ const handlePromptClick = (item: any) => {
|
|
|
+ const msg = item.data.description || item.data.label;
|
|
|
+ onSubmit(msg);
|
|
|
+ };
|
|
|
+
|
|
|
+ const getStyle: React.CSSProperties = useMemo(
|
|
|
+ () =>
|
|
|
+ props.position
|
|
|
+ ? { position: "absolute", ...props.position }
|
|
|
+ : { position: "absolute", top: 114, right: 18 },
|
|
|
+ [props.position]
|
|
|
+ );
|
|
|
+
|
|
|
+ return (
|
|
|
+ <>
|
|
|
+ {triggerDom}
|
|
|
+ {contextHolder}
|
|
|
+ <Modal
|
|
|
+ title={
|
|
|
+ <div
|
|
|
+ style={{ width: "100%", cursor: "move" }}
|
|
|
+ onMouseOver={() => disabled && setDisabled(false)}
|
|
|
+ onMouseOut={() => setDisabled(true)}
|
|
|
+ >
|
|
|
+ <svg className="icon h-16px w-16px color-#666" aria-hidden="true">
|
|
|
+ <use xlinkHref="#icon-AI1"></use>
|
|
|
+ </svg>
|
|
|
+ <span className="ml-4px">AI助手</span>
|
|
|
+ </div>
|
|
|
+ }
|
|
|
+ mask={false}
|
|
|
+ maskClosable={false}
|
|
|
+ open={open}
|
|
|
+ width={440}
|
|
|
+ style={getStyle}
|
|
|
+ styles={{
|
|
|
+ content: {
|
|
|
+ backgroundImage:
|
|
|
+ "linear-gradient(137deg, #e5f4ff 0%, #efe7ff 100%)",
|
|
|
+ },
|
|
|
+ header: { background: "transparent" },
|
|
|
+ }}
|
|
|
+ footer={null}
|
|
|
+ destroyOnClose
|
|
|
+ wrapClassName="ai-modal-wrapper"
|
|
|
+ onCancel={() => setOpen(false)}
|
|
|
+ modalRender={(modal) => (
|
|
|
+ <Draggable
|
|
|
+ disabled={disabled}
|
|
|
+ bounds={bounds}
|
|
|
+ nodeRef={draggleRef}
|
|
|
+ onStart={(event, uiData) => onStart(event, uiData)}
|
|
|
+ >
|
|
|
+ <div ref={draggleRef}>{modal}</div>
|
|
|
+ </Draggable>
|
|
|
+ )}
|
|
|
+ >
|
|
|
+ <div className="my-10">
|
|
|
+ <Welcome
|
|
|
+ variant="borderless"
|
|
|
+ icon={<img src={aiLogo} className="rounded-lg" alt="AI Logo" />}
|
|
|
+ title="你好,我是数据模型AI助手"
|
|
|
+ description="你需要创建什么的内容,我可以帮你快速生成~"
|
|
|
+ />
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <Prompts
|
|
|
+ className="mb-10"
|
|
|
+ items={items}
|
|
|
+ vertical
|
|
|
+ onItemClick={handlePromptClick}
|
|
|
+ />
|
|
|
+
|
|
|
+ <Sender
|
|
|
+ placeholder="如:创建一个用户表"
|
|
|
+ loading={loading}
|
|
|
+ value={input}
|
|
|
+ onChange={setInput}
|
|
|
+ onSubmit={onSubmit}
|
|
|
+ onCancel={onStop}
|
|
|
+ />
|
|
|
+ </Modal>
|
|
|
+ </>
|
|
|
+ );
|
|
|
+};
|