Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

✨ feat: 支持自定义 InputArea 区域,并添加 多模态示范案例 #74

Merged
merged 1 commit into from
Feb 2, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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) => {
ONLY-yours marked this conversation as resolved.
Show resolved Hide resolved
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);
ONLY-yours marked this conversation as resolved.
Show resolved Hide resolved

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
Loading