From d09e04aaad4e784d61e6ac6a03c309853cbb8ba4 Mon Sep 17 00:00:00 2001 From: Shinji-Li <1349021570@qq.com> Date: Fri, 2 Feb 2024 19:06:39 +0800 Subject: [PATCH] =?UTF-8?q?:sparkles:=20feat:=20=E6=94=AF=E6=8C=81?= =?UTF-8?q?=E8=87=AA=E5=AE=9A=E4=B9=89=20InputAreaRender=20(#74)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/guide/demos/renderInputArea.tsx | 82 ++++++++++++++++++++++ docs/guide/multimodal.md | 78 ++++++++++++++++++++ src/ProChat/components/InputArea/index.tsx | 53 +++++++++++--- src/ProChat/container/App.tsx | 10 ++- src/ProChat/container/index.tsx | 10 ++- src/ProChat/demos/renderInputArea.tsx | 82 ++++++++++++++++++++++ src/ProChat/index.md | 14 ++-- 7 files changed, 311 insertions(+), 18 deletions(-) create mode 100644 docs/guide/demos/renderInputArea.tsx create mode 100644 docs/guide/multimodal.md create mode 100644 src/ProChat/demos/renderInputArea.tsx diff --git a/docs/guide/demos/renderInputArea.tsx b/docs/guide/demos/renderInputArea.tsx new file mode 100644 index 00000000..bbe1c845 --- /dev/null +++ b/docs/guide/demos/renderInputArea.tsx @@ -0,0 +1,82 @@ +/** + * compact: true + * iframe: 800 + */ +import { PlusOutlined } from '@ant-design/icons'; +import { ProChat } from '@ant-design/pro-chat'; +import { Button, Form, Input, Space, Upload, message } from 'antd'; +import { useTheme } from 'antd-style'; +import { ReactNode } from 'react'; + +export default () => { + const theme = useTheme(); + + const renderInputArea = ( + _: ReactNode, + onMessageSend: (message: string) => void | Promise, + onClear: () => void, + ) => { + return ( +
{ + const { question, files } = value; + const FilesBase64List = files?.fileList.map( + (file: any) => `![${file.name}](${file.thumbUrl})`, + ); + const Prompt = `${question} ${FilesBase64List?.join('\n')}`; + await onMessageSend(Prompt); + }} + initialValues={{ question: '下面的图片是什么意思?' }} + > + + + + + + { + if (file.type === 'image/png') { + return true; + } else { + message.error('请上传png格式的图片'); + return Upload.LIST_IGNORE; + } + }} + action="https://run.mocky.io/v3/435e224c-44fb-4773-9faf-380c5e6a2188" + > + + + + + + + + + + +
+ ); + }; + + return ( +
+ +
+ ); +}; diff --git a/docs/guide/multimodal.md b/docs/guide/multimodal.md new file mode 100644 index 00000000..2c01817d --- /dev/null +++ b/docs/guide/multimodal.md @@ -0,0 +1,78 @@ +--- +title: 多模态怎么接入 +order: 19 +group: + title: 使用案例 +nav: + title: 文档 + order: 0 +--- + +# 多模态怎么接入 + +一开始我们打算直接让 InputArea(即下方的输入框)支持上传各种各样的文件 + +但是一旦这么设计,就会导致更多的问题 + +- 图片我们是直接转 Base64 还是 Cdn?如果是 Cdn 是不是还需要提供一个 Cdn 的 api 给开发者? +- 图片还好说,但是除开图片之外的其他文件呢?各种文件是否需要预览? +- 这些文件到底以怎么样的形式拼接到 Prompt 中去呢?怎么定义这个 Prompt 的位置? + +等等这些设计细节数不甚数,而且对于一些模型来说,并不一定支持多模态,默认提供分析下来并不是一个好的设计。 + +## 自定义输入部分 + +我们提供了一个 renderInputArea 的 api,来帮助你对多模态的情况下进行支持,以及和 ProChat 的数据流进行接入和交互 + +```ts + renderInputArea?: ( + defaultDom: ReactNode, + onMessageSend: (message: string) => void | Promise, + onClearAllHistory: () => void, + ) => ReactNode; +``` + +renderInputArea 共有三个参数: + +- defaultDom :即默认渲染的 dom,你如果是想包裹或者添加一些小内容,可以直接在这个基础上进行组合 +- onMessageSend :发送数据的方法,这个方法和 ProChat.sendMessage(Hooks) 本质上是一个方法,用于向 ProChat 的数据流发送一条数据 +- onClearAllHistory : 清空当前对话的方法,这个方法和 ProChat.clearMessage(Hooks) 本质上是一个方法 + +这下子你就可以随意组合当前的内容,以及你打算做的各种需求,例如:阻止一些不好的对话、上传内容的前置校验等 + +## 一个图片上传的演示案例 + + + +我们来详细拆解下这个案例 + +### 默认使用Base64 + +案例中使用了 antd 的 Upload 组件,我们可以轻易拿到当前内容的 Base64,然后在 onMessageSend 将其进行组合 + +如果你想用 CDN 代替 Base64,你需要做的事情就是在数据流上做处理。 + +> 下面这个改动是建立在,Upload 组件配置的 actions 接口如果有 response 返回,里面有一个 cdnUrl 返回告诉当前文件上传完毕后的 Cdn 链接在哪里 + +```js +onFinish={async (value) => { + const { question, files } = value; + const FilesCdnList = files?.fileList.map( + (file: any) => `![${file.name}](${file.response.cdnUrl})`, + ); + const Prompt = `${question} ${FilesCdnList?.join('\n')}`; + await onMessageSend(Prompt); +}} +``` + +### 非图片的内容支持 + +可以看到,本质上预览是依赖于 Markdown 的预览能力进行支持的,如果遇到了内容的文件,我们建议采用 `` 来进行渲染,然后使用 messageItemExtraRender 在下方进行额外文件的预览渲染 + +> 其实 Markdown 是支持 Html 渲染的,但是我们默认并没有开启这个能力,考虑各方面我们并不打算默认打开这个,我们建议你采用 messageItemExtraRender + +```ts +messageItemExtraRender: (message: ChatMessage, type: 'assistant' | 'user') => React.ReactNode; +``` + +messageItemExtraRender 可以拿到当前的 message,可以做很多自定义渲染的工作。 diff --git a/src/ProChat/components/InputArea/index.tsx b/src/ProChat/components/InputArea/index.tsx index a4c43ab2..53d9a80c 100644 --- a/src/ProChat/components/InputArea/index.tsx +++ b/src/ProChat/components/InputArea/index.tsx @@ -1,11 +1,12 @@ import { SendOutlined } from '@ant-design/icons'; import { Button, ConfigProvider } from 'antd'; import { createStyles, cx, useResponsive } from 'antd-style'; -import { useContext, useRef, useState } from 'react'; +import { ReactNode, useContext, useRef, useState } from 'react'; import { Flexbox } from 'react-layout-kit'; import { useStore } from '../../store'; +import { useMergedState } from 'rc-util'; import ActionBar from './ActionBar'; import { AutoCompleteTextArea } from './AutoCompleteTextArea'; @@ -51,12 +52,24 @@ const useStyles = createStyles(({ css, responsive, token }) => ({ `, })); -export const ChatInputArea = ({ className }: { className?: string }) => { - const [sendMessage, isLoading, placeholder, inputAreaProps] = useStore((s) => [ +type ChatInputAreaProps = { + className?: string; + onSend?: (message: string) => boolean | Promise; + renderInputArea?: ( + defaultDom: ReactNode, + onMessageSend: (message: string) => void | Promise, + onClearAllHistory: () => void, + ) => ReactNode; +}; + +export const ChatInputArea = (props: ChatInputAreaProps) => { + const { className, onSend, renderInputArea } = props || {}; + const [sendMessage, isLoading, placeholder, inputAreaProps, clearMessage] = useStore((s) => [ s.sendMessage, !!s.chatLoadingId, s.placeholder, s.inputAreaProps, + s.clearMessage, ]); const { getPrefixCls } = useContext(ConfigProvider.ConfigContext); const [message, setMessage] = useState(''); @@ -64,14 +77,26 @@ export const ChatInputArea = ({ className }: { className?: string }) => { const { styles, theme } = useStyles(); const { mobile } = useResponsive(); - const send = () => { - sendMessage(message); - setMessage(''); + const [ButtonLoading, setButtonLoading] = useMergedState(isLoading); + + const send = async () => { + if (onSend) { + setButtonLoading(true); + const success = await onSend(message); + setButtonLoading(false); + if (success) { + sendMessage(message); + setMessage(''); + } + } else { + sendMessage(message); + setMessage(''); + } }; const prefixClass = getPrefixCls('pro-chat-input-area'); - return ( + const defaultInputArea = ( { /> {mobile ? null : ( + + + + + + + + + + + ); + }; + + return ( +
+ +
+ ); +}; diff --git a/src/ProChat/index.md b/src/ProChat/index.md index 4b6ae34d..05e724b4 100644 --- a/src/ProChat/index.md +++ b/src/ProChat/index.md @@ -76,6 +76,14 @@ ProChat 使用 `meta` 来表意会话双方的头像、名称等信息。设定 +## 自定义输入区域 + +有些时候会觉得默认的输入区域不够好用,或是你有一些输入模块的自定义需求,可以使用 renderInputArea 来进行自定义输入 + +下面是一个支持图片上传的示范案例,试试上传文件并提交看看吧。 + + + ## 悬浮窗使用 将 `ProChat` 组件作为会话解决方案 @@ -165,13 +173,11 @@ useProChat hooks 必须在包裹 `ProChatProvider` 后方可使用。 ## backtoBottomConfig -回到底部按钮所支持的 api 内容 - | 参数 | 说明 | 类型 | 默认值 | -| --- | --- | --- | --- | --- | --- | +| --- | --- | --- | --- | | className | 类名 | string | - | | style | 额外添加的css内容 | CSSProperties | - | | onClick | 点击按钮的回掉 | `React.MouseEventHandler` | - | -| text | 展示的内容 | string | `Back to bottom` | -- | -- | +| text | 展示的内容 | string | `Back to bottom` | | render | 自定义渲染的方法 | (defaultDom: React.ReactNode,scrollToBottom,BackBottomConfig: BackBottomProps) => React.ReactNode | - | | alwaysShow | 是否一直显示按钮 | boolean | false |