某司面试题,用指定技术栈 实现一个GPT 对话功能
指定技术栈react hook、typescript、 material-ui 、 openrouter
material-ui 第一次用 不同版本方式不同,遇到问题也不少基本都是导入问题
1、消息组件
// Message.tsx
import React from 'react';
import {useStyles} from './styles';
import RenderContent from './content';
interface ChatMessageProps {
sender: string;
content: string;
isSender?: boolean;
}
const ChatMessage: React.FC<ChatMessageProps> = ({ sender, content='', isSender = false }) => {
const coImg = require('./assets/co.png')
const gptImg = require('./assets/gpt.png')
const {classes} = useStyles()
return (
<div className={classes.message}>
<div className={classes.item}>
<div className={classes.userIcon}>
<img src={isSender ?coImg:gptImg} className={classes.Icon}></img>
<span className={classes.userName}>{ isSender ? 'Me' :sender }</span>
</div>
<div>
{/* <p className={classes.content}>{content}</p> */}
<RenderContent message={content}/>
</div>
</div>
</div>
);
};
export default ChatMessage;
content.tsx
// conyent.tsx
import React from 'react'
import {useStyles} from './styles';
interface propsType{
message:string
}
const RenderContent:React.FC<propsType>=({message})=>{
const {classes} =useStyles()
const hasTags = /<[^>]+>/.test(message);
if( hasTags){
return (<div className={classes.content} dangerouslySetInnerHTML={{ __html: message }} />)
}else {
return (<p className={classes.content}>{message}</p>)
}
}
export default RenderContent
3、聊天窗口
// ChatWindow.tsx
import React, { useState,useRef,useEffect,useCallback } from 'react';
import Message from './Message';
import InputBase from '@mui/material/InputBase';
import { Input } from '@mui/material';
import IconButton from '@mui/material/IconButton';
import ArrowUpwardIcon from '@mui/icons-material/ArrowUpward';
import Paper from '@mui/material/Paper';
import Icon from '@mui/material/Icon';
import axios from 'axios';
import {useStyles} from './styles'
interface ChatMessageProps {
sender: string;
content: string;
isSender?: boolean;
}
const ChatBox:React.FC= () => {
const [messages, setMessages] = useState('');
const [allMsg,setAllmsg] = useState<ChatMessageProps []> ([])
const containerScroll = useRef<HTMLDivElement>(null)
const {classes} = useStyles()
const scrollToBottom = () => {
if (containerScroll.current) {
containerScroll.current.scrollTop = containerScroll.current.scrollHeight;
}
};
const addMessage = useCallback(
(info:ChatMessageProps) => {
setAllmsg((data)=>{
return [...data, info];
})
scrollToBottom();
},[allMsg]
)
// 渲染后滚动到底部
useEffect(() => {
scrollToBottom();
}, [allMsg]);
const handleSendMessage = () => {
const currMsg = {sender:'Me',content:messages,isSender:true}
addMessage(currMsg)
setMessages('');
axios.post(
'https://openrouter.ai/api/v1/chat/completions',
{
"model": "mistralai/mistral-7b-instruct:free",
"messages": [
{"role": "user", "content": messages},
],
},
{
headers: {
"Authorization": `openrouter key`,
"Content-Type": "application/json"
},
}
).then(res=>{
const result = res.data.choices[0].message;
const aiResData= {sender:'GPT',content:result.content,isSender:false}
addMessage(aiResData)
})
}
const handleInputChange = (event: React.ChangeEvent<HTMLInputElement>) => {
setMessages(event.target.value);
};
return (
<div className={classes.root}>
<div className={classes.messageList} ref={containerScroll}>
{allMsg.map((message, index) => (
<Message key={index} {...message} />
))}
</div>
<Paper
component="form"
sx={{ p: '2px 4px', display: 'flex', alignItems: 'center', width: 600 }}
>
<InputBase
sx={{ ml: 1, flex: 1 }}
value={messages}
onChange={handleInputChange}
placeholder="Message ChatGPT"
inputProps={{ 'aria-label': 'search google maps' }}
/>
<IconButton color="primary" sx={{ p: '10px' }} onClick={handleSendMessage} aria-label="directions">
<ArrowUpwardIcon></ArrowUpwardIcon>
</IconButton>
</Paper>
</div>)
}
export default ChatBox
补充:可用 useCallback 钩子优化一下消息队列
4、样式
// import {makeStyles} from '@mui/material/styles';
import { makeStyles } from 'tss-react/mui'
export const useStyles=makeStyles()((theme) => {
return {
root:{
height:'100%',
width:'100vh',
display:'flex',
flexDirection: 'column',
alignItems: 'center',
justifyContent: 'center',
backgroundColor:'#fff',
padding:'20px'
},
message:{
display:'flex',
flexDirection: 'column',
alignItems: 'center',
justifyContent: 'center',
fontSize:'14px',
color:'#2e3b4c'
},
messageList:{
height: 'calc(100vh - 120px)',
overflowY: 'auto',
padding: '20px',
width:'100%',
bosSizing:'border-box'
},
input:{
display:'flex',
padding:'5px',
border: '1px solid',
borderRadius:'4px',
fontSize:'14px',
marginBottom:'10px'
},
userIcon:{
display:'flex',
height: '40px',
alignItems: 'center'
},
userName:{
textAlign:'left',
fontSize:'16px',
marginLeft:'10px'
},
content:{
margin:'0',
textAlign:'left',
padding:'0 40px'
},
inputBox:{
display:'flex'
},
item:{
display: 'flex',
width: '100%',
margin: '10px',
flexDirection: 'column',
},
Icon:{
width: '28px',
height:'28px',
display: 'inline-block',
backgroundColor: 'skyblue'
},
inputField:{
width: '100%',
padding: '5px',
border: '1px solid',
borderRadius: '4px',
fontSize: '14px',
marginBottom: '10px',
outline: 'none'
}
};
});
样式引用版本不同 方式不同, 这里话的时间还不少