最近AI
类应用好像比较火,想起了之前在公司做过IM相关的实时通信和对话,感觉有很多类似的地方,主要实现一下类似GPT一样的前端功能,就当作代码练习了
前端脚手架用的vite
、框架用的``、UI直接用Antd比较快。
启动项目
# 启动项目、选react和ts,swc反正几年前有坑现在不知道了别选就是了
yarn create vite
# 装相关
yarn add antd react-router-dom
创建聊天界面
界面分为三部分,两个组件一个界面,界面负责处理逻辑,两个组件分别展示数据列表以及输入数据。
主界面:
import React from "react";
import {Layout} from "antd";
import {Content, Header, Footer} from "antd/es/layout/layout";
import {IMessage} from "../types/IChat.ts";
import MessageList from "../component/MessageList.tsx";
import MessageInput from "../component/MessageInput.tsx";
const Chat: React.FC = () => {
const [messages, setMessages] = React.useState<IMessage[]>([]);
const [writing, setWriting] = React.useState<boolean>(false);
const handleSendMessage = (text: string) => {
const id = Date.now().toString();
const newMessage:IMessage = { id, text, from: 'user' };
setMessages((prev) => [...prev, newMessage]);
// 模拟响应数据
setTimeout(() => {
setWriting(true);
const botMessage: IMessage = { id: Date.now().toString(), text: ``, from: 'bot', typing: true };
setMessages((prev) => [...prev, botMessage]);
let index = 0;
const response = `我只会鹦鹉学舌,${text}`;
// 模拟逐字回复
const interval = setInterval(() =>{
if(index < response.length) {
setMessages((prev:IMessage[]) => {
return prev.map((msg:IMessage) =>
msg.id == botMessage.id ? {
...msg,
text: msg.text + response[index++]
}: msg
)
})
} else {
// 回复完成,移除 typing 状态
setMessages((prev) =>
prev.map((msg) =>
msg.id === botMessage.id ? { ...msg, typing: false } : msg
)
);
setWriting(false);
clearInterval(interval);
}
}, 50)
},1000)
}
return (
<Layout style={{ height: `50vh`, width: '50vw',borderRadius: 10, overflow: 'hidden'}}>
<Header style={{ color: 'white', textAlign: 'center' }}>GPT对话模拟</Header>
<Content style={{ padding: '16px', overflow: 'auto' }}>
<MessageList messages={messages} />
</Content>
<Footer style={{padding: '16px'}}>
<MessageInput onSend={handleSendMessage} isWriting={writing}></MessageInput>
</Footer>
</Layout>
)
}
export default Chat;
消息列表
import {IMessageListProps} from "../types/IChat.ts";
import {useEffect, useRef} from "react";
import {List, Typography} from "antd";
const MessageList:React.FC<IMessageListProps> = ({messages}) => {
const listRef = useRef<HTMLDivElement>(null);
// 自动滚到底部
useEffect(() => {
if(listRef.current){
console.log("run this", listRef.current.scrollHeight)
requestAnimationFrame(() => {
listRef.current!.scrollTo({
top: listRef.current!.scrollHeight,
behavior: 'smooth',
});
});
// listRef.current.scrollTo({
// top: listRef.current.scrollHeight,
// behavior: 'smooth' // 平滑滚动
// })
}
}, [messages]);
return (
<div ref={listRef} style={{ maxHeight: 'calc(50vh - 200px)', overflowY: 'auto' }}>
<List
dataSource={messages}
renderItem={(message) => (
<List.Item style={{ textAlign: message.from === 'user' ? 'right' : 'left', display: 'block' }}>
<Typography.Text
style={{
textAlign: 'left',
display: 'inline-block',
padding: '10px',
borderRadius: '12px',
background: message.from === 'user' ? '#1890ff' : '#f0f0f0',
color: message.from === 'user' ? '#fff' : '#000',
}}
>
{message.from === 'bot' &&
<strong>机器人: </strong>
}
{message.text}
{message.typing && (
<span
style={{
color: '#dadada',
marginLeft: '6px',
display: 'inline-block',
animation: 'blink 1s step-start infinite',
}}
>
|
</span>
)}
</Typography.Text>
</List.Item>
)}
>
</List>
</div>
)
}
export default MessageList;
消息输入
import {Button, Input, Space} from "antd";
import {useState} from "react";
import {IMessageInputProps} from "../types/IChat.ts";
const MessageInput:React.FC<IMessageInputProps> = ({onSend, isWriting}) => {
const [text, setText] = useState<string>('');
const handleSend = () => {
if (text.trim()) {
onSend(text.trim());
setText('');
}
}
return (
<Space.Compact style={{ width: 'calc(100% - 80px)'}}>
<Input
disabled={isWriting}
placeholder="请输入内容..."
value={text}
onChange={(e) => setText(e.target.value)}
onPressEnter={handleSend}
></Input>
<Button disabled={isWriting} type="primary" onClick={handleSend} style={{fontSize: 20}}>
<b>→</b>
</Button>
</Space.Compact>
)
}
export default MessageInput;
随着文字的变多实时到最下面
大概就是这样一个demo,实现的功能就是输入之后持续拉到底部,然后输入完了之后才能继续说,还有一些比如说滚动效果啊,暂停啊啥的就先意念实现了,但有一个点的着重交代一下,就是listRef.current.scrollTo
的失效问题,数据变换的时候还没有撑开格子,所以我们放到了下一帧的时候再监听变化,就能正常处理了,就这。