Parcourir la source

feat: 添加AI创建面板

liaojiaxing il y a 2 mois
Parent
commit
3bcc819bfa

+ 1 - 1
apps/designer/.umirc.ts

@@ -9,7 +9,7 @@ export default defineConfig({
     '/favicon.ico'
   ],
   styles: [
-    '//at.alicdn.com/t/c/font_4676747_cuvm3rg4xsa.css'
+    '//at.alicdn.com/t/c/font_4676747_pixg9qt9e3r.css'
   ],
   metas: [
     { name: 'viewport', content: 'width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no' }

+ 1 - 1
apps/designer/src/components/ai/Chat.tsx

@@ -138,7 +138,7 @@ function groupDataByDate(data: ChatHistoryItem[]): DateGroups {
   return groups;
 }
 
-export default function Chat(props: { onClose?: () => void }) {
+export default function AIChat(props: { onClose?: () => void }) {
   const [focused, setFocused] = React.useState(false);
   const [chatStarted, setChatStarted] = React.useState(false);
   const [scrollHeight, setScrollHeight] = React.useState(0);

+ 433 - 0
apps/designer/src/components/ai/AiCreator.tsx

@@ -0,0 +1,433 @@
+import React, { useEffect, useMemo, useRef, useState } from "react";
+import {
+  CloseOutlined,
+  FieldTimeOutlined,
+  SendOutlined,
+  LoadingOutlined,
+  EditOutlined,
+  DeleteOutlined,
+  CaretDownOutlined,
+} from "@ant-design/icons";
+import { Button, Tooltip, Input, Form, Dropdown, MenuProps } from "antd";
+import { useChat, Message } from "ai/react";
+import { uuid } from "@repo/utils";
+import { useLocalStorageState } from "ahooks";
+
+interface ChatHistoryItem {
+  id: string;
+  messages: Message[];
+  createdAt: number;
+  updatedAt: number;
+  title: string;
+}
+
+interface DateGroups {
+  today: ChatHistoryItem[];
+  yesterday: ChatHistoryItem[];
+  last7Days: ChatHistoryItem[];
+  last30Days: ChatHistoryItem[];
+  older: ChatHistoryItem[];
+}
+
+// 对历史记录进行分组
+function groupDataByDate(data: ChatHistoryItem[]): DateGroups {
+  const normalizeDate = (date: Date | string): Date => {
+    const d = new Date(date);
+    d.setHours(0, 0, 0, 0);
+    return d;
+  };
+
+  const now = normalizeDate(new Date()); // 当前日期归一化
+
+  const groups: DateGroups = {
+    today: [],
+    yesterday: [],
+    last7Days: [],
+    last30Days: [],
+    older: [],
+  };
+
+  data.forEach((item: ChatHistoryItem) => {
+    const itemDate = normalizeDate(new Date(item.updatedAt));
+    const diffTime = now.getTime() - itemDate.getTime();
+    const diffDays = Math.floor(diffTime / (1000 * 3600 * 24));
+
+    if (diffDays === 0) {
+      groups.today.push(item);
+    } else if (diffDays === 1) {
+      groups.yesterday.push(item);
+    } else if (diffDays <= 6) {
+      groups.last7Days.push(item);
+    } else if (diffDays <= 29) {
+      groups.last30Days.push(item);
+    } else {
+      groups.older.push(item);
+    }
+  });
+
+  return groups;
+}
+
+export default function Chat(props: { onClose?: () => void }) {
+  const [focused, setFocused] = React.useState(false);
+  const [chatStarted, setChatStarted] = React.useState(false);
+  const [scrollHeight, setScrollHeight] = React.useState(0);
+  const scrollAreaRef = React.useRef<HTMLDivElement>(null);
+  const observer = useRef<ResizeObserver | null>(null);
+  // 对话历史
+  const [history, setHistory] = useLocalStorageState<ChatHistoryItem[]>(
+    "chat-history",
+    { defaultValue: [] }
+  );
+
+  const [chatId, setChatId] = React.useState(uuid());
+
+  const {
+    messages,
+    input,
+    handleInputChange,
+    handleSubmit,
+    isLoading,
+    stop,
+    error,
+    reload,
+    setMessages,
+  } = useChat({
+    api: "http://localhost:3000/ai/chat",
+    keepLastMessageOnError: true,
+    id: chatId,
+  });
+
+  useEffect(() => {
+    // 判断messages是否存在消息
+    if (!messages.length) {
+      return;
+    }
+    // 再判断历史记录是否存在该聊天,存在则更新记录
+    if (history?.find((item) => item.id === chatId)) {
+      setHistory(
+        history?.map((item) => {
+          if (item.id === chatId) {
+            return {
+              ...item,
+              messages,
+              updatedAt: Date.now(),
+            };
+          }
+          return item;
+        })
+      );
+    } else {
+      setHistory((history) => {
+        const title =
+          messages.find((message) => message.role === "user")?.content ||
+          "新对话";
+        const newData = {
+          id: chatId,
+          messages,
+          createdAt: Date.now(),
+          updatedAt: Date.now(),
+          title,
+        };
+        return history ? [newData, ...history] : [newData];
+      });
+    }
+  }, [messages, chatId]);
+
+  // 处理提交
+  const onSubmit = () => {
+    if (input.trim()) {
+      handleSubmit();
+      if (!chatStarted) setChatStarted(true);
+    }
+  };
+
+  React.useEffect(() => {
+    return () => {
+      // 取消所有进行中的请求
+      const controller = new AbortController();
+      controller.abort();
+    };
+  }, []);
+
+  const [hoverId, setHoverId] = useState<string | null>(null);
+
+  const items = useMemo(() => {
+    const hasCurrentChat = history?.find((item) => item.id === chatId);
+    const groups = groupDataByDate(
+      hasCurrentChat
+        ? history || []
+        : [
+            {
+              id: chatId,
+              messages,
+              createdAt: Date.now(),
+              updatedAt: Date.now(),
+              title:
+                messages.find((message) => message.role === "user")?.content ||
+                "新对话",
+            },
+            ...(history || []),
+          ]
+    );
+
+    // 获取items
+    const getItems = (list: ChatHistoryItem[]) => {
+      return (list || []).map((item) => {
+        return {
+          key: item.id,
+          label: (
+            <div className="w-180px relative">
+              <div className="w-full flex">
+                <span
+                  className="truncate"
+                  style={{
+                    overflow: "hidden",
+                    whiteSpace: "nowrap",
+                    textOverflow: "ellipsis",
+                  }}
+                >
+                  <Tooltip title={item.title}>{item.title}</Tooltip>
+                </span>
+                {item.id === chatId ? (
+                  <span className="text-12px color-#999 flex-shrink-0">
+                    (当前)
+                  </span>
+                ) : null}
+              </div>
+              {/* TODO: 添加编辑删除按钮 */}
+              {hoverId === item.id && (
+                <div className="h-full w-50px text-right absolute right-0 top-0 bg-#fff">
+                  <EditOutlined
+                    onClick={(e) => {
+                      e.stopPropagation();
+                    }}
+                  />
+                  <DeleteOutlined />
+                </div>
+              )}
+            </div>
+          ),
+          onClick: () => {
+            if (item.id === chatId) return;
+
+            setChatId(item.id);
+          },
+          onMouseOver: () => {
+            setHoverId(item.id);
+          },
+        };
+      });
+    };
+
+    const today = groups.today.length
+      ? [
+          {
+            key: "today",
+            label: "今天",
+            disabled: true,
+          },
+          {
+            key: "today-divider",
+            type: "divider",
+          },
+          ...getItems(groups.today),
+        ]
+      : [];
+
+    const yesterday = groups.yesterday.length
+      ? [
+          {
+            key: "yesterday",
+            label: "昨天",
+            disabled: true,
+          },
+          {
+            key: "yesterday-divider",
+            type: "divider",
+          },
+          ...getItems(groups.yesterday),
+        ]
+      : [];
+
+    const last7Days = groups.last7Days.length
+      ? [
+          {
+            key: "last7Days",
+            label: "最近7天",
+            disabled: true,
+          },
+          {
+            key: "last7Days-divider",
+            type: "divider",
+          },
+          ...getItems(groups.last7Days),
+        ]
+      : [];
+
+    const last30Days = groups.last30Days.length
+      ? [
+          {
+            key: "last30Days",
+            label: "最近30天",
+            disabled: true,
+          },
+          {
+            key: "last30Days-divider",
+            type: "divider",
+          },
+          ...getItems(groups.last30Days),
+        ]
+      : [];
+
+    const older = groups.older.length
+      ? [
+          {
+            key: "older",
+            label: "更早",
+            disabled: true,
+          },
+          {
+            key: "older-divider",
+            type: "divider",
+          },
+          ...getItems(groups.older),
+        ]
+      : [];
+
+    return [
+      ...today,
+      ...yesterday,
+      ...last7Days,
+      ...last30Days,
+      ...older,
+    ] as MenuProps["items"];
+  }, [messages, chatId]);
+
+  const handleList = [
+    {
+      key: "1",
+      label: "风格美化",
+      icon: "icon-yijianmeihua",
+      color: "#a171f2",
+    },
+    {
+      key: "2",
+      label: "语法修复",
+      icon: "icon-tubiao_yufajiucuo",
+      color: "#00c4ad",
+    },
+    {
+      key: "3",
+      label: "翻译为英文",
+      icon: "icon-fanyiweiyingwen",
+      color: "#8c4ff0",
+    },
+    {
+      key: "4",
+      label: "翻译为中文",
+      icon: "icon-fanyiweizhongwen",
+      color: "#3d72fb",
+    },
+  ];
+
+  return (
+    <div className="flex-1 h-full">
+      <div className="chat-head w-full h-40px px-10px color-#333 flex items-center justify-between">
+        <i className="iconfont icon-AIchuangzuo"></i>
+        <span>
+          <Dropdown
+            menu={{ items }}
+            trigger={["click"]}
+            placement="bottomLeft"
+            arrow
+          >
+            <Tooltip title="历史记录">
+              <Button
+                type="text"
+                size="small"
+                icon={<FieldTimeOutlined />}
+              ></Button>
+            </Tooltip>
+          </Dropdown>
+          <Button
+            type="text"
+            size="small"
+            icon={<CloseOutlined />}
+            onClick={() => props.onClose?.()}
+          ></Button>
+        </span>
+      </div>
+
+      <div className="text-14px pl-12px text-#333">绘制图形</div>
+      <div
+        className="chat-content bg-#f5f5f5 px-10px overflow-y-auto mt-12px"
+        ref={scrollAreaRef}
+      >
+        <div
+          style={{
+            borderColor: focused ? "#1890ff" : "#ddd",
+          }}
+          className="chat-foot bg-#fff rounded-10px border border-solid border-1px shadow-sm"
+        >
+          <Dropdown menu={{ items: [] }} placement="bottomLeft">
+            <div className="text-12px pl-10px pt-10px">
+              帮我绘制-流程图
+              <CaretDownOutlined />
+            </div>
+          </Dropdown>
+          <Form onFinish={onSubmit}>
+            <Input.TextArea
+              rows={3}
+              autoSize={{ maxRows: 3, minRows: 3 }}
+              placeholder="你可以这样问:用户登陆流程图"
+              variant="borderless"
+              onFocus={() => setFocused(true)}
+              onBlur={() => setFocused(false)}
+              value={input}
+              onChange={handleInputChange}
+              disabled={isLoading}
+              onPressEnter={onSubmit}
+            />
+            <div className="text-right p-10px">
+              {isLoading ? (
+                <Tooltip title="停止生成">
+                  <Button
+                    type="primary"
+                    shape="circle"
+                    icon={<i className="iconfont icon-stopcircle" />}
+                    onClick={stop}
+                  ></Button>
+                </Tooltip>
+              ) : (
+                <Button
+                  type="text"
+                  icon={<SendOutlined />}
+                  disabled={!input.trim()}
+                  htmlType="submit"
+                ></Button>
+              )}
+            </div>
+          </Form>
+        </div>
+      </div>
+
+      <div className="text-14px pl-12px text-#333 mt-32px">图形处理</div>
+      <div className="flex flex-wrap gap-10px p-10px">
+        {handleList.map((item) => (
+          <div
+            key={item.key}
+            className="flex-[40%] h-50px bg-#fff rounded-10px shadow-sm flex items-center pl-10px text-12px cursor-pointer"
+            style={{}}
+          >
+            <i
+              className={`iconfont ${item.icon} text-16px`}
+              style={{ color: item.color }}
+            ></i>
+            <span className="ml-10px">{item.label}</span>
+          </div>
+        ))}
+      </div>
+    </div>
+  );
+}

+ 5 - 3
apps/designer/src/components/ai/MarkdownViewer.tsx

@@ -22,9 +22,11 @@ const CodeHeader: React.FC<{
       <span className="text-xs text-#fff">{language}</span>
     </div>
 
-    <span className="cursor-pointer text-xs" onClick={onCopy}>
-      {copied ? "已复制!" : "复制代码"}
-    </span>
+    <div>
+      <span className="cursor-pointer text-xs" onClick={onCopy}>
+        {copied ? "已复制!" : "复制代码"}
+      </span>
+    </div>
   </div>
 );
 

+ 4 - 4
apps/designer/src/models/appModel.ts

@@ -36,8 +36,8 @@ export default function appModel() {
   const [leftPanelActiveKey, setLeftPanelActiveKey] = useState("1");
   // 历史记录
   const [showHistory, setShowHistory] = useState(false);
-  // 激活AI对话
-  const [activeAIChat, setActiveAIChat] = useState(false);
+  // 激活AI对话 chat对话 creator创作
+  const [activeAI, setActiveAI] = useState<'chat' | 'creator'>();
   // 右侧面板tab activeKey
   const [rightPanelTabActiveKey, setRightPanelTabActiveKey] = useState("1");
   // 右侧面板宽度
@@ -182,8 +182,8 @@ export default function appModel() {
     setLeftPanelActiveKey,
     showHistory,
     setShowHistory,
-    activeAIChat,
-    setActiveAIChat,
+    activeAI,
+    setActiveAI,
     rightPanelWidth,
     setRightPanelWidth,
   }

+ 11 - 7
apps/designer/src/pages/flow/components/Config/index.tsx

@@ -5,8 +5,8 @@ import GraphStyle from "./GraphStyle";
 import NodeAttrs from "@/components/NodeAttrs";
 import { useModel } from "umi";
 import InsetCss from "insert-css";
-import Chat from "@/components/ai/Chat";
-import { debounce, set } from "lodash-es";
+import AIChat from "@/components/ai/AIChat";
+import AICreator from "@/components/ai/AiCreator";
 
 InsetCss(`
   .shalu-tabs {
@@ -24,8 +24,8 @@ export default function Config() {
   const {
     rightPanelTabActiveKey,
     setRightPanelTabActiveKey,
-    activeAIChat,
-    setActiveAIChat,
+    activeAI,
+    setActiveAI,
     rightPanelWidth,
     setRightPanelWidth,
   } = useModel("appModel");
@@ -145,9 +145,13 @@ export default function Config() {
         onMouseDown={handleMouseDown}
       ></div>
       <div className="flex-1 overflow-hidden">
-        {activeAIChat ? (
-          <Chat onClose={() => setActiveAIChat(false)} />
-        ) : (
+        {activeAI === "chat" && (
+          <AIChat onClose={() => setActiveAI(undefined)} />
+        )}
+        {activeAI === "creator" && (
+          <AICreator onClose={() => setActiveAI(undefined)} />
+        )}
+        {activeAI === undefined && (
           <Tabs
             centered
             items={tabItems}

+ 48 - 20
apps/designer/src/pages/flow/components/ToolBar/index.tsx

@@ -43,8 +43,8 @@ export default function ToolBar() {
     toggleFormatBrush,
     enableFormatBrush,
     pageState,
-    activeAIChat,
-    setActiveAIChat,
+    activeAI,
+    setActiveAI,
   } = useModel("appModel");
   const { canRedo, canUndo, onRedo, onUndo, selectedCell, graph } =
     useModel("graphModel");
@@ -74,12 +74,12 @@ export default function ToolBar() {
     handleFindPrev,
     findCount,
     currentIndex,
-    setInstance
+    setInstance,
   } = useFindReplace(graph);
 
   useEffect(() => {
     graph && setInstance(graph);
-  }, [graph])
+  }, [graph]);
 
   const findModalRef = useRef<any>();
   const mermaidModelRef = useRef<any>();
@@ -166,8 +166,8 @@ export default function ToolBar() {
 
   // 插入mermaid代码结果数据
   const handleInsertMermaid = (res?: MermaidResult) => {
-    if(res) {
-      if(res.type === 'image') {
+    if (res) {
+      if (res.type === "image") {
         const node = graph?.addNode({
           ...BaseNode,
           data: {
@@ -179,8 +179,8 @@ export default function ToolBar() {
             },
           },
           size: {
-            width: parseInt((res.width || 100)+''),
-            height: parseInt((res.height || 100) + ''),
+            width: parseInt((res.width || 100) + ""),
+            height: parseInt((res.height || 100) + ""),
           },
         });
         // 右键菜单
@@ -196,7 +196,7 @@ export default function ToolBar() {
         graph?.cleanSelection();
         // cell数据
         res.data?.forEach((cell: Cell.Metadata) => {
-          if(cell?.source && cell?.target) {
+          if (cell?.source && cell?.target) {
             const node = graph?.addEdge(cell);
             // 右键菜单
             node?.addTools({
@@ -219,7 +219,7 @@ export default function ToolBar() {
         });
       }
     }
-  }
+  };
 
   return (
     <div className={styles.toolBar}>
@@ -772,18 +772,46 @@ export default function ToolBar() {
           right={30}
           top={110}
         />
-        <MermaidModal ref={mermaidModelRef} onChange={handleInsertMermaid}/>
+        <MermaidModal ref={mermaidModelRef} onChange={handleInsertMermaid} />
         <div>
-          <Tooltip placement="bottom" title="打开聊天">
-            <Button
-              type="text"
-              icon={<i className="iconfont icon-AI" />}
-              className={activeAIChat ? "active" : ""}
-              style={{marginRight: 16}}
-              onClick={() => {
-                setActiveAIChat(!activeAIChat);
+          <Tooltip placement="bottom" title="AI助手">
+            <Dropdown
+              menu={{
+                items: [
+                  {
+                    key: "1",
+                    label: (
+                      <div>
+                        <i className="iconfont icon-duihua mr-8px" />
+                        AI对话
+                      </div>
+                    ),
+                    onClick: () => {
+                      setActiveAI("chat");
+                    },
+                  },
+                  {
+                    key: "2",
+                    label: (
+                      <div>
+                        <i className="iconfont icon-AIchuangzuo mr-8px" />
+                        AI创作
+                      </div>
+                    ),
+                    onClick: () => {
+                      setActiveAI("creator");
+                    },
+                  },
+                ],
               }}
-            />
+            >
+              <Button
+                type="text"
+                icon={<i className="iconfont icon-AI" />}
+                className={activeAI ? "active" : ""}
+                style={{ marginRight: 16 }}
+              />
+            </Dropdown>
           </Tooltip>
           <Tooltip placement="bottom" title="替换">
             <Button