Quellcode durchsuchen

feat: 新增对话历史记录相关内容

liaojiaxing vor 3 Monaten
Ursprung
Commit
d5299ff8a9
1 geänderte Dateien mit 300 neuen und 21 gelöschten Zeilen
  1. 300 21
      apps/designer/src/components/ai/Chat.tsx

+ 300 - 21
apps/designer/src/components/ai/Chat.tsx

@@ -1,4 +1,4 @@
-import React from "react";
+import React, { useEffect, useMemo, useRef } from "react";
 import {
   PlusOutlined,
   CloseOutlined,
@@ -6,12 +6,24 @@ import {
   UserOutlined,
   SendOutlined,
   LoadingOutlined,
+  EditOutlined,
+  DeleteOutlined,
 } from "@ant-design/icons";
-import { Button, Tooltip, Input, Avatar, Form } from "antd";
+import {
+  Button,
+  Tooltip,
+  Input,
+  Avatar,
+  Form,
+  Dropdown,
+  MenuProps,
+} from "antd";
 import { useChat, Message } from "ai/react";
 import MarkdownViewer from "./MarkdownViewer";
 import { uuid } from "@repo/utils";
+import { useLocalStorageState } from "ahooks";
 
+// 用户消息
 function UserMessage({ message }: { message: Message }) {
   return (
     <>
@@ -24,6 +36,7 @@ function UserMessage({ message }: { message: Message }) {
   );
 }
 
+// 助手消息
 function AssistantMessage({ message }: { message: Message }) {
   return (
     <>
@@ -47,6 +60,7 @@ function AssistantMessage({ message }: { message: Message }) {
   );
 }
 
+// 消息信息
 function MessageInfo({ message }: { message: Message }) {
   return (
     <div
@@ -62,17 +76,79 @@ function MessageInfo({ message }: { message: Message }) {
   );
 }
 
+// 消息列表
 function MessageList({ messages }: { messages: Message[] }) {
   return messages.map((message) => (
     <MessageInfo key={message.id} message={message}></MessageInfo>
   ));
 }
 
+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());
 
@@ -92,6 +168,42 @@ export default function Chat(props: { onClose?: () => void }) {
     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()) {
@@ -103,7 +215,6 @@ export default function Chat(props: { onClose?: () => void }) {
   // 开启新的对话
   const onNewChat = () => {
     setChatId(uuid());
-    setMessages([]);
     setChatStarted(false);
   };
 
@@ -118,7 +229,7 @@ export default function Chat(props: { onClose?: () => void }) {
   React.useEffect(() => {
     const scrollElement = scrollAreaRef.current;
     if (scrollElement) {
-      const observer = new ResizeObserver((entries) => {
+      observer.current = new ResizeObserver((entries) => {
         for (let entry of entries) {
           if (entry.target === scrollElement) {
             setScrollHeight(entry.target.scrollHeight);
@@ -127,14 +238,173 @@ export default function Chat(props: { onClose?: () => void }) {
         }
       });
 
-      observer.observe(scrollElement);
+      observer.current.observe(scrollElement);
 
       return () => {
-        observer.disconnect();
+        observer.current?.disconnect();
       };
     }
   }, [messages]);
 
+  // 添加一个 useEffect 来监听 chatId 的变化
+  useEffect(() => {
+    // 当 chatId 变化时,从历史记录中找到对应的消息
+    const currentChat = history?.find((item) => item.id === chatId);
+    if (currentChat) {
+      observer.current?.disconnect();
+      // 清空可滚动高度
+      setScrollHeight(0);
+      setTimeout(() => {
+        setMessages(currentChat.messages);
+        setChatStarted(true);
+      }, 100);
+    }
+  }, [chatId]);
+
+  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>
+              <div className="h-full w-50px text-right absolute right-0 top-0 bg-#fff opacity-0 hover:opacity-100">
+                <EditOutlined onClick={(e) => {
+                  e.stopPropagation();
+                }}/>
+                <DeleteOutlined/>
+              </div>
+            </div>
+          ),
+          onClick: () => {
+            if (item.id === chatId) return;
+
+            setChatId(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]);
+
   return (
     <div className="flex-1 h-full flex flex-col">
       <div className="chat-head w-full h-40px px-10px color-#333 flex items-center justify-between">
@@ -148,13 +418,20 @@ export default function Chat(props: { onClose?: () => void }) {
               onClick={onNewChat}
             ></Button>
           </Tooltip>
-          <Tooltip title="历史记录">
-            <Button
-              type="text"
-              size="small"
-              icon={<FieldTimeOutlined />}
-            ></Button>
-          </Tooltip>
+          <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"
@@ -196,14 +473,16 @@ export default function Chat(props: { onClose?: () => void }) {
                 )}
               </div>
               {error && (
-                <div className="flex justify-center items-center h-40px">
-                  <Button
-                    type="primary"
-                    shape="circle"
-                    onClick={() => reload()}
-                  >
-                    重试
-                  </Button>
+                <div>
+                  <div className="text-center">请求失败:{error.message}</div>
+                  <div className="flex justify-center items-center h-40px">
+                    <Button
+                      type="primary"
+                      onClick={() => reload()}
+                    >
+                      重试
+                    </Button>
+                  </div>
                 </div>
               )}
             </div>