|
|
@@ -1,13 +1,25 @@
|
|
|
-import React, {useRef, useState} from "react";
|
|
|
+import React, {useEffect, 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";
|
|
|
+import useStore from "@/store";
|
|
|
+import {IDialogue} from "@/types/dialogue";
|
|
|
+import useApis from "@/apis";
|
|
|
|
|
|
-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"
|
|
|
+
|
|
|
+interface IProps {
|
|
|
+ dialogueId: string;
|
|
|
+ messages: IDialogue[]
|
|
|
+}
|
|
|
+
|
|
|
+const AIChatter:React.FC<IProps> = ({dialogueId, messages}) => {
|
|
|
+
|
|
|
+ const store = useStore()
|
|
|
+
|
|
|
+ const apis = useApis()
|
|
|
+
|
|
|
+ const modelOptions = ["ChatGLM3", "QWen14B"]
|
|
|
const questionTemplates = [
|
|
|
"请问您提供哪些服务?",
|
|
|
"您的营业时间是?",
|
|
|
@@ -22,72 +34,108 @@ const AIChatter = () => {
|
|
|
)
|
|
|
}))
|
|
|
|
|
|
- const inputRef = useRef<any>(null); // 创建引用
|
|
|
+ const inputRef = useRef<any>(null) // 创建引用
|
|
|
+
|
|
|
+ const messagesEndRef = useRef<HTMLDivElement>(null)
|
|
|
+
|
|
|
+ const containerTopRef = useRef<HTMLDivElement>(null);
|
|
|
|
|
|
|
|
|
- const [data, setData] = useState({
|
|
|
+ const [data, setData] = useState<{
|
|
|
+ model: string;
|
|
|
+ questionContent: string;
|
|
|
+ messages: IDialogue[];
|
|
|
+ loading: boolean;
|
|
|
+ loadedPage: number;
|
|
|
+ pageSize: number;
|
|
|
+ }>({
|
|
|
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")
|
|
|
- }
|
|
|
- ],
|
|
|
+ messages: [],
|
|
|
loading: false, // 请求加载过程
|
|
|
+ loadedPage: 0, //默认加载第一页
|
|
|
+ pageSize: 20, //默认加载20条内容,这个不变
|
|
|
});
|
|
|
|
|
|
+
|
|
|
+ useEffect(() => {
|
|
|
+ if (messages)
|
|
|
+ setData(prev => ({...prev, messages: messages}))
|
|
|
+ messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' });
|
|
|
+ }, [messages])
|
|
|
+
|
|
|
+
|
|
|
+ // 保证本地添加数据时,可以自动下拉
|
|
|
+ useEffect(() => {
|
|
|
+ messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' });
|
|
|
+ }, [data.messages])
|
|
|
+
|
|
|
+
|
|
|
+ useEffect(() => {
|
|
|
+ const handleScroll = () => {
|
|
|
+ if (containerTopRef.current) {
|
|
|
+ const { top } = containerTopRef.current.getBoundingClientRect() // 获取目标组件的位置
|
|
|
+ const reachedTop = top >= 0 // 检查组件是否到达顶部
|
|
|
+
|
|
|
+ // 确保,不重复加载数据
|
|
|
+ if (reachedTop && !data.loading) {
|
|
|
+ console.log("触顶了")
|
|
|
+ // setData(prev => ({...prev, loadedPage: prev.loadedPage+1}))
|
|
|
+ // loadData()
|
|
|
+ }
|
|
|
+ }
|
|
|
+ };
|
|
|
+
|
|
|
+ window.addEventListener('scroll', handleScroll)
|
|
|
+
|
|
|
+ return () => window.removeEventListener('scroll', handleScroll)
|
|
|
+ }, [data.loading]);
|
|
|
+
|
|
|
+
|
|
|
// 更新选中值的函数
|
|
|
const setValue = (model: string) => {
|
|
|
- console.log("父组件中接受到的值", model)
|
|
|
setData(prev => ({ ...prev, model }));
|
|
|
};
|
|
|
|
|
|
+ const loadData = () => {
|
|
|
+ // 假设这是从后端加载数据的函数
|
|
|
+ apis.getAIDialogues(1, store.user.id, data.loadedPage, data.pageSize)
|
|
|
+ .then((_res:any) => {
|
|
|
+ const newData = _res.data.data.content // 请求到的数据
|
|
|
+
|
|
|
+ // 和原有数组拼接,注意得拼接在前面
|
|
|
+ setData(prev => ({
|
|
|
+ ...prev,
|
|
|
+ messages: newData.concat(prev.messages)
|
|
|
+ }))
|
|
|
+ })
|
|
|
+
|
|
|
+ }
|
|
|
+
|
|
|
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)
|
|
|
+
|
|
|
+ // 注意:setData是异步的
|
|
|
+ let requestBody = data.messages
|
|
|
+ requestBody.push({
|
|
|
+ role: 'user',
|
|
|
+ content: data.questionContent
|
|
|
+ })
|
|
|
+
|
|
|
+ apis.requestAI(dialogueId, data.model, requestBody)
|
|
|
+ .then((_res:any) => {
|
|
|
+ const resDialogue = _res.data.data
|
|
|
+ console.log(resDialogue)
|
|
|
+ setData(prev => ({
|
|
|
+ ...prev,
|
|
|
+ messages: prev.messages.concat(resDialogue)
|
|
|
+ }))
|
|
|
+ }).catch((_err:any) => {
|
|
|
+ console.log(_err)
|
|
|
+ }).finally(() => {
|
|
|
+ setData(prev => ({...prev, questionContent: '', loading: false}))
|
|
|
+ })
|
|
|
+
|
|
|
}
|
|
|
|
|
|
const handleMenuClick = ({ key }: any) => {
|
|
|
@@ -98,6 +146,7 @@ const AIChatter = () => {
|
|
|
|
|
|
return (
|
|
|
<div className={styles.container}>
|
|
|
+ {/* 大语言模型选择器 */}
|
|
|
<div className={styles.header}>
|
|
|
<div className={styles["model-selector"]}>
|
|
|
<EDropdownSelect
|
|
|
@@ -107,36 +156,50 @@ const AIChatter = () => {
|
|
|
></EDropdownSelect>
|
|
|
</div>
|
|
|
</div>
|
|
|
- <div className={styles["message-box"]}>
|
|
|
+
|
|
|
+ {/* 消息区域,采用ChatGPT的交互模式 */}
|
|
|
+ <div className={styles["message-box"]} ref={containerTopRef}>
|
|
|
{
|
|
|
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>
|
|
|
+ {/* 分类展示用户和AI的消息 */}
|
|
|
+ {
|
|
|
+ index % 2 === 0 ?
|
|
|
+ <Avatar
|
|
|
+ size={32}
|
|
|
+ shape={"circle"}
|
|
|
+ src={store.user.userAvatar || ""}>{store.user.name.charAt(0)}</Avatar> :
|
|
|
+ <Avatar
|
|
|
+ size={32}
|
|
|
+ shape={"circle"}
|
|
|
+ >{data.model.charAt(0)}</Avatar>
|
|
|
+ }
|
|
|
</div>
|
|
|
<div style={{marginLeft: "15px"}}>
|
|
|
- <div>{item.senderType==="User"? "You": data.model}</div>
|
|
|
+ <div style={{marginBottom: "2px"}}>{index % 2 === 0? "You": data.model}</div>
|
|
|
<div className={styles["content"]}>{item.content}</div>
|
|
|
</div>
|
|
|
-
|
|
|
</div>
|
|
|
))
|
|
|
}
|
|
|
+ <div ref={messagesEndRef} />
|
|
|
</div>
|
|
|
+
|
|
|
+ {/* 对话框区域,用于AI提问和提示词模版展现 */}
|
|
|
<div className={styles["question-box"]}>
|
|
|
<div className={styles["input-group"]}>
|
|
|
- <Input
|
|
|
+ <Input.TextArea
|
|
|
className={styles["input"]}
|
|
|
ref={inputRef}
|
|
|
variant={"borderless"}
|
|
|
size={"large"}
|
|
|
placeholder={"Message "+data.model+" ..."}
|
|
|
+ autoSize={{ minRows: 1, maxRows: 2 }}
|
|
|
value={data.questionContent}
|
|
|
onChange={(e) => setData(prev => ({...prev, questionContent: e.target.value}))}
|
|
|
- />
|
|
|
+ ></Input.TextArea>
|
|
|
+
|
|
|
{data.loading && <Spin style={{marginRight: "10px"}}/>}
|
|
|
{
|
|
|
!data.loading &&
|