如何用React实现一个类 chatGPT 的交互式问答组件
前言
我正在参加「掘金·启航计划」,最近公司搞了一个前端技能大赛,大致要求是《实现一个类 chatGPT 的交互式问答组件》,刚好借这个项目来分享一下自己封装组件的思路(ps:好久没写文章了,刚好最近掘金有活动😂),最终的参赛作品如下:
API文档
属性名 | 描述 | 类型 | 默认值 |
---|---|---|---|
recommendList | 推荐模板列表 | string[] | undefined | null |
commonUseList | 常用模板列表 | string[] | undefined | null |
uploadProps | 文件上传,同 antd Upload 组件 | { accept?: string; children?: ReactNode; maxCount?: number; multiple?: boolean; } | undefined | null |
autoCompleteProps | 输入框自动补全,同 antd AutoComplete 组件 | { options?: DefaultOptionType[]; onSearch?: ((value: string) => void); } | undefined | null |
onSend | 发送消息 | ((msg: IQuestion) => void) | undefined | null |
onMsgLike | 点赞回答 | ((msg: IQuestion) => void) | undefined | null |
onMsgNotLike | 点踩回答 | ((msg: IQuestion) => void) | undefined | null |
onRefresh | 刷新回答 | ((msg: IQuestion) => void) | undefined | null |
renderAvatar | 自定义头像 | ((msg: IMessage) => ReactNode) | undefined | null |
renderTools | 自定义消息框底部操作栏 | ((msg: IMessage) => ReactNode) | undefined | null |
isAnimation | 是否开启消息动画效果 | boolean | undefined | null |
className | 自定义类名 | boolean | undefined | null |
style | 自定义样式 | CSSProperties | undefined | null |
loading | 加载状态 | boolean | undefined | null |
messages | 消息列表 | IMessage[] | undefined | null |
# 功能介绍 | |||
这个组件的主要功能主要包括: |
- 输入框上传附件
- 输入框自动补全
- 自定义消息操作栏
- 自定义头像
- 模板调用
下面会挨个介绍功能
输入框上传文件
这个功能很简单就是支持输入框上传附件,然后会把附件和问题一起丢给使用者,由使用者自己去调用api接口来获取回答.上传组件采用的是antd的Upload组件,API也一致.大致代码如下:
import React from 'react';
import { Chat, useMessage, EMessageType } from './chat';
export default function () {
const { msgProps } = useMessage(
(question, lastAnswer) => {
return new Promise((resolve, reject) => {
const { content, fileList } = question;
setTimeout(() => {
resolve({
content:
'\n\n' +
(lastAnswer ? '## 我又想了一下 \n\n' : '') +
content?.replace(/吗/g, '呢').replace(/你/g, '我') +
'\n\n' +
fileList.map((item) => item.name).join(' ') +
'\n\n'
});
}, 2000);
});
},
{
uploadProps: {
multiple: true
},
defaultMsgs: [
{
key: 1,
type: EMessageType.AI,
content: '快来和我对话吧',
renderTools: () => null,
isAnimation: false
}
]
}
);
return (
<Chat {...msgProps} />
);
}
输入框自动补全
自动补全采用的是antd的Autocomplete组件,API也一致,原理就是当输入框内容改变时去请求关联内容然后改变Autocomplete的options,就能实现自动补全啦.
import React, { useState } from 'react';
import { Chat, useMessage, EMessageType, IMessage } from './chat';
export default function () {
const [options, setOptions] = useState<{ value: string }[]>([]);
const handleSearch = (value: string) => {
setOptions(!value ? [] : [{ value }, { value: value + value }, { value: value + value + value }]);
};
const { msgProps } = useMessage(
(question, lastAnswer) => {
return new Promise((resolve, reject) => {
const { content } = question;
setTimeout(() => {
resolve({
isAnimation: !!lastAnswer,
content:
'\n\n' +
(lastAnswer ? '## 我又想了一下 \n\n' : '') +
content?.replace(/吗/g, '呢').replace(/你/g, '我') +
'\n\n'
});
}, 2000);
});
},
{
autoCompleteProps: {
options,
onSearch: handleSearch
}
}
);
return (
<Chat {...msgProps} />
);
}
自定义消息操作栏
由于每条消息下方还有操作栏,默认的有重新回答、点赞、点踩、复制等,所以提供了自定义的功能,如果不需要默认的这些功能也可以关闭或者自定义.
import React from 'react';
import { Chat, useMessage, EMessageType, IMessage } from './chat';
export default function () {
const { msgProps } = useMessage(
(question, lastAnswer) => {
return new Promise((resolve, reject) => {
const { content } = question;
setTimeout(() => {
resolve({
content:
'\n\n' +
(lastAnswer ? '## 我又想了一下 \n\n' : '') +
content?.replace(/吗/g, '呢').replace(/你/g, '我') +
'\n\n',
renderTools: (msg: IMessage) => <div>点个赞吧</div>
});
}, 2000);
});
},
{
renderTools: (msg: IMessage) => <div>这里是自定义操作栏</div>,
defaultMsgs: [
{
key: 1,
type: EMessageType.AI,
content: '快来和我对话吧',
renderTools: () => null,
isAnimation: false
},
{
key: 2,
type: EMessageType.AI,
content: '支持每条消息独立设置操作栏',
renderTools: (msg: IMessage) => <div>这里是消息独立操作栏</div>,
isAnimation: false
}
]
}
);
return (
<BixiProvider locale={enUS}>
<Chat {...msgProps} />
</BixiProvider>
);
}
自定义头像
既然是对话,那肯定得提供自定义头像的功能啦,使用方式的话是传入renderAvatar这个方法,通过传入的消息类型来判断显示那个头像,效果如下
import React from 'react';
import { Chat, useMessage, EMessageType, IMessage } from './chat';
export default function () {
const { msgProps } = useMessage(
(question, lastAnswer) => {
return new Promise((resolve, reject) => {
const { content } = question;
setTimeout(() => {
resolve({
content:
'\n\n' +
(lastAnswer ? '## 我又想了一下 \n\n' : '') +
content?.replace(/吗/g, '呢').replace(/你/g, '我')
});
}, 2000);
});
},
{
renderAvatar: (msg: IMessage) => <>{msg.type === EMessageType.AI ? '人工智障' : '大帅比'}</>,
defaultMsgs: [
{
key: 1,
type: EMessageType.AI,
content: '快来和我对话吧',
renderTools: () => null,
isAnimation: false
},
{
key: 2,
type: EMessageType.Self,
content: '你真的是人工智能吗',
isAnimation: false
},
{
key: 3,
type: EMessageType.AI,
content: '支持每条消息单独设置头像',
isAnimation: false
}
]
}
);
return (
<Chat {...msgProps} />
);
}
模板调用
这个功能是比赛要求的,需要给用户提供一些模板,用户点击模板就可以快速搜索答案之类的,在输入框输入“/”就可以唤醒模板了,实现方式也很简单,监听输入框值的改变,如果输入值和“/”匹配就直接唤起模板就可以了.效果如下
import React from 'react';
import { Chat, useMessage, EMessageType, IMessage } from './chat';
export default function () {
const { msgProps } = useMessage(
(question, lastAnswer) => {
return new Promise((resolve, reject) => {
const { content } = question;
setTimeout(() => {
resolve({
content:
'\n\n' +
(lastAnswer ? '## 我又想了一下 \n\n' : '') +
content?.replace(/吗/g, '呢').replace(/你/g, '我')
});
}, 2000);
});
},
{
recommendList: ['标题生成', '文章续写', '文章润色', '文章大纲', '朋友圈文案', '活动方案', '翻译', '演讲稿'],
commonUseList: ['标题生成', '朋友圈文案'],
}
);
return (
<Chat {...msgProps} />
);
}
自定义hook
除了上面的功能外,还对逻辑进行了封装,调用这个hook可以获取到messages、onRefresh、onSend、loading这几个主要的props,其他的props你也可以自己传入. 这个自定义hook封装了消息发送方法,消息刷新方法,组件loading状态,消息列表等主要逻辑.重新回答的原理是去消息列表中寻找之前最近的一条问题,然后在此调用获取回答的接口 代码如下:
import { useState, useCallback } from 'react';
import { IMessage, IChatProps, EMessageType, IQuestion } from './model';
import { uniqueId } from 'lodash-es';
type IUseMessageOption = Omit<IChatProps, 'onSend' | 'onRefresh' | 'loading' | 'messages'> & {
defaultMsgs: IMessage[];
};
type IService = (msg: IQuestion, lastAnswer?: IMessage) => Promise<IMessage>;
export function useMessage(
getMsg: IService,
{ defaultMsgs, ...props }: IUseMessageOption
): {
msgProps: IChatProps;
} {
const [messageList, setMessageList] = useState<IMessage[]>(defaultMsgs || []);
const [loading, setLoading] = useState(false);
const onRefresh = useCallback(
(item: IMessage) => {
const index = messageList.findIndex((msg) => msg.key === item.key);
if (index < 1) return;
const lastQuestion = messageList
.slice(0, index)
.reverse()
.find((msg) => msg.type === EMessageType.Self);
if (!lastQuestion) return;
setLoading(true);
getMsg(lastQuestion, messageList[index]).then((res) => {
setLoading(false);
const key = `${EMessageType.AI}-${uniqueId()}`;
messageList[index] = { ...res, key, type: EMessageType.AI };
setMessageList([...messageList]);
});
},
[getMsg, messageList]
);
const onSend = useCallback(
(msg: IQuestion) => {
setLoading(true);
setMessageList((list) => list.concat({ ...msg, key: `${EMessageType.Self}-${uniqueId()}`, type: EMessageType.Self }));
getMsg(msg).then((res) => {
setLoading(false);
const key = `${EMessageType.AI}-${uniqueId()}`;
setMessageList((list) => list.concat({ ...res, key, type: EMessageType.AI }));
});
},
[getMsg]
);
return {
msgProps: {
messages: messageList,
onRefresh,
onSend,
loading,
...props
}
};
}
使用方法
该组件使用起来很简单,调用useMessage这个hook,传入api接口(需要返回promise)就可以了,至于其他的组件api也可以一并传入,还支持传入历史消息列表.然后这个hook就会返回所有该组件所需要的props,包括消息发送方法,消息刷新方法,组件loading状态,消息列表等.是不是很方便呢,哈哈.
转载自:https://juejin.cn/post/7249608794748698684