Skip to content

Commit

Permalink
✨ feat: 支持自定义 InputAreaRender (#74)
Browse files Browse the repository at this point in the history
  • Loading branch information
ONLY-yours authored Feb 2, 2024
1 parent a5763fd commit d09e04a
Show file tree
Hide file tree
Showing 7 changed files with 311 additions and 18 deletions.
82 changes: 82 additions & 0 deletions docs/guide/demos/renderInputArea.tsx
Original file line number Diff line number Diff line change
@@ -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<any>,
onClear: () => void,
) => {
return (
<Form
onFinish={async (value) => {
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: '下面的图片是什么意思?' }}
>
<Form.Item
label="Question"
name="question"
rules={[{ required: true, message: '请输入你要询问的内容!' }]}
>
<Input.TextArea style={{ height: 100 }} />
</Form.Item>

<Form.Item
label="FileUpload"
name="files"
rules={[{ required: true, message: '请放入上传图片' }]}
>
<Upload
listType="picture-card"
beforeUpload={(file) => {
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"
>
<button style={{ border: 0, background: 'none' }} type="button">
<PlusOutlined />
<div style={{ marginTop: 8 }}>Upload</div>
</button>
</Upload>
</Form.Item>

<Form.Item>
<Space>
<Button type="primary" htmlType="submit">
发送对话消息
</Button>
<Button htmlType="button" onClick={onClear}>
清空当前对话内容
</Button>
</Space>
</Form.Item>
</Form>
);
};

return (
<div style={{ background: theme.colorBgLayout, height: '100vh' }}>
<ProChat renderInputArea={renderInputArea} />
</div>
);
};
78 changes: 78 additions & 0 deletions docs/guide/multimodal.md
Original file line number Diff line number Diff line change
@@ -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<any>,
onClearAllHistory: () => void,
) => ReactNode;
```

renderInputArea 共有三个参数:

- defaultDom :即默认渲染的 dom,你如果是想包裹或者添加一些小内容,可以直接在这个基础上进行组合
- onMessageSend :发送数据的方法,这个方法和 ProChat.sendMessage(Hooks) 本质上是一个方法,用于向 ProChat 的数据流发送一条数据
- onClearAllHistory : 清空当前对话的方法,这个方法和 ProChat.clearMessage(Hooks) 本质上是一个方法

这下子你就可以随意组合当前的内容,以及你打算做的各种需求,例如:阻止一些不好的对话、上传内容的前置校验等

## 一个图片上传的演示案例

<code src="./demos/renderInputArea.tsx" ></code>

我们来详细拆解下这个案例

### 默认使用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 的预览能力进行支持的,如果遇到了内容的文件,我们建议采用 `<a/>` 来进行渲染,然后使用 messageItemExtraRender 在下方进行额外文件的预览渲染
> 其实 Markdown 是支持 Html 渲染的,但是我们默认并没有开启这个能力,考虑各方面我们并不打算默认打开这个,我们建议你采用 messageItemExtraRender
```ts
messageItemExtraRender: (message: ChatMessage, type: 'assistant' | 'user') => React.ReactNode;
```
messageItemExtraRender 可以拿到当前的 message,可以做很多自定义渲染的工作。
53 changes: 45 additions & 8 deletions src/ProChat/components/InputArea/index.tsx
Original file line number Diff line number Diff line change
@@ -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';

Expand Down Expand Up @@ -51,27 +52,51 @@ 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<boolean>;
renderInputArea?: (
defaultDom: ReactNode,
onMessageSend: (message: string) => void | Promise<any>,
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('');
const isChineseInput = useRef(false);
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 = (
<ConfigProvider
theme={{
token: {
Expand Down Expand Up @@ -115,7 +140,7 @@ export const ChatInputArea = ({ className }: { className?: string }) => {
/>
{mobile ? null : (
<Button
loading={isLoading}
loading={ButtonLoading}
type="text"
className={styles.btn}
onClick={() => send()}
Expand All @@ -126,6 +151,18 @@ export const ChatInputArea = ({ className }: { className?: string }) => {
</Flexbox>
</ConfigProvider>
);

if (renderInputArea) {
return renderInputArea(
defaultInputArea,
(message) => {
sendMessage(message);
},
clearMessage,
);
}

return defaultInputArea;
};

export default ChatInputArea;
10 changes: 7 additions & 3 deletions src/ProChat/container/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -24,16 +24,20 @@ const useStyles = createStyles(
);

interface ConversationProps extends ProChatProps<any> {
chatInput?: ReactNode;
showTitle?: boolean;
style?: CSSProperties;
className?: string;
chatRef?: ProChatChatReference;
renderInputArea?: (
defaultDom: ReactNode,
onMessageSend: (message: string) => void | Promise<any>,
onClearAllHistory: () => void,
) => ReactNode;
}

const App = memo<ConversationProps>(
({
chatInput,
renderInputArea,
className,
style,
showTitle,
Expand Down Expand Up @@ -106,7 +110,7 @@ const App = memo<ConversationProps>(
/>
) : null}
</>
<div ref={areaHtml}>{chatInput ?? <ChatInputArea />}</div>
<div ref={areaHtml}>{<ChatInputArea renderInputArea={renderInputArea} />}</div>
</Flexbox>
</RcResizeObserver>
);
Expand Down
10 changes: 7 additions & 3 deletions src/ProChat/container/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,11 @@ import { ProChatProvider } from './Provider';
import { ProChatChatReference } from './StoreUpdater';

export interface ProChatProps<T extends Record<string, any>> extends ChatProps<T> {
renderInput?: ReactNode;
renderInputArea?: (
defaultDom: ReactNode,
onMessageSend: (message: string) => void | Promise<any>,
onClearAllHistory: () => void,
) => ReactNode;
__PRO_CHAT_STORE_DEVTOOLS__?: boolean | DevtoolsOptions;
showTitle?: boolean;
style?: CSSProperties;
Expand All @@ -21,7 +25,7 @@ export interface ProChatProps<T extends Record<string, any>> extends ChatProps<T
}

export function ProChat<T extends Record<string, any> = Record<string, any>>({
renderInput,
renderInputArea,
__PRO_CHAT_STORE_DEVTOOLS__,
showTitle,
style,
Expand All @@ -44,7 +48,7 @@ export function ProChat<T extends Record<string, any> = Record<string, any>>({
>
<App
chatItemRenderConfig={chatItemRenderConfig}
chatInput={renderInput}
renderInputArea={renderInputArea}
chatRef={props.chatRef}
showTitle={showTitle}
style={style}
Expand Down
Loading

0 comments on commit d09e04a

Please sign in to comment.