Pārlūkot izejas kodu

feat "zhurui:AI对话框组件"

Leonezhurui 2 gadi atpakaļ
vecāks
revīzija
376bcb0b89

+ 78 - 0
src/components/AIChatter/AIChatter.module.scss

@@ -0,0 +1,78 @@
+.container {
+  position: relative;
+  width: 100%;
+  height: 100%;
+  padding: 10px;
+  box-sizing: border-box;
+
+  .header {
+    position: relative;
+    padding-bottom: 10px;
+
+    &::after {
+      content: "";
+      position: absolute;
+      left: 0;
+      right: 0;
+      bottom: 0;
+      height: 10px; /* 控制阴影的高度 */
+      box-shadow: 0 8px 8px -4px rgba(0,0,0,0.2); /* 自定义阴影的大小和颜色 */
+      z-index: -1; /* 确保阴影在内容下方 */
+    }
+  }
+
+  .message-box {
+    width: 100%;
+    height: 80%;
+    overflow-y: auto;
+
+    .item {
+      display: flex;
+      align-items: flex-start;
+      margin-bottom: 15px;
+
+      .content {
+        background-color: #f0f0f0;
+        padding: 8px 12px;
+        border-radius: 16px;
+        word-break: break-word;
+        box-shadow: 0 1px 2px rgba(0, 0, 0, 0.1);
+      }
+    }
+  }
+
+  .question-box {
+    position: absolute;
+    bottom: 10px;
+    left: 10px;
+    right: 10px;
+    height: 10%;
+
+    &::before {
+      content: "";
+      position: absolute;
+      left: 0;
+      right: 0;
+      top: -10px; /* 将阴影向上偏移 */
+      height: 10px; /* 控制阴影的高度 */
+      box-shadow: 0 -8px 8px -4px rgba(0,0,0,0.2); /* 自定义阴影的大小和颜色,注意垂直偏移和扩展半径为负值 */
+      z-index: -1;
+    }
+
+    .input-group {
+      display: flex;
+      align-items: center;
+
+      border: 1px solid #d9d9d9;
+      border-radius: 5px;
+
+      .button {
+
+        &:hover {
+          background-color: #d9d9d9;
+        }
+      }
+    }
+  }
+
+}

+ 166 - 0
src/components/AIChatter/AIChatter.tsx

@@ -0,0 +1,166 @@
+import React, {useRef, useState} from "react";
+import EDropdownSelect from "@/components/EDropdownSelect/EDropdownSelect";
+import styles from "./AIChatter.module.scss"
+import {Avatar, Button, Dropdown, Input, Menu, MenuProps, Spin} from "antd";
+import {ArrowUpOutlined, DownOutlined} from "@ant-design/icons";
+
+const AIChatter = () => {
+    const modelOptions = ["Seecoder AI", "ChatGPT3.5", "ChatGLM3"]
+    const AIAvatar = "https://cdn.pixabay.com/photo/2023/11/14/11/07/sparrow-8387465_1280.jpg"
+    const userAvatar = "https://cdn.pixabay.com/photo/2023/07/22/08/54/european-shorthair-8142959_1280.jpg"
+    const questionTemplates = [
+        "请问您提供哪些服务?",
+        "您的营业时间是?",
+        "我想了解更多关于产品A的信息。",
+        // 更多问题模板...
+    ];
+
+    const items: MenuProps['items'] = questionTemplates.map((template, index) => ({
+        key: index,
+        label: (
+            <div className={"option-item"}>{template}</div>
+        )
+    }))
+
+    const inputRef = useRef<any>(null); // 创建引用
+
+
+    const [data, setData] = useState({
+        model: modelOptions[0],
+        questionContent: '',
+        messages: [
+            {
+                "content": "你好,你是什么AI工具?",
+                "senderType": "User",
+                "timestamp": new Date("2024-02-29 18:00:00")
+            },
+            {
+                "content": "我是Seecoder AI,是南京大学软件学院Seecoder组的AI制品,服务于软件工程教育、英文写作等工作!",
+                "senderType": "AI",
+                "timestamp": new Date("2024-02-29 18:01:10")
+            },
+            {
+                "content": "你好,你是什么AI工具?",
+                "senderType": "User",
+                "timestamp": new Date("2024-02-29 18:00:00")
+            },
+            {
+                "content": "我是Seecoder AI,是南京大学软件学院Seecoder组的AI制品,服务于软件工程教育、英文写作等工作!",
+                "senderType": "AI",
+                "timestamp": new Date("2024-02-29 18:01:10")
+            },
+            {
+                "content": "你好,你是什么AI工具?",
+                "senderType": "User",
+                "timestamp": new Date("2024-02-29 18:00:00")
+            },
+            {
+                "content": "我是Seecoder AI,是南京大学软件学院Seecoder组的AI制品,服务于软件工程教育、英文写作等工作!",
+                "senderType": "AI",
+                "timestamp": new Date("2024-02-29 18:01:10")
+            },
+            {
+                "content": "你好,你是什么AI工具?",
+                "senderType": "User",
+                "timestamp": new Date("2024-02-29 18:00:00")
+            },
+            {
+                "content": "我是Seecoder AI,是南京大学软件学院Seecoder组的AI制品,服务于软件工程教育、英文写作等工作!",
+                "senderType": "AI",
+                "timestamp": new Date("2024-02-29 18:01:10")
+            }
+        ],
+        loading: false, // 请求加载过程
+    });
+
+    // 更新选中值的函数
+    const setValue = (model: string) => {
+        console.log("父组件中接受到的值", model)
+        setData(prev => ({ ...prev, model }));
+    };
+
+    const handleSendQuestion = () => {
+        setData(prev => ({...prev, loading: true}))
+        let question = data.questionContent;
+        console.log("发送请求,请求参数为", question);
+        setData(prev => ({...prev, questionContent: ''}))
+        // TODO 请求方法,设置loading
+        setTimeout(() => {
+            setData(prev => ({...prev, loading: false}))
+        }, 3000)
+    }
+
+    const handleMenuClick = ({ key }: any) => {
+        setData(prev => ({...prev, questionContent: questionTemplates[key]}))
+        inputRef.current!.focus(); // 设置焦点自动回到Input
+    }
+
+
+    return (
+        <div className={styles.container}>
+            <div className={styles.header}>
+                <div className={styles["model-selector"]}>
+                    <EDropdownSelect
+                        options={modelOptions}
+                        value={data.model}
+                        setValue={setValue}
+                    ></EDropdownSelect>
+                </div>
+            </div>
+            <div className={styles["message-box"]}>
+                {
+                    data.messages.map((item, index) => (
+                        <div className={styles["item"]} key={index}>
+                            <div>
+                                <Avatar
+                                    size={24}
+                                    shape={"circle"}
+                                    src={index%2 === 0 && item.senderType === "User"?userAvatar: AIAvatar}></Avatar>
+                            </div>
+                            <div style={{marginLeft: "15px"}}>
+                                <div>{item.senderType==="User"? "You": data.model}</div>
+                                <div className={styles["content"]}>{item.content}</div>
+                            </div>
+
+                        </div>
+                    ))
+                }
+            </div>
+            <div className={styles["question-box"]}>
+                <div className={styles["input-group"]}>
+                    <Input
+                        className={styles["input"]}
+                        ref={inputRef}
+                        variant={"borderless"}
+                        size={"large"}
+                        placeholder={"Message "+data.model+" ..."}
+                        value={data.questionContent}
+                        onChange={(e) => setData(prev => ({...prev, questionContent: e.target.value}))}
+                    />
+                    {data.loading && <Spin style={{marginRight: "10px"}}/>}
+                    {
+                        !data.loading &&
+                        <Button
+                            className={styles["button"]}
+                            type={"text"}
+                            disabled={data.questionContent === ''}
+                            onClick={handleSendQuestion}
+                        >
+                            <ArrowUpOutlined/>
+                        </Button>
+                    }
+                    <Dropdown
+                        overlay={<Menu items={items} onClick={handleMenuClick}/>}
+                        trigger={['click']}
+                    >
+                        <Button type={"text"} className={styles["button"]}>
+                            <DownOutlined/>
+                        </Button>
+                    </Dropdown>
+                </div>
+            </div>
+        </div>
+    )
+}
+
+export default AIChatter

+ 44 - 0
src/components/EDropdownSelect/EDropdownSelect.tsx

@@ -0,0 +1,44 @@
+import {Dropdown, MenuProps, Space} from 'antd';
+import { DownOutlined } from '@ant-design/icons';
+import React, {useEffect} from "react";
+
+
+// Props类型定义
+interface DropdownSelectProps {
+    options: Array<string>;
+    value: string
+    setValue: (value: string) => void; // 更新选中值的函数
+}
+
+const EDropdownSelect: React.FC<DropdownSelectProps> = ({ options, value, setValue }) => {
+    useEffect(() => {
+        // 如果当前value不在options内,自动设置为options的第一个值
+        if (!options.includes(value)) {
+            setValue(options[0]);
+        }
+    }, [options, value, setValue]);
+
+    const handleMenuClick = ({ key }: any) => {
+        setValue(key);
+    };
+
+    const items: MenuProps['items'] = options.map(option => ({
+        key: option,
+        label: (
+            <div className={"option-item"}>{option}</div>
+        )
+    }))
+
+
+    return (
+        <Dropdown menu={{items, onClick: handleMenuClick}} trigger={['click']}>
+            <a className="ant-dropdown-link" onClick={(e) => e.preventDefault()}>
+               <Space>
+                   {value || options[0]} <DownOutlined />
+               </Space>
+            </a>
+        </Dropdown>
+    );
+}
+
+export default EDropdownSelect

+ 1 - 1
src/pages/Sign/index.scss

@@ -1,4 +1,4 @@
-@import "../../sass/public";
+@import "@/sass/public";
 
 .login-container {
 

+ 5 - 0
src/types/scss.d.ts

@@ -0,0 +1,5 @@
+// scss.d.ts
+declare module '*.module.scss' {
+    const classes: { [key: string]: string };
+    export default classes;
+}

+ 5 - 0
yarn.lock

@@ -5418,6 +5418,11 @@ fs.realpath@^1.0.0:
   resolved "https://r2.cnpmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz"
   integrity sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==
 
+fsevents@^2.3.2, fsevents@~2.3.2:
+  version "2.3.3"
+  resolved "https://r.cnpmjs.org/fsevents/-/fsevents-2.3.3.tgz"
+  integrity sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==
+
 function-bind@^1.1.2:
   version "1.1.2"
   resolved "https://r.cnpmjs.org/function-bind/-/function-bind-1.1.2.tgz"