diff --git a/components/__tests__/__snapshots__/index.test.ts.snap b/components/__tests__/__snapshots__/index.test.ts.snap
index 50f2581df9e6..8de60f5d3159 100644
--- a/components/__tests__/__snapshots__/index.test.ts.snap
+++ b/components/__tests__/__snapshots__/index.test.ts.snap
@@ -16,6 +16,7 @@ exports[`antd exports modules correctly 1`] = `
"Card",
"Carousel",
"Cascader",
+ "Chatbox",
"Checkbox",
"Col",
"Collapse",
diff --git a/components/chatbox/__tests__/__snapshots__/demo-extend.test.ts.snap b/components/chatbox/__tests__/__snapshots__/demo-extend.test.ts.snap
new file mode 100644
index 000000000000..cb828546aa38
--- /dev/null
+++ b/components/chatbox/__tests__/__snapshots__/demo-extend.test.ts.snap
@@ -0,0 +1,313 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`renders components/chatbox/demo/avatar-and-placement.tsx extend context correctly 1`] = `
+
+
+
+
+ Good morning, how are you ?
+
+
+
+
+
+
+
+
+
+ What a beautiful day !
+
+
+
+
+
+ Hi, good morning, I'm fine !
+
+
+
+
+
+
+
+
+
+ Thank you !
+
+
+
+`;
+
+exports[`renders components/chatbox/demo/avatar-and-placement.tsx extend context correctly 2`] = `[]`;
+
+exports[`renders components/chatbox/demo/basic.tsx extend context correctly 1`] = `
+
+`;
+
+exports[`renders components/chatbox/demo/basic.tsx extend context correctly 2`] = `[]`;
+
+exports[`renders components/chatbox/demo/contentRender.tsx extend context correctly 1`] = `
+
+`;
+
+exports[`renders components/chatbox/demo/contentRender.tsx extend context correctly 2`] = `[]`;
+
+exports[`renders components/chatbox/demo/loading.tsx extend context correctly 1`] = `
+
+
+
+ Loading state:
+
+
+
+`;
+
+exports[`renders components/chatbox/demo/loading.tsx extend context correctly 2`] = `[]`;
+
+exports[`renders components/chatbox/demo/typing.tsx extend context correctly 1`] = `
+
+`;
+
+exports[`renders components/chatbox/demo/typing.tsx extend context correctly 2`] = `[]`;
diff --git a/components/chatbox/__tests__/__snapshots__/demo.test.ts.snap b/components/chatbox/__tests__/__snapshots__/demo.test.ts.snap
new file mode 100644
index 000000000000..09e60f547b80
--- /dev/null
+++ b/components/chatbox/__tests__/__snapshots__/demo.test.ts.snap
@@ -0,0 +1,297 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`renders components/chatbox/demo/avatar-and-placement.tsx correctly 1`] = `
+
+
+
+
+ Good morning, how are you ?
+
+
+
+
+
+
+
+
+
+ What a beautiful day !
+
+
+
+
+
+ Hi, good morning, I'm fine !
+
+
+
+
+
+
+
+
+
+ Thank you !
+
+
+
+`;
+
+exports[`renders components/chatbox/demo/basic.tsx correctly 1`] = `
+
+`;
+
+exports[`renders components/chatbox/demo/contentRender.tsx correctly 1`] = `
+
+`;
+
+exports[`renders components/chatbox/demo/loading.tsx correctly 1`] = `
+
+
+
+ Loading state:
+
+
+
+`;
+
+exports[`renders components/chatbox/demo/typing.tsx correctly 1`] = `
+
+`;
diff --git a/components/chatbox/__tests__/__snapshots__/index.test.tsx.snap b/components/chatbox/__tests__/__snapshots__/index.test.tsx.snap
new file mode 100644
index 000000000000..c3dbc3f1388d
--- /dev/null
+++ b/components/chatbox/__tests__/__snapshots__/index.test.tsx.snap
@@ -0,0 +1,25 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`chatbox Chatbox component work 1`] = `
+
+`;
+
+exports[`chatbox rtl render component should be rendered correctly in RTL direction 1`] = `
+
+`;
diff --git a/components/chatbox/__tests__/demo-extend.test.ts b/components/chatbox/__tests__/demo-extend.test.ts
new file mode 100644
index 000000000000..4940e499e235
--- /dev/null
+++ b/components/chatbox/__tests__/demo-extend.test.ts
@@ -0,0 +1,3 @@
+import { extendTest } from '../../../tests/shared/demoTest';
+
+extendTest('chatbox');
diff --git a/components/chatbox/__tests__/demo.test.ts b/components/chatbox/__tests__/demo.test.ts
new file mode 100644
index 000000000000..549211096c11
--- /dev/null
+++ b/components/chatbox/__tests__/demo.test.ts
@@ -0,0 +1,3 @@
+import demoTest from '../../../tests/shared/demoTest';
+
+demoTest('chatbox');
diff --git a/components/chatbox/__tests__/image.test.ts b/components/chatbox/__tests__/image.test.ts
new file mode 100644
index 000000000000..7e79d7a14b77
--- /dev/null
+++ b/components/chatbox/__tests__/image.test.ts
@@ -0,0 +1,5 @@
+import { imageDemoTest } from '../../../tests/shared/imageTest';
+
+describe('chatbox image', () => {
+ imageDemoTest('chatbox');
+});
diff --git a/components/chatbox/__tests__/index.test.tsx b/components/chatbox/__tests__/index.test.tsx
new file mode 100644
index 000000000000..9289b0f43447
--- /dev/null
+++ b/components/chatbox/__tests__/index.test.tsx
@@ -0,0 +1,102 @@
+import React from 'react';
+
+import Chatbox from '..';
+import mountTest from '../../../tests/shared/mountTest';
+import rtlTest from '../../../tests/shared/rtlTest';
+import { render, waitFakeTimer } from '../../../tests/utils';
+
+describe('chatbox', () => {
+ mountTest(() => );
+ rtlTest(() => );
+
+ beforeAll(() => {
+ jest.useFakeTimers();
+ });
+
+ afterAll(() => {
+ jest.useRealTimers();
+ });
+
+ it('Chatbox component work', () => {
+ const { container } = render();
+ const element = container.querySelector('.ant-chatbox');
+ expect(element).toBeTruthy();
+ expect(element).toMatchSnapshot();
+ });
+
+ it('Chatbox support content', () => {
+ const { container } = render();
+ const element = container.querySelector('.ant-chatbox .ant-chatbox-content');
+ expect(element?.textContent).toBe('hello world');
+ });
+
+ it('Chatbox support contentRender', () => {
+ const { container } = render(
+ {content}}
+ />,
+ );
+ const element = container.querySelector('.ant-chatbox .test-contentRender');
+ expect(element).toBeTruthy();
+ expect(element?.textContent).toBe('test-contentRender');
+ });
+
+ it('Chatbox support typing', () => {
+ const { container } = render();
+ expect(container.querySelector('.ant-chatbox')).toHaveClass(
+ 'ant-chatbox-typing',
+ );
+ });
+
+ it('Chatbox support avatar', () => {
+ const { container } = render(
+ avatar} content="" />,
+ );
+ expect(container.querySelector('.ant-chatbox .test-avatar')).toBeTruthy();
+ });
+
+ it('Chatbox support loading', () => {
+ const { container } = render();
+ const selectors = '.ant-chatbox .ant-chatbox-content .ant-chatbox-dot';
+ expect(container.querySelector(selectors)).toBeTruthy();
+ });
+
+ it('Chatbox support placement', () => {
+ const { container, rerender } = render();
+ const element = container.querySelector('.ant-chatbox');
+ expect(element).toHaveClass('ant-chatbox-start');
+ rerender();
+ expect(element).toHaveClass('ant-chatbox-end');
+ });
+
+ it('Chatbox support typing effect', async () => {
+ const { container } = render();
+ const element = container.querySelector('.ant-chatbox .ant-chatbox-content');
+ expect(element?.textContent).toBe('你');
+ await waitFakeTimer();
+ expect(element?.textContent).toBe('你好你好你好');
+ });
+
+ it('Chatbox Should support className & classNames & style & styles', () => {
+ const { container } = render(
+ avatar}
+ className="test-className"
+ classNames={{ avatar: 'test-avatar', content: 'test-content' }}
+ style={{ backgroundColor: 'green' }}
+ styles={{ avatar: { color: 'red' }, content: { color: 'blue' } }}
+ />,
+ );
+ const element = container.querySelector('.ant-chatbox');
+ const avatarElement = element?.querySelector('.ant-chatbox-avatar');
+ const contentElement = element?.querySelector('.ant-chatbox-content');
+ expect(element).toHaveClass('test-className');
+ expect(avatarElement).toHaveClass('test-avatar');
+ expect(contentElement).toHaveClass('test-content');
+ expect(element).toHaveStyle({ backgroundColor: 'green' });
+ expect(avatarElement).toHaveStyle({ color: 'red' });
+ expect(contentElement).toHaveStyle({ color: 'blue' });
+ });
+});
diff --git a/components/chatbox/demo/_semantic.tsx b/components/chatbox/demo/_semantic.tsx
new file mode 100644
index 000000000000..6c93b9fce49d
--- /dev/null
+++ b/components/chatbox/demo/_semantic.tsx
@@ -0,0 +1,36 @@
+import React from 'react';
+import { UserOutlined } from '@ant-design/icons';
+import { Avatar, Chatbox } from 'antd';
+
+import SemanticPreview from '../../../.dumi/components/SemanticPreview';
+import useLocale from '../../../.dumi/hooks/useLocale';
+
+const locales = {
+ cn: {
+ avatar: '头像的外层容器',
+ content: '聊天内容的容器',
+ },
+ en: {
+ avatar: 'Wrapper element of the avatar',
+ content: 'Wrapper element of the content',
+ },
+};
+
+const App: React.FC = () => {
+ const [locale] = useLocale(locales);
+ return (
+
+ } />}
+ />
+
+ );
+};
+
+export default App;
diff --git a/components/chatbox/demo/avatar-and-placement.md b/components/chatbox/demo/avatar-and-placement.md
new file mode 100644
index 000000000000..6928e1e1c2b8
--- /dev/null
+++ b/components/chatbox/demo/avatar-and-placement.md
@@ -0,0 +1,7 @@
+## zh-CN
+
+通过 `avatar` 设置自定义头像,通过 `placement` 设置位置,提供了 `start`、`end` 两个选项。
+
+## en-US
+
+Set custom avatar by `avatar` prop, set the placement of the message by `placement` prop, which has two preset values: `start` and `end`.
diff --git a/components/chatbox/demo/avatar-and-placement.tsx b/components/chatbox/demo/avatar-and-placement.tsx
new file mode 100644
index 000000000000..02f7ab2adffb
--- /dev/null
+++ b/components/chatbox/demo/avatar-and-placement.tsx
@@ -0,0 +1,46 @@
+import React from 'react';
+import { UserOutlined } from '@ant-design/icons';
+import { Avatar, Chatbox, Flex } from 'antd';
+
+const fooAvatar: React.CSSProperties = {
+ color: '#f56a00',
+ backgroundColor: '#fde3cf',
+};
+
+const barAvatar: React.CSSProperties = {
+ color: '#fff',
+ backgroundColor: '#87d068',
+};
+
+const hideAvatar: React.CSSProperties = {
+ visibility: 'hidden',
+};
+
+const App: React.FC = () => (
+
+ } style={fooAvatar} />}
+ />
+ }
+ />
+ } style={barAvatar} />}
+ />
+ }
+ />
+
+);
+
+export default App;
diff --git a/components/chatbox/demo/basic.md b/components/chatbox/demo/basic.md
new file mode 100644
index 000000000000..673339b1ba7d
--- /dev/null
+++ b/components/chatbox/demo/basic.md
@@ -0,0 +1,7 @@
+## zh-CN
+
+基础用法。
+
+## en-US
+
+Basic usage.
diff --git a/components/chatbox/demo/basic.tsx b/components/chatbox/demo/basic.tsx
new file mode 100644
index 000000000000..329c4fd82e58
--- /dev/null
+++ b/components/chatbox/demo/basic.tsx
@@ -0,0 +1,6 @@
+import React from 'react';
+import { Chatbox } from 'antd';
+
+const App = () => ;
+
+export default App;
diff --git a/components/chatbox/demo/contentRender.md b/components/chatbox/demo/contentRender.md
new file mode 100644
index 000000000000..a985e8857d5a
--- /dev/null
+++ b/components/chatbox/demo/contentRender.md
@@ -0,0 +1,7 @@
+## zh-CN
+
+配合 `markdown-it` 实现自定义渲染内容。
+
+## en-US
+
+Cooperate with `markdown-it` to achieve customized rendering content.
diff --git a/components/chatbox/demo/contentRender.tsx b/components/chatbox/demo/contentRender.tsx
new file mode 100644
index 000000000000..a0a9e39bc8d7
--- /dev/null
+++ b/components/chatbox/demo/contentRender.tsx
@@ -0,0 +1,47 @@
+/* eslint-disable react/no-danger */
+import React from 'react';
+import { UserOutlined } from '@ant-design/icons';
+import { Avatar, Chatbox } from 'antd';
+import type { ChatboxProps } from 'antd';
+import markdownit from 'markdown-it';
+
+const sentences = [
+ '# Title \n An enterprise-class UI design language and React UI library. \n ...丨',
+ '# 标题 \n 企业级产品设计体系,创造高效愉悦的工作体验。\n ...丨',
+];
+
+const md = markdownit({ html: true, breaks: true });
+
+const useLoopSentence = () => {
+ const [index, setIndex] = React.useState(0);
+ const timerRef = React.useRef>();
+ React.useEffect(() => {
+ timerRef.current = setTimeout(
+ () => setIndex((prevState) => (prevState ? 0 : 1)),
+ sentences[index].length * 100 + 1000,
+ );
+ return () => clearTimeout(timerRef.current);
+ }, [index]);
+ return sentences[index];
+};
+
+const contentRender: ChatboxProps['contentRender'] = (content) => {
+ if (!content) {
+ return null;
+ }
+ return ;
+};
+
+const App: React.FC = () => {
+ const content = useLoopSentence();
+ return (
+ } />}
+ />
+ );
+};
+
+export default App;
diff --git a/components/chatbox/demo/loading.md b/components/chatbox/demo/loading.md
new file mode 100644
index 000000000000..8f0c95770dff
--- /dev/null
+++ b/components/chatbox/demo/loading.md
@@ -0,0 +1,7 @@
+## zh-CN
+
+通过 `loading` 属性控制加载状态。
+
+## en-US
+
+Control the loading state by `loading` prop.
diff --git a/components/chatbox/demo/loading.tsx b/components/chatbox/demo/loading.tsx
new file mode 100644
index 000000000000..5812ff7b167b
--- /dev/null
+++ b/components/chatbox/demo/loading.tsx
@@ -0,0 +1,22 @@
+import React from 'react';
+import { UserOutlined } from '@ant-design/icons';
+import { Avatar, Chatbox, Flex, Switch } from 'antd';
+
+const App: React.FC = () => {
+ const [loading, setLoading] = React.useState(true);
+ return (
+
+ } />}
+ />
+
+ Loading state:
+
+
+
+ );
+};
+
+export default App;
diff --git a/components/chatbox/demo/typing.md b/components/chatbox/demo/typing.md
new file mode 100644
index 000000000000..0715f21b0875
--- /dev/null
+++ b/components/chatbox/demo/typing.md
@@ -0,0 +1,7 @@
+## zh-CN
+
+通过设置 `typing` 属性,开启打字效果。
+
+## en-US
+
+Enable typing output by setting the `typing` prop.
diff --git a/components/chatbox/demo/typing.tsx b/components/chatbox/demo/typing.tsx
new file mode 100644
index 000000000000..5e432508da83
--- /dev/null
+++ b/components/chatbox/demo/typing.tsx
@@ -0,0 +1,31 @@
+import React from 'react';
+import { UserOutlined } from '@ant-design/icons';
+import { Avatar, Chatbox } from 'antd';
+
+const sentences = ['Feel free to use Ant Design !', '欢迎使用 Ant Design!'];
+
+const useLoopSentence = () => {
+ const [index, setIndex] = React.useState(0);
+ const timerRef = React.useRef>();
+ React.useEffect(() => {
+ timerRef.current = setTimeout(
+ () => setIndex((prevState) => (prevState ? 0 : 1)),
+ sentences[index].length * 100 + 1000,
+ );
+ return () => clearTimeout(timerRef.current);
+ }, [index]);
+ return sentences[index];
+};
+
+const App: React.FC = () => {
+ const content = useLoopSentence();
+ return (
+ } />}
+ />
+ );
+};
+
+export default App;
diff --git a/components/chatbox/hooks/useTypedEffect.ts b/components/chatbox/hooks/useTypedEffect.ts
new file mode 100644
index 000000000000..1565ba5b955e
--- /dev/null
+++ b/components/chatbox/hooks/useTypedEffect.ts
@@ -0,0 +1,46 @@
+import React from 'react';
+
+import type { TypingOption } from '../interface';
+
+const useTypedEffect = (content?: string, mergedTyping?: Required | false) => {
+ const [typedContent, setTypedContent] = React.useState('');
+ const [isTyping, setIsTyping] = React.useState(mergedTyping !== false);
+
+ const timerRef = React.useRef>();
+
+ const clearTimer = () => {
+ if (timerRef.current) {
+ clearTimeout(timerRef.current);
+ }
+ };
+
+ React.useEffect(() => {
+ if (!content || !mergedTyping) {
+ return;
+ }
+ setIsTyping(true);
+ let stepCount = 0;
+ const { step, interval } = mergedTyping;
+
+ const typedTimer = () => {
+ stepCount += step;
+ setTypedContent(content.slice(0, stepCount) ?? '');
+ if (stepCount < content.length) {
+ timerRef.current = setTimeout(typedTimer, interval);
+ } else {
+ setIsTyping(false);
+ }
+ };
+
+ typedTimer();
+
+ return () => {
+ clearTimer();
+ setIsTyping(false);
+ };
+ }, [content, mergedTyping]);
+
+ return { typedContent, isTyping };
+};
+
+export default useTypedEffect;
diff --git a/components/chatbox/hooks/useTypingValue.ts b/components/chatbox/hooks/useTypingValue.ts
new file mode 100644
index 000000000000..bd1975327f54
--- /dev/null
+++ b/components/chatbox/hooks/useTypingValue.ts
@@ -0,0 +1,30 @@
+import React from 'react';
+
+import type { ChatboxProps, TypingOption } from '../interface';
+
+function isObject(value: any): value is Record {
+ return value && typeof value === 'object';
+}
+
+const defaultTypingOption: Required = {
+ step: 1,
+ interval: 100,
+};
+
+const useTypingValue = (typing: ChatboxProps['typing']) => {
+ const mergedTyping = React.useMemo | false>(
+ () => {
+ if (isObject(typing)) {
+ return { ...defaultTypingOption, ...typing };
+ }
+ if (typing === true) {
+ return defaultTypingOption;
+ }
+ return false;
+ },
+ isObject(typing) ? [typing.interval, typing.step] : [typing],
+ );
+ return mergedTyping;
+};
+
+export default useTypingValue;
diff --git a/components/chatbox/index.en-US.md b/components/chatbox/index.en-US.md
new file mode 100644
index 000000000000..555a15afe175
--- /dev/null
+++ b/components/chatbox/index.en-US.md
@@ -0,0 +1,51 @@
+---
+category: Components
+group: Data Display
+title: Chatbox
+description: A bubble component for chat.
+cover: https://mdn.alipayobjects.com/huamei_7uahnr/afts/img/A*NMvqRZpuJfQAAAAAAAAAAAAADrJ8AQ/original
+coverDark: https://mdn.alipayobjects.com/huamei_7uahnr/afts/img/A*D70qQJJmzhgAAAAAAAAAAAAADrJ8AQ/original
+demo:
+ cols: 2
+tag: 5.19.0
+---
+
+## When To Use
+
+Often used when chatting.
+
+## Examples
+
+
+Basic
+Placement and avatar
+Loading
+Typing effect
+Content render
+
+## API
+
+Common props ref:[Common props](/docs/react/common-props)
+
+> This component is available since `antd@5.19.0`.
+
+### Chatbox
+
+| Property | Description | Type | Default | Version |
+| --- | --- | --- | --- | --- |
+| avatar | Avatar component | `React.ReactNode` | - | |
+| classNames | Semantic DOM class | [Record](#semantic-dom) | - | |
+| styles | Semantic DOM style | [Record](#semantic-dom) | - | |
+| placement | Direction of Message | `start \| end` | `start` | |
+| loading | Loading state of Message | `boolean` | - | |
+| typing | Show message with typing motion | `boolean \| { step?: number, interval?: number }` | `false` | |
+| content | Content of Chatbox | `string` | - | |
+| contentRender | Display cuztomized content | `(content?: string) => ReactNode` | - | |
+
+## Semantic DOM
+
+
+
+## Design Token
+
+
diff --git a/components/chatbox/index.tsx b/components/chatbox/index.tsx
new file mode 100644
index 000000000000..a48455b10446
--- /dev/null
+++ b/components/chatbox/index.tsx
@@ -0,0 +1,86 @@
+import React from 'react';
+import classnames from 'classnames';
+
+import { ConfigContext } from '../config-provider';
+import type { ConfigConsumerProps } from '../config-provider';
+import useTypedEffect from './hooks/useTypedEffect';
+import useTypingValue from './hooks/useTypingValue';
+import type { ChatboxProps } from './interface';
+import Loading from './loading';
+import useStyle from './style';
+
+const Chatbox: React.FC = (props) => {
+ const {
+ prefixCls: customizePrefixCls,
+ className,
+ rootClassName,
+ style,
+ classNames,
+ styles,
+ avatar,
+ placement = 'start',
+ loading = false,
+ typing,
+ content,
+ contentRender,
+ ...otherHtmlProps
+ } = props;
+ const { direction, chatbox, getPrefixCls } = React.useContext(ConfigContext);
+ const prefixCls = getPrefixCls('chatbox', customizePrefixCls);
+ const [wrapCSSVar, hashId, cssVarCls] = useStyle(prefixCls);
+
+ const mergedTyping = useTypingValue(typing);
+
+ const { typedContent, isTyping } = useTypedEffect(content, mergedTyping);
+
+ const mergedCls = classnames(
+ className,
+ rootClassName,
+ chatbox?.className,
+ prefixCls,
+ hashId,
+ cssVarCls,
+ `${prefixCls}-${placement}`,
+ {
+ [`${prefixCls}-rtl`]: direction === 'rtl',
+ [`${prefixCls}-typing`]: isTyping && !loading && !contentRender,
+ },
+ );
+
+ const mergedAvatarCls = classnames(
+ `${prefixCls}-avatar`,
+ classNames?.avatar,
+ chatbox?.classNames?.avatar,
+ );
+
+ const mergedContentCls = classnames(
+ `${prefixCls}-content`,
+ classNames?.content,
+ chatbox?.classNames?.content,
+ );
+
+ const mergedText = mergedTyping !== false ? typedContent : content;
+
+ const mergedContent = contentRender ? contentRender(mergedText) : mergedText;
+
+ return wrapCSSVar(
+
+ {avatar && (
+
+ {avatar}
+
+ )}
+
+ {loading ? : mergedContent}
+
+
,
+ );
+};
+
+if (process.env.NODE_ENV !== 'production') {
+ Chatbox.displayName = 'Chatbox';
+}
+
+export type { ChatboxProps };
+
+export default Chatbox;
diff --git a/components/chatbox/index.zh-CN.md b/components/chatbox/index.zh-CN.md
new file mode 100644
index 000000000000..d16f2cc4b5b5
--- /dev/null
+++ b/components/chatbox/index.zh-CN.md
@@ -0,0 +1,52 @@
+---
+category: Components
+group: 数据展示
+title: Chatbox
+subtitle: 聊天框
+description: 用于聊天的气泡组件。
+cover: https://mdn.alipayobjects.com/huamei_7uahnr/afts/img/A*NMvqRZpuJfQAAAAAAAAAAAAADrJ8AQ/original
+coverDark: https://mdn.alipayobjects.com/huamei_7uahnr/afts/img/A*D70qQJJmzhgAAAAAAAAAAAAADrJ8AQ/original
+demo:
+ cols: 2
+tag: 5.19.0
+---
+
+## 何时使用
+
+常用于聊天的时候。
+
+## 代码演示
+
+
+基本
+支持位置和头像
+加载中
+打字效果
+自定义渲染
+
+## API
+
+通用属性参考:[通用属性](/docs/react/common-props)
+
+> 自 `antd@5.19.0` 版本开始提供该组件。
+
+### Chatbox
+
+| 属性 | 说明 | 类型 | 默认值 | 版本 |
+| --- | --- | --- | --- | --- |
+| avatar | 展示头像 | `React.ReactNode` | - | |
+| classNames | 语义化结构 class | [Record](#semantic-dom) | - | |
+| styles | 语义化结构 style | [Record](#semantic-dom) | - | |
+| placement | 信息位置 | `start \| end` | `start` | |
+| loading | 聊天内容加载状态 | `boolean` | - | |
+| typing | 设置聊天内容打字动画 | `boolean \| { step?: number, interval?: number }` | `false` | |
+| content | 聊天内容 | `string` | - | |
+| contentRender | 自定义渲染内容 | `(content?: string) => ReactNode` | - | |
+
+## Semantic DOM
+
+
+
+## 主题变量(Design Token)
+
+
diff --git a/components/chatbox/interface.ts b/components/chatbox/interface.ts
new file mode 100644
index 000000000000..6d51e133189e
--- /dev/null
+++ b/components/chatbox/interface.ts
@@ -0,0 +1,31 @@
+export interface TypingOption {
+ /**
+ * @since 5.19.0
+ * @default 1
+ */
+ step?: number;
+ /**
+ * @since 5.19.0
+ * @default 100
+ */
+ interval?: number;
+}
+
+export interface ChatboxProps extends React.HTMLAttributes {
+ prefixCls?: string;
+ rootClassName?: string;
+ classNames?: {
+ avatar?: string;
+ content?: string;
+ };
+ styles?: {
+ avatar?: React.CSSProperties;
+ content?: React.CSSProperties;
+ };
+ avatar?: React.ReactNode;
+ placement?: 'start' | 'end';
+ loading?: boolean;
+ typing?: boolean | TypingOption;
+ content: string;
+ contentRender?: (content?: string) => React.ReactNode;
+}
diff --git a/components/chatbox/loading.tsx b/components/chatbox/loading.tsx
new file mode 100644
index 000000000000..5e4bd3afcbe7
--- /dev/null
+++ b/components/chatbox/loading.tsx
@@ -0,0 +1,18 @@
+import React from 'react';
+
+interface LoadingProps {
+ prefixCls?: string;
+}
+
+const Loading: React.FC = (props) => {
+ const { prefixCls } = props;
+ return (
+
+
+
+
+
+ );
+};
+
+export default Loading;
diff --git a/components/chatbox/style/index.ts b/components/chatbox/style/index.ts
new file mode 100644
index 000000000000..cbf4a86baafb
--- /dev/null
+++ b/components/chatbox/style/index.ts
@@ -0,0 +1,131 @@
+import { Keyframes, unit } from '@ant-design/cssinjs';
+
+import type { FullToken, GenerateStyle, GetDefaultToken } from '../../theme/internal';
+import { genStyleHooks, mergeToken } from '../../theme/internal';
+
+const loadingMove = new Keyframes('loadingMove', {
+ '0%': {
+ transform: 'translateY(0)',
+ },
+ '10%': {
+ transform: 'translateY(4px)',
+ },
+ '20%': {
+ transform: 'translateY(0)',
+ },
+ '30%': {
+ transform: 'translateY(-4px)',
+ },
+ '40%': {
+ transform: 'translateY(0)',
+ },
+});
+
+const cursorBlink = new Keyframes('cursorBlink', {
+ '0%': {
+ opacity: 1,
+ },
+ '50%': {
+ opacity: 0,
+ },
+ '100%': {
+ opacity: 1,
+ },
+});
+
+export interface ComponentToken {
+ //
+}
+
+export interface ChatboxToken extends FullToken<'Chatbox'> {
+ chatboxContentMaxWidth: number | string;
+}
+
+const genChatboxStyle: GenerateStyle = (token) => {
+ const { componentCls, fontSize, lineHeight, paddingSM, padding, paddingXS, colorText, calc } =
+ token;
+ return {
+ [componentCls]: {
+ display: 'flex',
+ columnGap: paddingXS,
+ maxWidth: '100%',
+ [`&${componentCls}-end`]: {
+ justifyContent: 'end',
+ flexDirection: 'row-reverse',
+ },
+ [`&${componentCls}-rtl`]: {
+ direction: 'rtl',
+ },
+ [`&${componentCls}-typing ${componentCls}-content:last-child::after`]: {
+ content: '"|"',
+ fontWeight: 900,
+ userSelect: 'none',
+ opacity: 1,
+ marginInlineStart: '0.1em',
+ animationName: cursorBlink,
+ animationDuration: '0.8s',
+ animationIterationCount: 'infinite',
+ animationTimingFunction: 'linear',
+ },
+ [`& ${componentCls}-avatar`]: {
+ display: 'inline-flex',
+ justifyContent: 'center',
+ },
+ [`& ${componentCls}-content`]: {
+ position: 'relative',
+ padding: `${unit(paddingSM)} ${unit(padding)}`,
+ color: colorText,
+ fontSize: token.fontSize,
+ lineHeight: token.lineHeight,
+ minHeight: calc(paddingSM).mul(2).add(calc(lineHeight).mul(fontSize)).equal(),
+ maxWidth: token.chatboxContentMaxWidth,
+ backgroundColor: token.colorInfoBg,
+ borderRadius: token.borderRadiusLG,
+ boxShadow: token.boxShadowTertiary,
+ [`& ${componentCls}-dot`]: {
+ position: 'relative',
+ height: '100%',
+ display: 'flex',
+ alignItems: 'center',
+ columnGap: token.marginXS,
+ padding: `0 ${unit(token.paddingXXS)}`,
+ '&-item': {
+ backgroundColor: token.colorPrimary,
+ borderRadius: '100%',
+ width: 4,
+ height: 4,
+ animationName: loadingMove,
+ animationDuration: '2s',
+ animationIterationCount: 'infinite',
+ animationTimingFunction: 'linear',
+ '&:nth-child(1)': {
+ animationDelay: '0s',
+ },
+ '&:nth-child(2)': {
+ animationDelay: '0.2s',
+ },
+ '&:nth-child(3)': {
+ animationDelay: '0.4s',
+ },
+ },
+ },
+ },
+ },
+ };
+};
+
+export const prepareComponentToken: GetDefaultToken<'Chatbox'> = () => ({
+ //
+});
+
+export default genStyleHooks<'Chatbox'>(
+ 'Chatbox',
+ (token) => {
+ const { paddingXS, calc } = token;
+ const chatBoxToken = mergeToken(token, {
+ chatboxContentMaxWidth: `calc(100% - ${calc(paddingXS).add(32).equal()})`,
+ });
+ return genChatboxStyle(chatBoxToken);
+ },
+ prepareComponentToken,
+);
diff --git a/components/config-provider/__tests__/style.test.tsx b/components/config-provider/__tests__/style.test.tsx
index 4050da315f07..c37da8ef88b1 100644
--- a/components/config-provider/__tests__/style.test.tsx
+++ b/components/config-provider/__tests__/style.test.tsx
@@ -11,6 +11,7 @@ import Calendar from '../../calendar';
import Card from '../../card';
import Carousel from '../../carousel';
import Cascader from '../../cascader';
+import Chatbox from '../../chatbox';
import Checkbox from '../../checkbox';
import Collapse from '../../collapse';
import ColorPicker from '../../color-picker';
@@ -1575,4 +1576,28 @@ describe('ConfigProvider support style and className props', () => {
const element = container.querySelector('.test-cp-icon');
expect(element).toBeTruthy();
});
+
+ it('CP Should support Chatbox', () => {
+ const { container } = render(
+
+ avatar} />
+ ,
+ );
+ const element = container.querySelector('.ant-chatbox');
+ const avatarElement = element?.querySelector('.ant-chatbox-avatar');
+ const contentElement = element?.querySelector('.ant-chatbox-content');
+ expect(element).toHaveClass('test-cp-className');
+ expect(avatarElement).toHaveClass('test-cp-avatar');
+ expect(contentElement).toHaveClass('test-cp-content');
+ expect(element).toHaveStyle({ backgroundColor: 'green' });
+ expect(avatarElement).toHaveStyle({ color: 'red' });
+ expect(contentElement).toHaveStyle({ color: 'blue' });
+ });
});
diff --git a/components/config-provider/context.ts b/components/config-provider/context.ts
index 2e20f2249b91..4d1fc72a5f67 100644
--- a/components/config-provider/context.ts
+++ b/components/config-provider/context.ts
@@ -6,6 +6,7 @@ import type { AlertProps } from '../alert';
import type { BadgeProps } from '../badge';
import type { ButtonProps } from '../button';
import type { CardProps } from '../card';
+import type { ChatboxProps } from '../chatbox/interface';
import type { CollapseProps } from '../collapse';
import type { DrawerProps } from '../drawer';
import type { FlexProps } from '../flex/interface';
@@ -150,6 +151,8 @@ export type TagConfig = ComponentStyleConfig & Pick;
+export type ChatboxConfig = ComponentStyleConfig & Pick;
+
export type DrawerConfig = ComponentStyleConfig &
Pick;
@@ -222,6 +225,7 @@ export interface ConfigConsumerProps {
calendar?: ComponentStyleConfig;
carousel?: ComponentStyleConfig;
cascader?: ComponentStyleConfig;
+ chatbox?: ChatboxConfig;
collapse?: CollapseConfig;
floatButtonGroup?: FloatButtonGroupConfig;
typography?: ComponentStyleConfig;
diff --git a/components/config-provider/index.en-US.md b/components/config-provider/index.en-US.md
index 942cd4defec7..5bc4af31baca 100644
--- a/components/config-provider/index.en-US.md
+++ b/components/config-provider/index.en-US.md
@@ -106,6 +106,7 @@ const {
| breadcrumb | Set Breadcrumb common props | { className?: string, style?: React.CSSProperties } | - | 5.7.0 |
| button | Set Button common props | { className?: string, style?: React.CSSProperties, classNames?: { icon: string }, styles?: { icon: React.CSSProperties }, autoInsertSpace?: boolean } | - | 5.6.0, autoInsertSpace: 5.17.0 |
| card | Set Card common props | { className?: string, style?: React.CSSProperties, classNames?: [CardProps\["classNames"\]](/components/card#api), styles?: [CardProps\["styles"\]](/components/card#api) } | - | 5.7.0, `classNames` and `styles`: 5.14.0 |
+| chatbox | Set Chatbox common props | { className?: string, style?: React.CSSProperties, classNames?: { avatar?: string; content?: string; }, styles?: { avatar?: CSSProperties, content?: CSSProperties } } | - | 5.19.0 |
| calendar | Set Calendar common props | { className?: string, style?: React.CSSProperties } | - | 5.7.0 |
| carousel | Set Carousel common props | { className?: string, style?: React.CSSProperties } | - | 5.7.0 |
| cascader | Set Cascader common props | { className?: string, style?: React.CSSProperties } | - | 5.7.0 |
diff --git a/components/config-provider/index.tsx b/components/config-provider/index.tsx
index 99e536e717a2..8fb58285243e 100644
--- a/components/config-provider/index.tsx
+++ b/components/config-provider/index.tsx
@@ -19,6 +19,7 @@ import type {
BadgeConfig,
ButtonConfig,
CardConfig,
+ ChatboxConfig,
CollapseConfig,
ComponentStyleConfig,
ConfigConsumerProps,
@@ -162,6 +163,7 @@ export interface ConfigProviderProps {
calendar?: ComponentStyleConfig;
carousel?: ComponentStyleConfig;
cascader?: ComponentStyleConfig;
+ chatbox?: ChatboxConfig;
collapse?: CollapseConfig;
divider?: ComponentStyleConfig;
drawer?: DrawerConfig;
@@ -320,6 +322,7 @@ const ProviderChildren: React.FC = (props) => {
calendar,
carousel,
cascader,
+ chatbox,
collapse,
typography,
checkbox,
@@ -418,6 +421,7 @@ const ProviderChildren: React.FC = (props) => {
cascader,
collapse,
typography,
+ chatbox,
checkbox,
descriptions,
divider,
diff --git a/components/config-provider/index.zh-CN.md b/components/config-provider/index.zh-CN.md
index 4ee55456bde2..69bf60f3e1c4 100644
--- a/components/config-provider/index.zh-CN.md
+++ b/components/config-provider/index.zh-CN.md
@@ -109,6 +109,7 @@ const {
| button | 设置 Button 组件的通用属性 | { className?: string, style?: React.CSSProperties, classNames?: { icon: string }, styles?: { icon: React.CSSProperties }, autoInsertSpace?: boolean } | - | 5.6.0, autoInsertSpace: 5.17.0 |
| calendar | 设置 Calendar 组件的通用属性 | { className?: string, style?: React.CSSProperties } | - | 5.7.0 |
| card | 设置 Card 组件的通用属性 | { className?: string, style?: React.CSSProperties, classNames?: [CardProps\["classNames"\]](/components/card-cn#api), styles?: [CardProps\["styles"\]](/components/card-cn#api) } | - | 5.7.0, `classNames` 和 `styles`: 5.14.0 |
+| chatbox | 设置 Chatbox 组件的通用属性 | { className?: string, style?: React.CSSProperties, classNames?: { avatar?: string; content?: string; }, styles?: { avatar?: CSSProperties, content?: CSSProperties } } | - | 5.19.0 |
| carousel | 设置 Carousel 组件的通用属性 | { className?: string, style?: React.CSSProperties } | - | 5.7.0 |
| cascader | 设置 Cascader 组件的通用属性 | { className?: string, style?: React.CSSProperties } | - | 5.7.0 |
| checkbox | 设置 Checkbox 组件的通用属性 | { className?: string, style?: React.CSSProperties } | - | 5.7.0 |
diff --git a/components/index.ts b/components/index.ts
index 905586012069..573060dda531 100644
--- a/components/index.ts
+++ b/components/index.ts
@@ -29,6 +29,8 @@ export type { CarouselProps } from './carousel';
export { default as Cascader } from './cascader';
export type { CascaderProps, CascaderAutoProps } from './cascader';
export type { CascaderPanelProps, CascaderPanelAutoProps } from './cascader/Panel';
+export { default as Chatbox } from './chatbox';
+export type { ChatboxProps } from './chatbox';
export { default as Checkbox } from './checkbox';
export type { CheckboxOptionType, CheckboxProps, CheckboxRef } from './checkbox';
export { default as Col } from './col';
diff --git a/components/theme/interface/components.ts b/components/theme/interface/components.ts
index 2ecbe62e32c8..a4d7f94a411d 100644
--- a/components/theme/interface/components.ts
+++ b/components/theme/interface/components.ts
@@ -12,6 +12,7 @@ import type { ComponentToken as CalendarComponentToken } from '../../calendar/st
import type { ComponentToken as CardComponentToken } from '../../card/style';
import type { ComponentToken as CarouselComponentToken } from '../../carousel/style';
import type { ComponentToken as CascaderComponentToken } from '../../cascader/style';
+import type { ComponentToken as ChatboxComponentToken } from '../../chatbox/style';
import type { ComponentToken as CheckboxComponentToken } from '../../checkbox/style';
import type { ComponentToken as CollapseComponentToken } from '../../collapse/style';
import type { ComponentToken as ColorPickerComponentToken } from '../../color-picker/style';
@@ -76,6 +77,7 @@ export interface ComponentTokenMap {
Card?: CardComponentToken;
Carousel?: CarouselComponentToken;
Cascader?: CascaderComponentToken;
+ Chatbox?: ChatboxComponentToken;
Checkbox?: CheckboxComponentToken;
ColorPicker?: ColorPickerComponentToken;
Collapse?: CollapseComponentToken;
diff --git a/package.json b/package.json
index ab9053c32691..f273310d976a 100644
--- a/package.json
+++ b/package.json
@@ -206,6 +206,7 @@
"@types/jquery": "^3.5.29",
"@types/jsdom": "^21.1.6",
"@types/lodash": "^4.17.0",
+ "@types/markdown-it": "^14.0.1",
"@types/minimist": "^1.2.5",
"@types/node": "^20.12.7",
"@types/nprogress": "^0.2.3",
@@ -285,6 +286,7 @@
"lodash": "^4.17.21",
"lunar-typescript": "^1.7.5",
"lz-string": "^1.5.0",
+ "markdown-it": "^14.1.0",
"minimist": "^1.2.8",
"mockdate": "^3.0.5",
"node-fetch": "^3.3.2",
diff --git a/scripts/__snapshots__/check-site.ts.snap b/scripts/__snapshots__/check-site.ts.snap
index 40ca1dd0d32e..3d52a63028c0 100644
--- a/scripts/__snapshots__/check-site.ts.snap
+++ b/scripts/__snapshots__/check-site.ts.snap
@@ -56,6 +56,10 @@ exports[`site test Component components/checkbox en Page 1`] = `3`;
exports[`site test Component components/checkbox zh Page 1`] = `3`;
+exports[`site test Component components/chatbox en Page 1`] = `1`;
+
+exports[`site test Component components/chatbox zh Page 1`] = `1`;
+
exports[`site test Component components/collapse en Page 1`] = `2`;
exports[`site test Component components/collapse zh Page 1`] = `2`;
diff --git a/tests/__snapshots__/index.test.ts.snap b/tests/__snapshots__/index.test.ts.snap
index 0a4758fc7e28..e2d99a784e71 100644
--- a/tests/__snapshots__/index.test.ts.snap
+++ b/tests/__snapshots__/index.test.ts.snap
@@ -16,6 +16,7 @@ exports[`antd dist files exports modules correctly 1`] = `
"Card",
"Carousel",
"Cascader",
+ "Chatbox",
"Checkbox",
"Col",
"Collapse",