}) => afterUploadResult`
-afterUpload 在上传完成后(xhr.onload)且没有发生错误的情况下触发,需返回一个 Object 对象(不支持异步返回),具体结构如下
+`afterUpload` 在上传完成后(`xhr.onload`)且没有发生错误的情况下触发,需返回一个 Object 对象(不支持异步返回),具体结构如下
```ts
// afterUploadResult:
{
- status?: 'success' | 'uploadFail' | 'validateFail' | 'validating' | 'uploading' | 'wait',
- validateMessage?: React.ReactNode | string, // 文件的校验信息
- autoRemove: boolean, // 是否从fileList中移除该文件,默认为false
- name: string,
+ status?: 'success' | 'uploadFail' | 'validateFail' | 'validating' | 'uploading' | 'wait';
+ // 文件的校验信息
+ validateMessage?: React.ReactNode | string;
+ // 是否从fileList中移除该文件,默认为false
+ autoRemove?: boolean;
+ // 文件的名称
+ name?: string;
+ // 预览文件的url,一般为当次上传请求中 Server 接收到文件后返回的存储地址,v2.63后支持传入。
+ // 之前的版本也可以通过 onChange 中结合 status 手动更新受控 fileList 中的属性实现
+ url?: string
}
```
@@ -1203,14 +1210,8 @@ import React from 'react';
import { Upload, Button } from '@douyinfe/semi-ui';
import { IconUpload } from '@douyinfe/semi-icons';
-class ValidateDemo extends React.Component {
- constructor(props) {
- super(props);
- this.state = {};
- this.count = 0;
- }
-
- afterUpload({ response, file }) {
+() => {
+ const afterUpload = ({ response, file }) => {
// 可以根据业务接口返回,决定当次上传是否成功
if (response.status_code === 200) {
return {
@@ -1218,21 +1219,20 @@ class ValidateDemo extends React.Component {
status: 'uploadFail',
validateMessage: '内容不合法',
name: 'RenameByServer.jpg',
+ url: 'https://lf3-static.bytednsdoc.com/obj/eden-cn/ptlz_zlp/ljhwZthlaukjlkulzlp/root-web-sites/edit-bag.jpeg'
};
} else {
return {};
}
- }
+ };
- render() {
- return (
-
- } theme="light">
- 点击上传
-
-
- );
- }
+ return (
+
+ } theme="light">
+ 点击上传
+
+
+ )
}
```
diff --git a/content/order.js b/content/order.js
index be6b6a9efe..77de606c50 100644
--- a/content/order.js
+++ b/content/order.js
@@ -80,6 +80,7 @@ const order = [
'toast',
'configprovider',
'locale',
+ 'chat',
];
let { exec } = require('child_process');
let fs = require('fs');
diff --git a/content/plus/chat/index-en-US.md b/content/plus/chat/index-en-US.md
new file mode 100644
index 0000000000..a657b6b1f9
--- /dev/null
+++ b/content/plus/chat/index-en-US.md
@@ -0,0 +1,1612 @@
+---
+localeCode: en-US
+order: 82
+category: Plus
+title: Chat
+icon: doc-chat
+dir: column
+brief: Used to quickly build conversation content
+---
+
+## When to use
+
+The Chat component can be used in scenarios such as regular conversations or AI conversations.
+
+The rendering of the conversation content is based on the MarkdownRender component, which supports Markdown and MDX. It allows for common rich text features such as images, tables, links, bold text, code blocks, and more. More complex and customized document writing and display requirements can be achieved using JSX.
+
+## Demos
+
+### How to import
+
+Chat is supported starting from version v2.63.0.
+```jsx
+import { Chat } from '@douyinfe/semi-ui';
+```
+
+### Basic usage
+
+By setting `chats`, `onChatsChange`, and `onMessageSend`, you can achieve basic conversation display and interaction.
+
+Conversations involve multiple participants and multiple rounds of interaction. Role information, including names and avatars, can be passed through the `roleConfig` parameter. For detailed parameter information, refer to [RoleConfig](#RoleConfig).
+
+The prompt text of the upload button can be set through `uploadTipProps`. For details, please refer to [Tooltip](/zh-CN/tooltip#API%20%E5%8F%82%E8%80%83).
+
+Dialogue is a scene involving multiple parties and multiple rounds of interaction. Role information (including name, avatar, etc.) can be passed in through `roleConfig`, and the specific parameter details are [RoleConfig](#roleConfig).
+
+Use the `align` attribute to set the alignment of the dialog, supporting left and right alignment (`leftRight`, default) and left alignment (`leftAlign`).
+
+```jsx live=true noInline=true dir="column"
+import React, {useState, useCallback} from 'react';
+import { Chat, Radio } from '@douyinfe/semi-ui';
+
+const defaultMessage = [
+ {
+ role: 'system',
+ id: '1',
+ createAt: 1715676751919,
+ content: "Hello, I'm your AI assistant.",
+ },
+ {
+ role: 'user',
+ id: '2',
+ createAt: 1715676751919,
+ content: "Give an example of using Semi Design’s Button component",
+ },
+ {
+ role: 'assistant',
+ id: '3',
+ createAt: 1715676751919,
+ content: "The following is an example of using Semi code:\n\`\`\`jsx \nimport React from 'react';\nimport { Button } from '@douyinfe/semi-ui';\n\nconst MyComponent = () => {\n return (\n \n );\n};\nexport default MyComponent;\n\`\`\`\n",
+ }
+];
+
+const roleInfo = {
+ user: {
+ name: 'User',
+ avatar: 'https://lf3-static.bytednsdoc.com/obj/eden-cn/ptlz_zlp/ljhwZthlaukjlkulzlp/docs-icon.png'
+ },
+ assistant: {
+ name: 'Assistant',
+ avatar: 'https://lf3-static.bytednsdoc.com/obj/eden-cn/ptlz_zlp/ljhwZthlaukjlkulzlp/other/logo.png'
+ },
+ system: {
+ name: 'System',
+ avatar: 'https://lf3-static.bytednsdoc.com/obj/eden-cn/ptlz_zlp/ljhwZthlaukjlkulzlp/other/logo.png'
+ }
+}
+
+const commonOuterStyle = {
+ border: '1px solid var(--semi-color-border)',
+ borderRadius: '16px',
+ margin: '8px 16px',
+ height: 550,
+}
+
+let id = 0;
+function getId() {
+ return `id-${id++}`
+}
+
+const uploadProps = { action: 'https://api.semi.design/upload' }
+const uploadTipProps = { content: 'Customize upload button prompt information' }
+
+function DefaultChat() {
+ const [message, setMessage] = useState(defaultMessage);
+ const [mode, setMode] = useState('bubble');
+ const [align, setAlign] = useState('leftRight');
+
+ const onAlignChange = useCallback((e) => {
+ setAlign(e.target.value);
+ }, []);
+
+ const onModeChange = useCallback((e) => {
+ setMode(e.target.value);
+ }, []);
+
+ const onMessageSend = useCallback((content, attachment) => {
+ const newAssistantMessage = {
+ role: 'assistant',
+ id: getId(),
+ createAt: Date.now(),
+ content: "This is a mock response",
+ }
+ setTimeout(() => {
+ setMessage((message) => ([ ...message, newAssistantMessage]));
+ }, 200);
+ }, []);
+
+ const onChatsChange = useCallback((chats) => {
+ setMessage(chats);
+ }, []);
+
+ const onMessageReset = useCallback((e) => {
+ setTimeout(() => {
+ setMessage((message) => {
+ const lastMessage = message[message.length - 1];
+ const newLastMessage = {
+ ...lastMessage,
+ status: 'complete',
+ content: 'This is a mock reset message.',
+ }
+ return [...message.slice(0, -1), newLastMessage]
+ })
+ }, 200);
+ })
+
+ return (
+ <>
+
+
+ Mode
+
+ bubble
+ noBubble
+ userBubble
+
+
+
+ Chat align
+
+ leftRight
+ leftAlign
+
+
+
+
+ >
+ )
+}
+
+render(DefaultChat);
+```
+
+### Chat status
+
+The chats type is `Message[]`, where each `Message` contains various information about the conversation, such as role, content, attachment, status, unique identifier (id), creation time (createAt), and more. For detailed information, please refer to [Message](#message). The conversation style may vary depending on the different status values.
+
+``` jsx live=true noInline=true dir="column"
+import React, {useState, useCallback} from 'react';
+import { Chat } from '@douyinfe/semi-ui';
+
+const defaultMessage = [
+ {
+ role: 'assistant',
+ id: '1',
+ createAt: 1715676751919,
+ content: "Success response",
+ },
+ {
+ id: 'loading',
+ role: 'assistant',
+ status: 'loading'
+ },
+ {
+ role: 'assistant',
+ id: 'error',
+ content: 'Error response',
+ status: 'error'
+ }
+];
+
+const roleInfo = {
+ user: {
+ name: 'User',
+ avatar: 'https://lf3-static.bytednsdoc.com/obj/eden-cn/ptlz_zlp/ljhwZthlaukjlkulzlp/docs-icon.png'
+ },
+ assistant: {
+ name: 'Assistant',
+ avatar: 'https://lf3-static.bytednsdoc.com/obj/eden-cn/ptlz_zlp/ljhwZthlaukjlkulzlp/other/logo.png'
+ },
+ system: {
+ name: 'System',
+ avatar: 'https://lf3-static.bytednsdoc.com/obj/eden-cn/ptlz_zlp/ljhwZthlaukjlkulzlp/other/logo.png'
+ }
+}
+
+const commonOuterStyle = {
+ border: '1px solid var(--semi-color-border)',
+ borderRadius: '16px',
+ height: 400,
+}
+
+let id = 0;
+function getId() { return `id-${id++}` }
+const uploadProps = { action: 'https://api.semi.design/upload' }
+
+function MessageStatus() {
+ const [message, setMessage] = useState(defaultMessage);
+
+ const onMessageSend = useCallback((content, attachment) => {
+ const newAssistantMessage = {
+ role: 'assistant',
+ id: getId(),
+ createAt: Date.now(),
+ content: "This is a mock response",
+ }
+ setTimeout(() => {
+ setMessage((message) => ([ ...message, newAssistantMessage]));
+ }, 200);
+ }, []);
+
+ const onChatsChange = useCallback((chats) => {
+ setMessage(chats);
+ }, []);
+
+ return (
+
+ )
+}
+
+render(MessageStatus);
+```
+
+### Dynamic update chats
+
+For the case of receiving Server-Sent Event data from the backend, the obtained data can be used to update the `chats`, and the conversation content will be updated in real-time.
+
+The `showStopGenerate` parameter can be used to determine whether to display the stop generation button, with a default value of `false`. The logic for stopping the generation can be handled in the `onStopGenerator` function.
+
+```jsx live=true noInline=true dir="column"
+import React, {useState, useCallback} from 'react';
+import { Chat } from '@douyinfe/semi-ui';
+
+const defaultMessage = [
+ {
+ role: 'system',
+ id: '1',
+ createAt: 1715676751919,
+ content: "Hello, I'm your AI assistant.",
+ },
+ {
+ role: 'user',
+ id: '2',
+ createAt: 1715676751919,
+ content: "介绍一下 Semi design"
+ },
+ {
+ role: 'assistant',
+ id: '3',
+ createAt: 1715676751919,
+ content: `
+Semi Design is a design system designed, developed and maintained by Douyin's front-end team and MED product design team. As a comprehensive, easy-to-use, high-quality modern application UI solution, Semi Design is extracted from the complex scenarios of ByteDance's various business lines. It has currently supported nearly a thousand platform products and served more than 100,000 internal and external users.[[1]](https://semi.design/zh-CN/start/introduction)。
+
+Semi Design features include:
+
+1. Simple and modern design.
+2. Provide theme solutions, which can be customized in depth.
+3. Provide two sets of light and dark color modes, easy to switch.
+4. Internationalization, covering 20+ languages such as Simplified/Traditional Chinese, English, Japanese, Korean, Portuguese, etc. The date and time component provides global time zone support, and all components can automatically adapt to the Arabic RTL layout.
+5. Use Foundation and Adapter cross-framework technical solutions to facilitate expansion.
+
+---
+Learn more:
+1. [Introduction - Semi Design](https://semi.design/zh-CN/start/introduction)
+2. [Getting Started - Semi Design](https://semi.design/zh-CN/start/getting-started)
+3. [The evolution of Semi D2C design draft to code - Zhihu](https://zhuanlan.zhihu.com/p/667189184)
+`,
+ }
+];
+
+const roleInfo = {
+ user: {
+ name: 'User',
+ avatar: 'https://lf3-static.bytednsdoc.com/obj/eden-cn/ptlz_zlp/ljhwZthlaukjlkulzlp/docs-icon.png'
+ },
+ assistant: {
+ name: 'Assistant',
+ avatar: 'https://lf3-static.bytednsdoc.com/obj/eden-cn/ptlz_zlp/ljhwZthlaukjlkulzlp/other/logo.png'
+ },
+ system: {
+ name: 'System',
+ avatar: 'https://lf3-static.bytednsdoc.com/obj/eden-cn/ptlz_zlp/ljhwZthlaukjlkulzlp/other/logo.png'
+ }
+}
+
+const commonOuterStyle = {
+ border: '1px solid var(--semi-color-border)',
+ borderRadius: '16px',
+ height: 600,
+}
+
+let id = 0;
+function getId() {
+ return `id-${id++}`
+}
+const uploadProps = { action: 'https://api.semi.design/upload' }
+
+function DynamicUpdateChat() {
+ const [message, setMessage] = useState(defaultMessage);
+ const intervalId = useRef();
+ const onMessageSend = useCallback((content, attachment) => {
+ setMessage((message) => {
+ return [
+ ...message,
+ {
+ role: 'assistant',
+ status: 'loading',
+ createAt: Date.now(),
+ id: getId()
+ }
+ ]
+ });
+ generateMockResponse(content);
+ }, []);
+
+ const onChatsChange = useCallback((chats) => {
+ setMessage(chats);
+ }, []);
+
+ const generateMockResponse = useCallback((content) => {
+ const id = setInterval(() => {
+ setMessage((message) => {
+ const lastMessage = message[message.length - 1];
+ let newMessage = {...lastMessage};
+ if (lastMessage.status === 'loading') {
+ newMessage = {
+ ...newMessage,
+ content: `mock Response for ${content} \n`,
+ status: 'incomplete'
+ }
+ } else if (lastMessage.status === 'incomplete') {
+ if (lastMessage.content.length > 200) {
+ clearInterval(id);
+ intervalId.current = null
+ newMessage = {
+ ...newMessage,
+ content: `${lastMessage.content} mock stream message`,
+ status: 'complete'
+ }
+ } else {
+ newMessage = {
+ ...newMessage,
+ content: `${lastMessage.content} mock stream message`
+ }
+ }
+ }
+ return [ ...message.slice(0, -1), newMessage ]
+ })
+ }, 400);
+ intervalId.current = id;
+ }, []);
+
+ const onStopGenerator = useCallback(() => {
+ if (intervalId.current) {
+ clearInterval(intervalId.current);
+ setMessage((message) => {
+ const lastMessage = message[message.length - 1];
+ if (lastMessage.status && lastMessage.status !== 'complete') {
+ const lastMessage = message[message.length - 1];
+ let newMessage = {...lastMessage};
+ newMessage.status = 'complete';
+ return [
+ ...message.slice(0, -1),
+ newMessage
+ ]
+ } else {
+ return message;
+ }
+ })
+ }
+ }, [intervalId]);
+
+ return (
+
+ )
+}
+
+render(DynamicUpdateChat);
+```
+
+### Clear context
+
+Displaying the clear context button in the input box can be enabled through `showClearContext`, which defaults to `false`.
+The context can also be cleared by calling the `clearContext` method through ref.
+
+```jsx live=true noInline=true dir="column"
+import React, {useState, useCallback} from 'react';
+import { Chat, Radio } from '@douyinfe/semi-ui';
+
+const defaultMessage = [
+ {
+ role: 'system',
+ id: '1',
+ createAt: 1715676751919,
+ content: "Hello, I'm your AI assistant.",
+ },
+ {
+ role: 'user',
+ id: '2',
+ createAt: 1715676751919,
+ content: "Introduce semi design",
+ },
+ {
+ role: 'assistant',
+ id: '3',
+ createAt: 1715676751919,
+ content: 'Semi Design is a design system designed, developed and maintained by the Douyin front-end team and MED product design team.',
+ }
+];
+
+const roleInfo = {
+ user: {
+ name: 'User',
+ avatar: 'https://lf3-static.bytednsdoc.com/obj/eden-cn/ptlz_zlp/ljhwZthlaukjlkulzlp/docs-icon.png'
+ },
+ assistant: {
+ name: 'Assistant',
+ avatar: 'https://lf3-static.bytednsdoc.com/obj/eden-cn/ptlz_zlp/ljhwZthlaukjlkulzlp/other/logo.png'
+ },
+ system: {
+ name: 'System',
+ avatar: 'https://lf3-static.bytednsdoc.com/obj/eden-cn/ptlz_zlp/ljhwZthlaukjlkulzlp/other/logo.png'
+ }
+}
+
+const commonOuterStyle = {
+ border: '1px solid var(--semi-color-border)',
+ borderRadius: '16px',
+ margin: '8px 16px',
+ height: 550,
+}
+
+let id = 0;
+function getId() {
+ return `id-${id++}`
+}
+
+const uploadProps = { action: 'https://api.semi.design/upload' }
+const uploadTipProps = { content: 'Customize upload button prompt information' }
+
+function DefaultChat() {
+ const [message, setMessage] = useState(defaultMessage);
+
+ const onMessageSend = useCallback((content, attachment) => {
+ const newAssistantMessage = {
+ role: 'assistant',
+ id: getId(),
+ createAt: Date.now(),
+ content: "This is a mock response message.",
+ }
+ setTimeout(() => {
+ setMessage((message) => ([ ...message, newAssistantMessage]));
+ }, 200);
+ }, []);
+
+ const onChatsChange = useCallback((chats) => {
+ setMessage(chats);
+ }, []);
+
+ const onMessageReset = useCallback((e) => {
+ setTimeout(() => {
+ setMessage((message) => {
+ const lastMessage = message[message.length - 1];
+ const newLastMessage = {
+ ...lastMessage,
+ status: 'complete',
+ content: 'This is a mock reset message.',
+ }
+ return [...message.slice(0, -1), newLastMessage]
+ })
+ }, 200);
+ })
+
+ return (
+ <>
+
+ >
+ )
+}
+
+render(DefaultChat);
+```
+
+### Custom rendering dialog box
+
+Pass in custom rendering configuration through `chatBoxRenderConfig`, the chatBoxRenderConfig type is as follows
+
+```ts
+interface ChatBoxRenderConfig {
+ /* Custom rendering title */
+ renderChatBoxTitle?: (props: {role?: Metadata, defaultTitle?: ReactNode}) => ReactNode;
+ /* Custom rendering avatr */
+ renderChatBoxAvatar?: (props: { role?: Metadata, defaultAvatar?: ReactNode}) => ReactNode;
+ /* Custom rendering content */
+ renderChatBoxContent?: (props: {message?: Message, role?: Metadata, defaultContent?: ReactNode | ReactNode[], className?: string}) => ReactNode;
+ /* Custom rendering message action bar */
+ renderChatBoxAction?: (props: {message?: Message, defaultActions?: ReactNode | ReactNode[], className: string}) => ReactNode;
+ /* Fully customized rendering of the entire chat box */
+ renderFullChatBox?: (props: {message?: Message, role?: Metadata, defaultNodes?: FullChatBoxNodes, className: string}) => ReactNode;
+}
+```
+
+Custom render avatar and Title through `renderChatBoxAvatar` and `renderChatBoxTitle`。
+
+```jsx live=true noInline=true dir="column"
+
+import React, {useState, useCallback} from 'react';
+import { Chat, Avatar, Tag } from '@douyinfe/semi-ui';
+
+const defaultMessage = [
+ {
+ role: 'system',
+ id: '1',
+ createAt: 1715676751919,
+ content: "Hello, I'm your AI assistant.",
+ },
+ {
+ role: 'user',
+ id: '2',
+ createAt: 1715676751919,
+ content: [
+ {
+ type: 'text',
+ text: 'What\'s in this picture?'
+ },
+ {
+ type: 'image_url',
+ image_url: {
+ url: 'https://lf3-static.bytednsdoc.com/obj/eden-cn/ptlz_zlp/ljhwZthlaukjlkulzlp/root-web-sites/edit-bag.jpeg'
+ }
+ }
+ ],
+ },
+ {
+ role: 'assistant',
+ id: '3',
+ createAt: 1715676751919,
+ content: 'The picture shows a yellow backpack decorated with cartoon images'
+ },
+
+];
+
+const roleInfo = {
+ user: {
+ name: 'User',
+ avatar: 'https://lf3-static.bytednsdoc.com/obj/eden-cn/ptlz_zlp/ljhwZthlaukjlkulzlp/docs-icon.png'
+ },
+ assistant: {
+ name: 'Assistant',
+ avatar: 'https://lf3-static.bytednsdoc.com/obj/eden-cn/ptlz_zlp/ljhwZthlaukjlkulzlp/other/logo.png'
+ },
+ system: {
+ name: 'System',
+ avatar: 'https://lf3-static.bytednsdoc.com/obj/eden-cn/ptlz_zlp/ljhwZthlaukjlkulzlp/other/logo.png'
+ }
+}
+
+const commonOuterStyle = {
+ border: '1px solid var(--semi-color-border)',
+ borderRadius: '16px',
+ height: 400,
+}
+
+let id = 0;
+function getId() { return `id-${id++}`; }
+const uploadProps = { action: 'https://api.semi.design/upload' }
+
+function CustomRender() {
+ const [title, setTitle] = useState('null');
+ const [avatar, setAvatar] = useState('null');
+ const [message, setMessage] = useState(defaultMessage);
+
+ const onChatsChange = useCallback((chats) => {
+ setMessage(chats);
+ }, []);
+
+ const customRenderAvatar = useMemo(()=> {
+ switch(avatar) {
+ case 'custom': return (props) => {
+ const { role, defaultAvatar } = props;
+ return {role.name}
+ }
+ case 'null': return () => null
+ case 'default': return undefined;
+ }
+ }, [avatar]);
+
+ const customRenderTitle = useMemo(()=> {
+ switch(title) {
+ case 'custom': return (props) => {
+ const { role, defaultTitle, message } = props;
+ const date = new Date(message.createAt);
+ const hours = ('0' + date.getHours()).slice(-2);
+ const minutes = ('0' + date.getMinutes()).slice(-2);
+ const formatTime = `${hours}:${minutes}`;
+ return (
+ {role.name}
+ {formatTime}
+ )
+ }
+ case 'null': return () => null
+ case 'default': return undefined;
+ }
+ }, [title]);;
+
+ const onAvatarChange = useCallback((e) => { setAvatar(e.target.value) }, []);
+ const onTitleChange = useCallback((e) => { setTitle(e.target.value) }, []);
+
+ const onMessageSend = useCallback((content, attachment) => {
+ const newAssistantMessage = {
+ role: 'assistant',
+ id: getId(),
+ content: `This is a mock response`
+ }
+ setTimeout(() => {
+ setMessage((message) => ([ ...message, newAssistantMessage]));
+ }, 200);
+ }, []);
+
+ return (
+ <>
+
+
+ Avatar Render Mode
+
+ default
+ null
+ custom
+
+
+
+ Title Render mode
+
+ default
+ null
+ custom
+
+
+
+
+ >
+ );
+}
+
+render(CustomRender);
+```
+
+When hovering over a conversation, the conversation action area will be displayed. You can customize the rendering of the action area using `renderChatBoxAction`.
+
+```jsx live=true noInline=true dir="column"
+import React, {useState, useCallback} from 'react';
+import { Chat, Dropdown } from '@douyinfe/semi-ui';
+import { IconForward } from '@douyinfe/semi-icons';
+
+const defaultMessage = [
+ {
+ role: 'system',
+ id: '1',
+ createAt: 1715676751919,
+ content: "Hello, I'm your AI assistant.",
+ },
+ {
+ role: 'user',
+ id: '2',
+ createAt: 1715676751919,
+ content: "Introduce Semi design",
+ },
+ {
+ role: 'assistant',
+ id: '3',
+ createAt: 1715676751919,
+ content: 'Semi Design is a design system designed, developed, and maintained by the front-end team at Douyin and the MED product design team.',
+ }
+];
+
+const roleInfo = {
+ user: {
+ name: 'User',
+ avatar: 'https://lf3-static.bytednsdoc.com/obj/eden-cn/ptlz_zlp/ljhwZthlaukjlkulzlp/docs-icon.png'
+ },
+ assistant: {
+ name: 'Assistant',
+ avatar: 'https://lf3-static.bytednsdoc.com/obj/eden-cn/ptlz_zlp/ljhwZthlaukjlkulzlp/other/logo.png'
+ },
+ system: {
+ name: 'System',
+ avatar: 'https://lf3-static.bytednsdoc.com/obj/eden-cn/ptlz_zlp/ljhwZthlaukjlkulzlp/other/logo.png'
+ }
+}
+
+const commonOuterStyle = {
+ border: '1px solid var(--semi-color-border)',
+ borderRadius: '16px',
+ height: 400,
+}
+
+let id = 0;
+function getId() { return `id-${id++}`; }
+const uploadProps = { action: 'https://api.semi.design/upload' }
+
+const CustomActions = React.memo((props) => {
+ const { role, message, defaultActions, className } = props;
+ const myRef = useRef();
+ const getContainer = useCallback(() => {
+ if (myRef.current) {
+ const element = myRef.current;
+ let parentElement = element.parentElement;
+ while (parentElement) {
+ if (parentElement.classList.contains('semi-chat-chatBox-wrap')) {
+ return parentElement;
+ }
+ parentElement = parentElement.parentElement;
+ }
+ }
+ }, [myRef]);
+
+ return
+ {defaultActions}
+ {
+ }>Share
+
+ }
+ trigger="click"
+ position="top"
+ getPopupContainer={getContainer}
+ >
+ }
+
+});
+
+function CustomRender() {
+ const [message, setMessage] = useState(defaultMessage);
+
+ const customRenderAction = useCallback((props) => {
+ return
+ }, []);
+
+ const onChatsChange = useCallback((chats) => {
+ setMessage(chats);
+ }, []);
+
+ const onMessageSend = useCallback((content, attachment) => {
+ const newAssistantMessage = {
+ role: 'assistant',
+ id: getId(),
+ content: `This is a mock response`
+ }
+ setTimeout(() => {
+ setMessage((message) => ([ ...message, newAssistantMessage]));
+ }, 200);
+ }, []);
+
+ return (
+
+ );
+}
+
+render(CustomRender);
+```
+
+You can customize the content of the action area using `renderChatBoxContent`.
+
+```jsx live=true noInline=true dir="column"
+import React, { useState, useCallback, useRef} from 'react';
+import { Chat, MarkdownRender } from '@douyinfe/semi-ui';
+
+const defaultMessage = [
+ {
+ role: 'assistant',
+ id: '1',
+ createAt: 1715676751919,
+ content: "Semi Design is a design system designed, developed and maintained by Douyin's front-end team and MED product design team. As a comprehensive, easy-to-use, high-quality modern application UI solution, it is extracted from the complex scenarios of ByteDance's various business lines, supports nearly a thousand platform products, and serves 100,000+ internal and external users.",
+ source: [
+ {
+ avatar: 'https://lf3-static.bytednsdoc.com/obj/eden-cn/ptlz_zlp/ljhwZthlaukjlkulzlp/other/logo.png',
+ url: '/en-US/start/introduction',
+ title: 'semi Design',
+ subTitle: 'Semi design website',
+ content: 'Semi Design is a design system designed, developed and maintained by Douyin\'s front-end team and MED product design team.'
+ },
+ {
+ avatar: 'https://lf3-static.bytednsdoc.com/obj/eden-cn/ptlz_zlp/ljhwZthlaukjlkulzlp/other/logo.png',
+ url: '/dsm/landing',
+ subTitle: 'Semi DSM website',
+ title: 'Semi Design System',
+ content: 'From Semi Design to Any Design, quickly define your design system and apply it in design drafts and code'
+ },
+ {
+ avatar: 'https://lf3-static.bytednsdoc.com/obj/eden-cn/ptlz_zlp/ljhwZthlaukjlkulzlp/other/logo.png',
+ url: '/code/en-US/start/introduction',
+ subTitle: 'Semi D2C website',
+ title: 'Design to Code',
+ content: 'Semi Design to Code, or Semi D2C for short, is a new performance improvement tool launched by the Douyin front-end Semi Design team.'
+ },
+ ]
+ }];
+
+const roleInfo = {
+ user: {
+ name: 'User',
+ avatar: 'https://lf3-static.bytednsdoc.com/obj/eden-cn/ptlz_zlp/ljhwZthlaukjlkulzlp/docs-icon.png'
+ },
+ assistant: {
+ name: 'Assistant',
+ avatar: 'https://lf3-static.bytednsdoc.com/obj/eden-cn/ptlz_zlp/ljhwZthlaukjlkulzlp/other/logo.png'
+ },
+ system: {
+ name: 'System',
+ avatar: 'https://lf3-static.bytednsdoc.com/obj/eden-cn/ptlz_zlp/ljhwZthlaukjlkulzlp/other/logo.png'
+ }
+}
+
+const commonOuterStyle = {
+ border: '1px solid var(--semi-color-border)',
+ borderRadius: '16px',
+ height: 500,
+}
+
+let id = 0;
+function getId() { return `id-${id++}` }
+const uploadProps = { action: 'https://api.semi.design/upload' }
+
+const SourceCard = (props) => {
+ const [open, setOpen] = useState(true);
+ const [show, setShow] = useState(false);
+ const { source } = props;
+ const spanRef = useRef();
+ const onOpen = useCallback(() => {
+ setOpen(false);
+ setShow(true);
+ }, []);
+
+ const onClose = useCallback(() => {
+ setOpen(true);
+ setTimeout(() => {
+ setShow(false);
+ }, 350)
+ }, []);
+
+ return (
+
+ Got {source.length} sources
+
+ {source.map((s, index) => ())}
+
+
+
+
+ Source
+
+
+
+ {source.map(s => (
+
+
+
+ {s.title}
+
+ {s.subTitle}
+ {s.content}
+ ))}
+
+
+
+ )
+}
+
+function CustomRender() {
+ const [message, setMessage] = useState(defaultMessage);
+
+ const onChatsChange = useCallback((chats) => {
+ setMessage(chats);
+ }, []);
+
+ const onMessageSend = useCallback((content, attachment) => {
+ const newAssistantMessage = {
+ role: 'assistant',
+ id: getId(),
+ content: `This is a mock response`
+ }
+ setTimeout(() => {
+ setMessage((message) => ([ ...message, newAssistantMessage]));
+ }, 200);
+ }, []);
+
+ const renderContent = useCallback((props) => {
+ const { role, message, defaultNode, className } = props;
+ return
+ {message.source && }
+
+
+ }, []);
+
+ return (
+
+ );
+}
+
+render(CustomRender);
+```
+
+Use `renderFullChatBox` to custom render the entire chat box
+
+```jsx live=true noInline=true dir="column"
+import React, {useState, useCallback} from 'react';
+import { Chat, Avatar } from '@douyinfe/semi-ui';
+
+const defaultMessage = [
+ {
+ role: 'system',
+ id: '1',
+ createAt: 1715676751919,
+ content: "Hello, I'm your AI assistant.",
+ },
+ {
+ role: 'user',
+ id: '2',
+ createAt: 1715676751919,
+ content: "Introduce Semi",
+ },
+ {
+ role: 'assistant',
+ id: '3',
+ createAt: 1715676751919,
+ content: 'Semi Design is a design system designed, developed, and maintained by the front-end team at Douyin and the MED product design team.',
+ }
+];
+
+const roleInfo = {
+ user: {
+ name: 'User',
+ avatar: 'https://lf3-static.bytednsdoc.com/obj/eden-cn/ptlz_zlp/ljhwZthlaukjlkulzlp/docs-icon.png'
+ },
+ assistant: {
+ name: 'Assistant',
+ avatar: 'https://lf3-static.bytednsdoc.com/obj/eden-cn/ptlz_zlp/ljhwZthlaukjlkulzlp/other/logo.png'
+ },
+ system: {
+ name: 'System',
+ avatar: 'https://lf3-static.bytednsdoc.com/obj/eden-cn/ptlz_zlp/ljhwZthlaukjlkulzlp/other/logo.png'
+ }
+}
+
+const commonOuterStyle = {
+ border: '1px solid var(--semi-color-border)',
+ borderRadius: '16px',
+ height: 400,
+}
+
+let id = 0;
+function getId() { return `id-${id++}`; }
+const uploadProps = { action: 'https://api.semi.design/upload' }
+
+const titleStyle = { display:' flex', alignItems: 'center', justifyContent: 'center', columnGap: '10px', padding: '5px 0px', width: 'fit-content' };
+
+function CustomFullRender() {
+ const [message, setMessage] = useState(defaultMessage);
+
+ const customRenderChatBox = useCallback((props) => {
+ const { role, message, defaultNodes, className } = props;
+ let titleNode = null;
+ if (message.role !== 'user') {
+ titleNode = (
+
+ {defaultNodes.title}
+ )
+ }
+ return
+
+ {titleNode}
+
+ {defaultNodes.content}
+
+ {defaultNodes.action}
+
+
+ }, []);
+
+ const onChatsChange = useCallback((chats) => {
+ setMessage(chats)
+ } ,[]);
+
+ const onMessageSend = useCallback((content, attachment) => {
+ const newAssistantMessage = {
+ role: 'assistant',
+ id: getId(),
+ content: `This is a mock response`
+ }
+ setTimeout(() => {
+ setMessage((message) => ([ ...message, newAssistantMessage]));
+ }, 200);
+ }, []);
+
+ return ( );
+}
+
+render(CustomFullRender)
+```
+
+### Custom render InputArea
+
+The rendering input box can be customized through `renderInputArea`, the parameters are as follows
+
+``` ts
+export interface RenderInputAreaProps {
+ /* Default node */
+ defaultNode?: ReactNode;
+ /* If you customize the input box, you need to call it when sending a message. */
+ onSend?: (content?: string, attachment?: FileItem[]) => void;
+ /* If you customize the clear context button, it needs to be called when you click to clear the context */
+ onClear?: (e?: any) => void;
+}
+```
+
+Example:
+
+```jsx live=true noInline=true dir="column"
+import React, {useState, useCallback} from 'react';
+import { Form, Chat } from '@douyinfe/semi-ui';
+
+const defaultMessage = [
+ {
+ role: 'system',
+ id: '1',
+ createAt: 1715676751919,
+ content: "Hello, I'm your AI assistant.",
+ },
+];
+
+const roleInfo = {
+ user: {
+ name: 'User',
+ avatar: 'https://lf3-static.bytednsdoc.com/obj/eden-cn/ptlz_zlp/ljhwZthlaukjlkulzlp/docs-icon.png'
+ },
+ assistant: {
+ name: 'Assistant',
+ avatar: 'https://lf3-static.bytednsdoc.com/obj/eden-cn/ptlz_zlp/ljhwZthlaukjlkulzlp/other/logo.png'
+ },
+ system: {
+ name: 'System',
+ avatar: 'https://lf3-static.bytednsdoc.com/obj/eden-cn/ptlz_zlp/ljhwZthlaukjlkulzlp/other/logo.png'
+ }
+}
+
+const commonOuterStyle = {
+ border: '1px solid var(--semi-color-border)',
+ borderRadius: '16px',
+ height: 500,
+};
+
+let id = 0;
+function getId() {
+ return `id-${id++}`
+}
+const uploadProps = { action: 'https://api.semi.design/upload' }
+
+const inputStyle = {
+ display: 'flex',
+ flexDirection: 'column',
+ border: '1px solid var(--semi-color-border)',
+ margin: '8px 16px',
+ borderRadius: 8,
+ padding: 8
+}
+
+function CustomInputRender(props) {
+ const { defaultNode, onClear, onSend } = props;
+ const api = useRef();
+ const onSubmit = useCallback(() => {
+ if (api.current) {
+ const values = api.current.getValues();
+ if ((values.name && values.name.length !== 0) || (values.file && values.file.length !== 0)) {
+ onSend(values.name, values.file);
+ api.current.reset();
+ }
+ }
+ }, []);
+
+ return (
+
+
+ } theme="light">
+ Upload
+
+
+
+
+ );
+}
+
+function CustomRenderInputArea() {
+ const [message, setMessage] = useState(defaultMessage);
+
+ const onChatsChange = useCallback((chats) => {
+ setMessage(chats);
+ }, []);
+
+ const onMessageSend = useCallback((content, attachment) => {
+ const newAssistantMessage = {
+ role: 'assistant',
+ id: getId(),
+ content: `This is a mock response`
+ }
+ setTimeout(() => {
+ setMessage((message) => ([ ...message, newAssistantMessage]));
+ }, 200);
+ }, []);
+
+ const renderInputArea = useCallback((props) => {
+ return ()
+ }, []);
+
+ return (
+
+ )
+}
+render(CustomRenderInputArea);
+```
+
+### Hint
+
+The prompt area content can be set through `hints`. After clicking the prompt content, the prompt content will become the new user input content and trigger the `onHintClick` callback.
+
+```jsx live=true noInline=true dir="column"
+import React, {useState, useCallback} from 'react';
+import { Chat } from '@douyinfe/semi-ui';
+
+const defaultMessage = [
+ {
+ role: 'assistant',
+ id: '1',
+ createAt: 1715676751919,
+ content: 'Semi Design is a design system designed, developed, and maintained by the front-end team at Douyin and the MED product design team.',
+ }
+];
+
+const hintsExample = [
+ "Tell me more",
+ "What are the components of Semi Design?",
+ "What are the addresses of Semi Design’s official website and github warehouse?",
+]
+
+const roleInfo = {
+ user: {
+ name: 'User',
+ avatar: 'https://lf3-static.bytednsdoc.com/obj/eden-cn/ptlz_zlp/ljhwZthlaukjlkulzlp/docs-icon.png'
+ },
+ assistant: {
+ name: 'Assistant',
+ avatar: 'https://lf3-static.bytednsdoc.com/obj/eden-cn/ptlz_zlp/ljhwZthlaukjlkulzlp/other/logo.png'
+ },
+ system: {
+ name: 'System',
+ avatar: 'https://lf3-static.bytednsdoc.com/obj/eden-cn/ptlz_zlp/ljhwZthlaukjlkulzlp/other/logo.png'
+ }
+}
+
+const commonOuterStyle = {
+ border: '1px solid var(--semi-color-border)',
+ borderRadius: '16px',
+ height: 400,
+};
+
+let id = 0;
+function getId() {
+ return `id-${id++}`
+}
+const uploadProps = { action: 'https://api.semi.design/upload' }
+
+function DefaultChat() {
+ const [message, setMessage] = useState(defaultMessage);
+ const [hints, setHints] = useState(hintsExample);
+
+ const onHintClick = useCallback(() => {
+ setHints([]);
+ }, [])
+
+ const onMessageSend = useCallback((content, attachment) => {
+ const newAssistantMessage = {
+ role: 'assistant',
+ id: getId(),
+ createAt: Date.now(),
+ content: "This is a mock response",
+ }
+ setTimeout(() => {
+ setMessage((message) => ([ ...message, newAssistantMessage]));
+ }, 200);
+ }, []);
+
+ const onChatsChange = useCallback((chats) => {
+ setMessage(chats);
+ }, []);
+
+ onClear = useCallback(() => {
+ setHints([]);
+ }, [])
+
+ return (
+
+ )
+}
+
+render(DefaultChat);
+```
+
+### Custom render Hint
+
+Customize the content of the prompt area through `renderHintBox`, the parameters are as follows
+
+```ts
+type renderHintBox = (props: {content: string; index: number,onHintClick: () => void}) => React.ReactNode;
+```
+
+Example:
+
+```jsx live=true noInline=true dir="column"
+import React, {useState, useCallback} from 'react';
+import { Chat } from '@douyinfe/semi-ui';
+
+const defaultMessage = [
+ {
+ role: 'assistant',
+ id: '1',
+ createAt: 1715676751919,
+ content: 'Semi Design is a design system designed, developed, and maintained by the front-end team at Douyin and the MED product design team.',
+ }
+];
+
+const hintsExample = [
+ "Tell me more",
+ "What are the components of Semi Design?",
+ "What are the addresses of Semi Design’s official website and github warehouse?",
+]
+
+const roleInfo = {
+ user: {
+ name: 'User',
+ avatar: 'https://lf3-static.bytednsdoc.com/obj/eden-cn/ptlz_zlp/ljhwZthlaukjlkulzlp/docs-icon.png'
+ },
+ assistant: {
+ name: 'Assistant',
+ avatar: 'https://lf3-static.bytednsdoc.com/obj/eden-cn/ptlz_zlp/ljhwZthlaukjlkulzlp/other/logo.png'
+ },
+ system: {
+ name: 'System',
+ avatar: 'https://lf3-static.bytednsdoc.com/obj/eden-cn/ptlz_zlp/ljhwZthlaukjlkulzlp/other/logo.png'
+ }
+}
+
+const commonOuterStyle = {
+ border: '1px solid var(--semi-color-border)',
+ borderRadius: '16px',
+ height: 400,
+};
+
+let id = 0;
+function getId() {
+ return `id-${id++}`
+}
+const uploadProps = { action: 'https://api.semi.design/upload' }
+
+function DefaultChat() {
+ const [message, setMessage] = useState(defaultMessage);
+ const [hints, setHints] = useState(hintsExample);
+
+ const onHintClick = useCallback(() => {
+ setHints([]);
+ }, [])
+
+ const onMessageSend = useCallback((content, attachment) => {
+ const newAssistantMessage = {
+ role: 'assistant',
+ id: getId(),
+ createAt: Date.now(),
+ content: "This is a mock reply message",
+ }
+ setTimeout(() => {
+ setMessage((message) => ([ ...message, newAssistantMessage]));
+ }, 200);
+ setHints([]);
+ }, []);
+
+ const onChatsChange = useCallback((chats) => {
+ setMessage(chats);
+ }, []);
+
+ const commonHintStyle = useMemo(() => ({
+ border: '1px solid var(--semi-color-border)',
+ padding: '10px',
+ borderRadius: '10px',
+ color: 'var( --semi-color-text-1)',
+ display: 'flex',
+ justifyContent: 'space-between',
+ alignItems: 'center',
+ cursor: 'pointer',
+ fontSize: '14px'
+ }), []);
+
+ const renderHintBox = useCallback((props) => {
+ const { content, onHintClick, index } = props;
+ return
+ {content}
+ click me
+
+ }, []);
+
+ onClear = useCallback(() => {
+ setHints([]);
+ }, [])
+
+ return (
+
+ )
+}
+
+render(DefaultChat);
+```
+
+### API
+
+| PROPERTIES | INSTRUCTIONS | TYPE | DEFAULT |
+|------|--------|-------|-------|
+| align | Dialog alignment, supports `leftRight`,`leftAlign` | string | `leftRight` |
+| bottomSlot | bottom slot for chat | React.ReactNode | - |
+| chatBoxRenderConfig | chatBox rendering configuration | ChatBoxRenderConfig | - |
+| chats | Controlled conversation list | Message | - |
+| className | Custom class name | string | - |
+| customMarkDownComponents | custom markdown render, transparently passed to MarkdownRender for conversation content rendering | MDXProps\['components'\]| - |
+| hints | prompt information | string | - |
+| hintCls | hint style | string | - |
+| hintStyle | hint style | CSSProperties | - |
+| inputBoxStyle | Input box style | CSSProperties | - |
+| inputBoxCls | Input box className | string | - |
+| sendHotKey | Keyboard shortcut for sending content, supports `enter` \| `shift+enter`. The former will send the message in the input box when you press enter alone. When the shift and enter keys are pressed at the same time, it will only wrap the line and not send it. The latter is the opposite | string | `enter` |
+| mode | Conversation mode, support `bubble` \| `noBubble` \| `userBubble` | string | `bubble` |
+| roleConfig | Role information configuration, see[RoleConfig](#RoleConfig) | RoleConfig | - |
+| renderHintBox | Custom rendering prompt information | (props: {content: string; index: number,onHintClick: () => void}) => React.ReactNode| - |
+| onChatsChange | Triggered when the conversation list changes | (chats: Message[]) => void | - |
+| onClear | Triggered when context message is cleared | () => void | - |
+| onHintClick | Triggered when the prompt message is clicked | (hint: string) => void | - |
+| onInputChange | Triggered when input area information changes | (props: { value?: string, attachment?: FileItem[] }) => void; | - |
+| onMessageBadFeedback | Triggered when the message is negatively fed back | (message: Message) => void | - |
+| onMessageCopy | Triggered when copying a message | (message: Message) => void | - |
+| onMessageDelete | Triggered when a message is deleted | (message: Message) => void | - |
+| onMessageGoodFeedback | Triggered when the message is fed back positively | (message: Message) => void | - |
+| onMessageReset | Triggered when message is reset | (message: Message) => void | - |
+| onMessageSend | Triggered when sending a message | (content: string, attachment?: FileItem[]) => void | - |
+| onStopGenerator | Fires when the stop generation button is clicked | (message: Message) => void | - |
+| placeholder | Input box placeholder | string | - |
+| renderInputArea | Custom rendering input box | (props: RenderInputAreaProps) => React.ReactNode | - |
+| showClearContext | Whether to display the clear context button| boolean | false |
+| showStopGenerate | Whether to display the stop generation button| boolean | false |
+| topSlot | top slot for chat | React.ReactNode | - |
+| uploadProps | Upload component properties, refer to details [Upload](/zh-CN/input/upload#API%20%E5%8F%82%E8%80%83) | UploadProps | - |
+| uploadTipProps | Upload component prompt attribute, refer to details [Tooltip](/zh-CN/show/tooltip#API%20%E5%8F%82%E8%80%83) | TooltipProps | - |
+
+
+#### RoleConfig
+
+| PROPERTIES | INSTRUCTIONS | TYPE | DEFAULT |
+|------|--------|-------|-------|
+| user | User information | Metadata | - |
+| assistant | Assistant information | Metadata | - |
+| system | System information | Metadata | - |
+
+#### Metadata
+
+| PROPERTIES | INSTRUCTIONS | TYPE | DEFAULT |
+|------|--------|-------|-------|
+| name | name | string | - |
+| avatar | avatar | string | - |
+| color | Avatar background color, same as the color parameter of Avatar component, support `amber`、 `blue`、 `cyan`、 `green`、 `grey`、 `indigo`、 `light-blue`、 `light-green`、 `lime`、 `orange`、 `pink`、 `purple`、 `red`、 `teal`、 `violet`、 `yellow` | string | `grey` |
+
+#### Message
+
+| PROPERTIES | INSTRUCTIONS | TYPE | DEFAULT |
+|------|--------|-------|-------|
+| role | role | string | - |
+| name | name | string | - |
+| id | Uniquely identifies | string\| number | - |
+| content | all content | string| Content[] | - |
+| parentId | parent Uniquely identifies | string | - |
+| createAt | creation time | number | -|
+| status | Information status, `loading` \| `incomplete` \| `complete` \| `error` | string | complete |
+
+
+#### Content
+
+| PROPERTIES | INSTRUCTIONS | TYPE | DEFAULT |
+|------|--------|-------|-------|
+| type | type, suport `text` \| `image_url` \| `file_url` | string | - |
+| text | Content data when type is `text` | string | - |
+| image_url | Content data when type is `image_url` | { url: string } | - |
+| file_url | Content data when type is `file_url` | { url: string; name: string; size: string; type: string } | - |
+
+#### Methods
+
+| METHOD | INSTRUCTIONS |
+|------|--------|
+| resetMessage | Reset message |
+| scrollToBottom(animation: boolean) | Scroll to the bottom, if animation is true, there will be animation, otherwise there will be no animation. |
+| clearContext | clear context|
+| sendMessage(content: string, attachment: FileItem[]) | send message with content and attachment |
+
+## Design Token
+
+
\ No newline at end of file
diff --git a/content/plus/chat/index.md b/content/plus/chat/index.md
new file mode 100644
index 0000000000..87591c2d0d
--- /dev/null
+++ b/content/plus/chat/index.md
@@ -0,0 +1,1616 @@
+---
+localeCode: zh-CN
+order: 82
+category: Plus
+title: Chat 对话
+icon: doc-chat
+dir: column
+brief: 用于快速搭建对话内容
+---
+
+## 使用场景
+
+Chat 组件可用于普通会话,AI 会话等场景。
+
+对话内容渲染基于 MarkdownRender 组件,支持 Markdown 和 MDX,可实现图片,表格,链接,加粗,代码区等常用富文本功能。也可通过 JSX 实现更加复杂定制化的文档撰写与展示需求。
+
+
+## 代码演示
+
+### 如何引入
+
+Chat 从 v2.63.0 版本开始支持。
+
+```jsx
+import { Chat } from '@douyinfe/semi-ui';
+```
+
+### 基本用法
+
+通过设置 `chats` 和 `onChatsChange`,`onMessageSend` 实现基础对话显示和交互。
+
+附件支持通过点击上传按钮,输入框粘贴,拖拽文件至 Chat 区域上传。通过 `uploadProps` 设置上传参数,详情参考 [Upload](/zh-CN/input/upload#API%20%E5%8F%82%E8%80%83)。
+
+上传按钮的提示文案可通过 `uploadTipProps` 设置,详情参考 [Tooltip](/zh-CN/tooltip#API%20%E5%8F%82%E8%80%83)。
+
+对话是多方参与,多轮交互的场景。可通过 `roleConfig` 传入角色信息(包括名称,头像等),具体参数细节 [RoleConfig](#roleConfig)。
+
+使用 `align` 属性可以设置对话的对齐方式,支持左右对齐(`leftRight`, 默认)和左对齐(`leftAlign`)。
+
+```jsx live=true noInline=true dir="column"
+import React, {useState, useCallback} from 'react';
+import { Chat, Radio } from '@douyinfe/semi-ui';
+
+const defaultMessage = [
+ {
+ role: 'system',
+ id: '1',
+ createAt: 1715676751919,
+ content: "Hello, I'm your AI assistant.",
+ },
+ {
+ role: 'user',
+ id: '2',
+ createAt: 1715676751919,
+ content: "给一个 Semi Design 的 Button 组件的使用示例",
+ },
+ {
+ role: 'assistant',
+ id: '3',
+ createAt: 1715676751919,
+ content: "以下是一个 Semi 代码的使用示例:\n\`\`\`jsx \nimport React from 'react';\nimport { Button } from '@douyinfe/semi-ui';\n\nconst MyComponent = () => {\n return (\n \n );\n};\nexport default MyComponent;\n\`\`\`\n",
+ }
+];
+
+const roleInfo = {
+ user: {
+ name: 'User',
+ avatar: 'https://lf3-static.bytednsdoc.com/obj/eden-cn/ptlz_zlp/ljhwZthlaukjlkulzlp/docs-icon.png'
+ },
+ assistant: {
+ name: 'Assistant',
+ avatar: 'https://lf3-static.bytednsdoc.com/obj/eden-cn/ptlz_zlp/ljhwZthlaukjlkulzlp/other/logo.png'
+ },
+ system: {
+ name: 'System',
+ avatar: 'https://lf3-static.bytednsdoc.com/obj/eden-cn/ptlz_zlp/ljhwZthlaukjlkulzlp/other/logo.png'
+ }
+}
+
+const commonOuterStyle = {
+ border: '1px solid var(--semi-color-border)',
+ borderRadius: '16px',
+ margin: '8px 16px',
+ height: 550,
+}
+
+let id = 0;
+function getId() {
+ return `id-${id++}`
+}
+
+const uploadProps = { action: 'https://api.semi.design/upload' }
+const uploadTipProps = { content: '自定义上传按钮提示信息' }
+
+function DefaultChat() {
+ const [message, setMessage] = useState(defaultMessage);
+ const [mode, setMode] = useState('bubble');
+ const [align, setAlign] = useState('leftRight');
+
+ const onAlignChange = useCallback((e) => {
+ setAlign(e.target.value);
+ }, []);
+
+ const onModeChange = useCallback((e) => {
+ setMode(e.target.value);
+ }, []);
+
+ const onMessageSend = useCallback((content, attachment) => {
+ const newAssistantMessage = {
+ role: 'assistant',
+ id: getId(),
+ createAt: Date.now(),
+ content: "这是一条 mock 回复信息",
+ }
+ setTimeout(() => {
+ setMessage((message) => ([ ...message, newAssistantMessage]));
+ }, 200);
+ }, []);
+
+ const onChatsChange = useCallback((chats) => {
+ setMessage(chats);
+ }, []);
+
+ const onMessageReset = useCallback((e) => {
+ setTimeout(() => {
+ setMessage((message) => {
+ const lastMessage = message[message.length - 1];
+ const newLastMessage = {
+ ...lastMessage,
+ status: 'complete',
+ content: 'This is a mock reset message.',
+ }
+ return [...message.slice(0, -1), newLastMessage]
+ })
+ }, 200);
+ })
+
+ return (
+ <>
+
+
+ 模式
+
+ 气泡
+ 非气泡
+ 用户会话气泡
+
+
+
+ 会话对齐方式
+
+ 左右分布
+ 左对齐
+
+
+
+
+ >
+ )
+}
+
+render(DefaultChat);
+```
+
+### 消息状态
+
+chats 类型为 `Message[]`, `Message` 包含对话的各种信息,如角色(role)、内容(content)、附件(attachment)、状态(status)
+、唯一标识(id)、创建时间(createAt)等,具体见 [Message](#Message)。其中 status 不同,会话样式不同。
+
+``` jsx live=true noInline=true dir="column"
+import React, {useState, useCallback} from 'react';
+import { Chat } from '@douyinfe/semi-ui';
+
+const defaultMessage = [
+ {
+ role: 'assistant',
+ id: '1',
+ createAt: 1715676751919,
+ content: "请求成功",
+ },
+ {
+ id: 'loading',
+ role: 'assistant',
+ status: 'loading'
+ },
+ {
+ role: 'assistant',
+ id: 'error',
+ content: '请求错误',
+ status: 'error'
+ }
+];
+
+const roleInfo = {
+ user: {
+ name: 'User',
+ avatar: 'https://lf3-static.bytednsdoc.com/obj/eden-cn/ptlz_zlp/ljhwZthlaukjlkulzlp/docs-icon.png'
+ },
+ assistant: {
+ name: 'Assistant',
+ avatar: 'https://lf3-static.bytednsdoc.com/obj/eden-cn/ptlz_zlp/ljhwZthlaukjlkulzlp/other/logo.png'
+ },
+ system: {
+ name: 'System',
+ avatar: 'https://lf3-static.bytednsdoc.com/obj/eden-cn/ptlz_zlp/ljhwZthlaukjlkulzlp/other/logo.png'
+ }
+}
+
+const commonOuterStyle = {
+ border: '1px solid var(--semi-color-border)',
+ borderRadius: '16px',
+ height: 400,
+}
+
+let id = 0;
+function getId() { return `id-${id++}` }
+const uploadProps = { action: 'https://api.semi.design/upload' }
+
+function MessageStatus() {
+ const [message, setMessage] = useState(defaultMessage);
+
+ const onMessageSend = useCallback((content, attachment) => {
+ const newAssistantMessage = {
+ role: 'assistant',
+ id: getId(),
+ createAt: Date.now(),
+ content: "这是一条 mock 回复信息",
+ }
+ setTimeout(() => {
+ setMessage((message) => ([ ...message, newAssistantMessage]));
+ }, 200);
+ }, []);
+
+ const onChatsChange = useCallback((chats) => {
+ setMessage(chats);
+ }, []);
+
+ return (
+
+ )
+}
+
+render(MessageStatus);
+```
+
+### 动态更新数据
+
+对于后台返回 Serve Side Event 数据情况,可将获取到的数据用于更新 `chats`,对话内容将实时更新。
+
+`showStopGenerate` 参数可用于设置是否展示停止生成按钮,默认为 `false`。 可以在 `onStopGenerator` 中处理停止生成逻辑。
+
+```jsx live=true noInline=true dir="column"
+import React, {useState, useCallback} from 'react';
+import { Chat } from '@douyinfe/semi-ui';
+
+const defaultMessage = [
+ {
+ role: 'system',
+ id: '1',
+ createAt: 1715676751919,
+ content: "Hello, I'm your AI assistant.",
+ },
+ {
+ role: 'user',
+ id: '2',
+ createAt: 1715676751919,
+ content: "介绍一下 Semi design"
+ },
+ {
+ role: 'assistant',
+ id: '3',
+ createAt: 1715676751919,
+ content: `
+Semi Design 是由抖音前端团队和MED产品设计团队设计、开发并维护的设计系统。作为一个全面、易用、优质的现代应用UI解决方案,Semi Design从字节跳动各业务线的复杂场景中提炼而来,目前已经支撑了近千个平台产品,服务了内外部超过10万用户[[1]](https://semi.design/zh-CN/start/introduction)。
+
+Semi Design的特点包括:
+
+1. 设计简洁、现代化。
+2. 提供主题方案,可深度样式定制。
+3. 提供明暗色两套模式,切换方便。
+4. 国际化,覆盖了简/繁体中文、英语、日语、韩语、葡萄牙语等20+种语言,日期时间组件提供全球时区支持,全部组件可自动适配阿拉伯文RTL布局。
+5. 采用 Foundation 和 Adapter 跨框架技术方案,方便扩展。
+
+---
+Learn more:
+1. [Introduction 介绍 - Semi Design](https://semi.design/zh-CN/start/introduction)
+2. [Getting Started 快速开始 - Semi Design](https://semi.design/zh-CN/start/getting-started)
+3. [Semi D2C 设计稿转代码的演进之路 - 知乎](https://zhuanlan.zhihu.com/p/667189184)
+`,
+ }
+];
+
+const roleInfo = {
+ user: {
+ name: 'User',
+ avatar: 'https://lf3-static.bytednsdoc.com/obj/eden-cn/ptlz_zlp/ljhwZthlaukjlkulzlp/docs-icon.png'
+ },
+ assistant: {
+ name: 'Assistant',
+ avatar: 'https://lf3-static.bytednsdoc.com/obj/eden-cn/ptlz_zlp/ljhwZthlaukjlkulzlp/other/logo.png'
+ },
+ system: {
+ name: 'System',
+ avatar: 'https://lf3-static.bytednsdoc.com/obj/eden-cn/ptlz_zlp/ljhwZthlaukjlkulzlp/other/logo.png'
+ }
+}
+
+const commonOuterStyle = {
+ border: '1px solid var(--semi-color-border)',
+ borderRadius: '16px',
+ height: 600,
+}
+
+let id = 0;
+function getId() {
+ return `id-${id++}`
+}
+const uploadProps = { action: 'https://api.semi.design/upload' }
+
+function DynamicUpdateChat() {
+ const [message, setMessage] = useState(defaultMessage);
+ const intervalId = useRef();
+ const onMessageSend = useCallback((content, attachment) => {
+ setMessage((message) => {
+ return [
+ ...message,
+ {
+ role: 'assistant',
+ status: 'loading',
+ createAt: Date.now(),
+ id: getId()
+ }
+ ]
+ });
+ generateMockResponse(content);
+ }, []);
+
+ const onChatsChange = useCallback((chats) => {
+ setMessage(chats);
+ }, []);
+
+ const generateMockResponse = useCallback((content) => {
+ const id = setInterval(() => {
+ setMessage((message) => {
+ const lastMessage = message[message.length - 1];
+ let newMessage = {...lastMessage};
+ if (lastMessage.status === 'loading') {
+ newMessage = {
+ ...newMessage,
+ content: `mock Response for ${content} \n`,
+ status: 'incomplete'
+ }
+ } else if (lastMessage.status === 'incomplete') {
+ if (lastMessage.content.length > 200) {
+ clearInterval(id);
+ intervalId.current = null
+ newMessage = {
+ ...newMessage,
+ content: `${lastMessage.content} mock stream message`,
+ status: 'complete'
+ }
+ } else {
+ newMessage = {
+ ...newMessage,
+ content: `${lastMessage.content} mock stream message`
+ }
+ }
+ }
+ return [ ...message.slice(0, -1), newMessage ]
+ })
+ }, 400);
+ intervalId.current = id;
+ }, []);
+
+ const onStopGenerator = useCallback(() => {
+ if (intervalId.current) {
+ clearInterval(intervalId.current);
+ setMessage((message) => {
+ const lastMessage = message[message.length - 1];
+ if (lastMessage.status && lastMessage.status !== 'complete') {
+ const lastMessage = message[message.length - 1];
+ let newMessage = {...lastMessage};
+ newMessage.status = 'complete';
+ return [
+ ...message.slice(0, -1),
+ newMessage
+ ]
+ } else {
+ return message;
+ }
+ })
+ }
+ }, [intervalId]);
+
+ return (
+
+ )
+}
+
+render(DynamicUpdateChat);
+```
+
+### 清除上下文
+
+通过 `showClearContext` 可以开启在输入框中显示清除上下文按钮,默认为 `false`。
+也可以通过 ref 调用 `clearContext` 方法清除上下文。
+
+```jsx live=true noInline=true dir="column"
+import React, {useState, useCallback} from 'react';
+import { Chat, Radio } from '@douyinfe/semi-ui';
+
+const defaultMessage = [
+ {
+ role: 'system',
+ id: '1',
+ createAt: 1715676751919,
+ content: "Hello, I'm your AI assistant.",
+ },
+ {
+ role: 'user',
+ id: '2',
+ createAt: 1715676751919,
+ content: "介绍一下 semi design",
+ },
+ {
+ role: 'assistant',
+ id: '3',
+ createAt: 1715676751919,
+ content: 'Semi Design 是由抖音前端团队和MED产品设计团队设计、开发并维护的设计系统',
+ }
+];
+
+const roleInfo = {
+ user: {
+ name: 'User',
+ avatar: 'https://lf3-static.bytednsdoc.com/obj/eden-cn/ptlz_zlp/ljhwZthlaukjlkulzlp/docs-icon.png'
+ },
+ assistant: {
+ name: 'Assistant',
+ avatar: 'https://lf3-static.bytednsdoc.com/obj/eden-cn/ptlz_zlp/ljhwZthlaukjlkulzlp/other/logo.png'
+ },
+ system: {
+ name: 'System',
+ avatar: 'https://lf3-static.bytednsdoc.com/obj/eden-cn/ptlz_zlp/ljhwZthlaukjlkulzlp/other/logo.png'
+ }
+}
+
+const commonOuterStyle = {
+ border: '1px solid var(--semi-color-border)',
+ borderRadius: '16px',
+ margin: '8px 16px',
+ height: 550,
+}
+
+let id = 0;
+function getId() {
+ return `id-${id++}`
+}
+
+const uploadProps = { action: 'https://api.semi.design/upload' }
+const uploadTipProps = { content: '自定义上传按钮提示信息' }
+
+function DefaultChat() {
+ const [message, setMessage] = useState(defaultMessage);
+
+ const onMessageSend = useCallback((content, attachment) => {
+ const newAssistantMessage = {
+ role: 'assistant',
+ id: getId(),
+ createAt: Date.now(),
+ content: "这是一条 mock 回复信息",
+ }
+ setTimeout(() => {
+ setMessage((message) => ([ ...message, newAssistantMessage]));
+ }, 200);
+ }, []);
+
+ const onChatsChange = useCallback((chats) => {
+ setMessage(chats);
+ }, []);
+
+ const onMessageReset = useCallback((e) => {
+ setTimeout(() => {
+ setMessage((message) => {
+ const lastMessage = message[message.length - 1];
+ const newLastMessage = {
+ ...lastMessage,
+ status: 'complete',
+ content: 'This is a mock reset message.',
+ }
+ return [...message.slice(0, -1), newLastMessage]
+ })
+ }, 200);
+ })
+
+ return (
+ <>
+
+ >
+ )
+}
+
+render(DefaultChat);
+```
+
+### 自定义渲染会话框
+
+通过 `chatBoxRenderConfig` 传入自定义渲染配置, chatBoxRenderConfig 类型如下
+
+```ts
+interface ChatBoxRenderConfig {
+ /* 自定义渲染标题 */
+ renderChatBoxTitle?: (props: {role?: Metadata, defaultTitle?: ReactNode}) => ReactNode;
+ /* 自定义渲染头像 */
+ renderChatBoxAvatar?: (props: { role?: Metadata, defaultAvatar?: ReactNode}) => ReactNode;
+ /* 自定义渲染内容区域 */
+ renderChatBoxContent?: (props: {message?: Message, role?: Metadata, defaultContent?: ReactNode | ReactNode[], className?: string}) => ReactNode;
+ /* 自定义渲染消息操作栏 */
+ renderChatBoxAction?: (props: {message?: Message, defaultActions?: ReactNode | ReactNode[], className: string}) => ReactNode;
+ /* 完全自定义渲染整个聊天框 */
+ renderFullChatBox?: (props: {message?: Message, role?: Metadata, defaultNodes?: FullChatBoxNodes, className: string}) => ReactNode;
+}
+```
+
+自定义渲染头像和标题,可通过 `renderChatBoxAvatar` 和 `renderChatBoxTitle` 实现。
+
+```jsx live=true noInline=true dir="column"
+
+import React, {useState, useCallback} from 'react';
+import { Chat, Avatar, Tag } from '@douyinfe/semi-ui';
+
+const defaultMessage = [
+ {
+ role: 'system',
+ id: '1',
+ createAt: 1715676751919,
+ content: "Hello, I'm your AI assistant.",
+ },
+ {
+ role: 'user',
+ id: '2',
+ createAt: 1715676751919,
+ content: [
+ {
+ type: 'text',
+ text: '这张图片里有什么?'
+ },
+ {
+ type: 'image_url',
+ image_url: {
+ url: 'https://lf3-static.bytednsdoc.com/obj/eden-cn/ptlz_zlp/ljhwZthlaukjlkulzlp/root-web-sites/edit-bag.jpeg'
+ }
+ }
+ ],
+ },
+ {
+ role: 'assistant',
+ id: '3',
+ createAt: 1715676751919,
+ content: '图片中是一个有卡通画像装饰的黄色背包。'
+ },
+
+];
+
+const roleInfo = {
+ user: {
+ name: 'User',
+ avatar: 'https://lf3-static.bytednsdoc.com/obj/eden-cn/ptlz_zlp/ljhwZthlaukjlkulzlp/docs-icon.png'
+ },
+ assistant: {
+ name: 'Assistant',
+ avatar: 'https://lf3-static.bytednsdoc.com/obj/eden-cn/ptlz_zlp/ljhwZthlaukjlkulzlp/other/logo.png'
+ },
+ system: {
+ name: 'System',
+ avatar: 'https://lf3-static.bytednsdoc.com/obj/eden-cn/ptlz_zlp/ljhwZthlaukjlkulzlp/other/logo.png'
+ }
+}
+
+const commonOuterStyle = {
+ border: '1px solid var(--semi-color-border)',
+ borderRadius: '16px',
+ height: 400,
+}
+
+let id = 0;
+function getId() { return `id-${id++}`; }
+const uploadProps = { action: 'https://api.semi.design/upload' }
+
+function CustomRender() {
+ const [title, setTitle] = useState('null');
+ const [avatar, setAvatar] = useState('null');
+ const [message, setMessage] = useState(defaultMessage);
+
+ const onChatsChange = useCallback((chats) => {
+ setMessage(chats);
+ }, []);
+
+ const customRenderAvatar = useMemo(()=> {
+ switch(avatar) {
+ case 'custom': return (props) => {
+ const { role, defaultAvatar } = props;
+ return {role.name}
+ }
+ case 'null': return () => null
+ case 'default': return undefined;
+ }
+ }, [avatar]);
+
+ const customRenderTitle = useMemo(()=> {
+ switch(title) {
+ case 'custom': return (props) => {
+ const { role, defaultTitle, message } = props;
+ const date = new Date(message.createAt);
+ const hours = ('0' + date.getHours()).slice(-2);
+ const minutes = ('0' + date.getMinutes()).slice(-2);
+ const formatTime = `${hours}:${minutes}`;
+ return (
+ {role.name}
+ {formatTime}
+ )
+ }
+ case 'null': return () => null
+ case 'default': return undefined;
+ }
+ }, [title]);;
+
+ const onAvatarChange = useCallback((e) => { setAvatar(e.target.value) }, []);
+ const onTitleChange = useCallback((e) => { setTitle(e.target.value) }, []);
+
+ const onMessageSend = useCallback((content, attachment) => {
+ const newAssistantMessage = {
+ role: 'assistant',
+ id: getId(),
+ content: `This is a mock response`
+ }
+ setTimeout(() => {
+ setMessage((message) => ([ ...message, newAssistantMessage]));
+ }, 200);
+ }, []);
+
+ return (
+ <>
+
+
+ 头像渲染模式
+
+ 默认头像
+ 无头像
+ 自定义头像
+
+
+
+ 标题渲染模式
+
+ 默认标题
+ 无标题
+ 自定义标题
+
+
+
+
+ >
+ );
+}
+
+render(CustomRender);
+```
+
+鼠标移动到会话上,即可显示会话操作区,通过 `renderChatBoxAction` 自定义渲染操作区
+
+```jsx live=true noInline=true dir="column"
+import React, {useState, useCallback} from 'react';
+import { Chat, Dropdown } from '@douyinfe/semi-ui';
+import { IconForward } from '@douyinfe/semi-icons';
+
+const defaultMessage = [
+ {
+ role: 'system',
+ id: '1',
+ createAt: 1715676751919,
+ content: "Hello, I'm your AI assistant.",
+ },
+ {
+ role: 'user',
+ id: '2',
+ createAt: 1715676751919,
+ content: "介绍一下 semi design",
+ },
+ {
+ role: 'assistant',
+ id: '3',
+ createAt: 1715676751919,
+ content: 'Semi Design 是由抖音前端团队和MED产品设计团队设计、开发并维护的设计系统',
+ }
+];
+
+const roleInfo = {
+ user: {
+ name: 'User',
+ avatar: 'https://lf3-static.bytednsdoc.com/obj/eden-cn/ptlz_zlp/ljhwZthlaukjlkulzlp/docs-icon.png'
+ },
+ assistant: {
+ name: 'Assistant',
+ avatar: 'https://lf3-static.bytednsdoc.com/obj/eden-cn/ptlz_zlp/ljhwZthlaukjlkulzlp/other/logo.png'
+ },
+ system: {
+ name: 'System',
+ avatar: 'https://lf3-static.bytednsdoc.com/obj/eden-cn/ptlz_zlp/ljhwZthlaukjlkulzlp/other/logo.png'
+ }
+}
+
+const commonOuterStyle = {
+ border: '1px solid var(--semi-color-border)',
+ borderRadius: '16px',
+ height: 400,
+}
+
+let id = 0;
+function getId() { return `id-${id++}`; }
+const uploadProps = { action: 'https://api.semi.design/upload' }
+
+const CustomActions = React.memo((props) => {
+ const { role, message, defaultActions, className } = props;
+ const myRef = useRef();
+ const getContainer = useCallback(() => {
+ if (myRef.current) {
+ const element = myRef.current;
+ let parentElement = element.parentElement;
+ while (parentElement) {
+ if (parentElement.classList.contains('semi-chat-chatBox-wrap')) {
+ return parentElement;
+ }
+ parentElement = parentElement.parentElement;
+ }
+ }
+ }, [myRef]);
+
+ return
+ {defaultActions}
+ {
+ }>分享
+
+ }
+ trigger="click"
+ position="top"
+ getPopupContainer={getContainer}
+ >
+ }
+
+});
+
+function CustomRender() {
+ const [message, setMessage] = useState(defaultMessage);
+
+ const customRenderAction = useCallback((props) => {
+ return
+ }, []);
+
+ const onChatsChange = useCallback((chats) => {
+ setMessage(chats);
+ }, []);
+
+ const onMessageSend = useCallback((content, attachment) => {
+ const newAssistantMessage = {
+ role: 'assistant',
+ id: getId(),
+ content: `This is a mock response`
+ }
+ setTimeout(() => {
+ setMessage((message) => ([ ...message, newAssistantMessage]));
+ }, 200);
+ }, []);
+
+ return (
+
+ );
+}
+
+render(CustomRender);
+```
+
+通过 `renderChatBoxContent` 自定义操作区域
+
+```jsx live=true noInline=true dir="column"
+import React, { useState, useCallback, useRef} from 'react';
+import { Chat, MarkdownRender } from '@douyinfe/semi-ui';
+
+const defaultMessage = [
+ {
+ role: 'assistant',
+ id: '3',
+ createAt: 1715676751919,
+ content: "Semi Design 是由抖音前端团队,MED 产品设计团队设计、开发并维护的设计系统。它作为全面、易用、优质的现代应用 UI 解决方案,从字节跳动各业务线的复杂场景提炼而来,支撑近千计平台产品,服务内外部 10 万+ 用户。",
+ source: [
+ {
+ avatar: 'https://lf3-static.bytednsdoc.com/obj/eden-cn/ptlz_zlp/ljhwZthlaukjlkulzlp/other/logo.png',
+ url: '/zh-CN/start/introduction',
+ title: 'semi Design',
+ subTitle: 'Semi design website',
+ content: 'Semi Design 是由抖音前端团队,MED 产品设计团队设计、开发并维护的设计系统。'
+ },
+ {
+ avatar: 'https://lf3-static.bytednsdoc.com/obj/eden-cn/ptlz_zlp/ljhwZthlaukjlkulzlp/other/logo.png',
+ url: '/dsm/landing',
+ subTitle: 'Semi DSM website',
+ title: 'Semi 设计系统',
+ content: '从 Semi Design,到 Any Design 快速定义你的设计系统,并应用在设计稿和代码中'
+ },
+ {
+ avatar: 'https://lf3-static.bytednsdoc.com/obj/eden-cn/ptlz_zlp/ljhwZthlaukjlkulzlp/other/logo.png',
+ url: '/code/zh-CN/start/introduction',
+ subTitle: 'Semi D2C website',
+ title: '设计稿转代码',
+ content: 'Semi 设计稿转代码(Semi Design to Code,或简称 Semi D2C),是由抖音前端 Semi Design 团队推出的全新的提效工具'
+ },
+ ]
+ }];
+
+const roleInfo = {
+ user: {
+ name: 'User',
+ avatar: 'https://lf3-static.bytednsdoc.com/obj/eden-cn/ptlz_zlp/ljhwZthlaukjlkulzlp/docs-icon.png'
+ },
+ assistant: {
+ name: 'Assistant',
+ avatar: 'https://lf3-static.bytednsdoc.com/obj/eden-cn/ptlz_zlp/ljhwZthlaukjlkulzlp/other/logo.png'
+ },
+ system: {
+ name: 'System',
+ avatar: 'https://lf3-static.bytednsdoc.com/obj/eden-cn/ptlz_zlp/ljhwZthlaukjlkulzlp/other/logo.png'
+ }
+}
+
+const commonOuterStyle = {
+ border: '1px solid var(--semi-color-border)',
+ borderRadius: '16px',
+ height: 500,
+}
+
+let id = 0;
+function getId() { return `id-${id++}` }
+const uploadProps = { action: 'https://api.semi.design/upload' }
+
+const SourceCard = (props) => {
+ const [open, setOpen] = useState(true);
+ const [show, setShow] = useState(false);
+ const { source } = props;
+ const spanRef = useRef();
+ const onOpen = useCallback(() => {
+ setOpen(false);
+ setShow(true);
+ }, []);
+
+ const onClose = useCallback(() => {
+ setOpen(true);
+ setTimeout(() => {
+ setShow(false);
+ }, 350)
+ }, []);
+
+ return (
+
+ 基于{source.length}个搜索来源
+
+ {source.map((s, index) => ())}
+
+
+
+
+ Source
+
+
+
+ {source.map(s => (
+
+
+
+ {s.title}
+
+ {s.subTitle}
+ {s.content}
+ ))}
+
+
+
+ )
+}
+
+function CustomRender() {
+ const [message, setMessage] = useState(defaultMessage);
+
+ const onChatsChange = useCallback((chats) => {
+ setMessage(chats);
+ }, []);
+
+ const onMessageSend = useCallback((content, attachment) => {
+ const newAssistantMessage = {
+ role: 'assistant',
+ id: getId(),
+ content: `This is a mock response`
+ }
+ setTimeout(() => {
+ setMessage((message) => ([ ...message, newAssistantMessage]));
+ }, 200);
+ }, []);
+
+ const renderContent = useCallback((props) => {
+ const { role, message, defaultNode, className } = props;
+ return
+ {message.source && }
+
+
+ }, []);
+
+ return (
+
+ );
+}
+
+render(CustomRender);
+```
+
+使用 `renderFullChatBox` 自定义渲染整个会话框
+
+```jsx live=true noInline=true dir="column"
+import React, {useState, useCallback} from 'react';
+import { Chat, Avatar } from '@douyinfe/semi-ui';
+
+const defaultMessage = [
+ {
+ role: 'system',
+ id: '1',
+ createAt: 1715676751919,
+ content: "Hello, I'm your AI assistant.",
+ },
+ {
+ role: 'user',
+ id: '2',
+ createAt: 1715676751919,
+ content: "介绍一下 semi design",
+ },
+ {
+ role: 'assistant',
+ id: '3',
+ createAt: 1715676751919,
+ content: 'Semi Design 是由抖音前端团队和MED产品设计团队设计、开发并维护的设计系统',
+ }
+];
+
+const roleInfo = {
+ user: {
+ name: 'User',
+ avatar: 'https://lf3-static.bytednsdoc.com/obj/eden-cn/ptlz_zlp/ljhwZthlaukjlkulzlp/docs-icon.png'
+ },
+ assistant: {
+ name: 'Assistant',
+ avatar: 'https://lf3-static.bytednsdoc.com/obj/eden-cn/ptlz_zlp/ljhwZthlaukjlkulzlp/other/logo.png'
+ },
+ system: {
+ name: 'System',
+ avatar: 'https://lf3-static.bytednsdoc.com/obj/eden-cn/ptlz_zlp/ljhwZthlaukjlkulzlp/other/logo.png'
+ }
+}
+
+const commonOuterStyle = {
+ border: '1px solid var(--semi-color-border)',
+ borderRadius: '16px',
+ height: 400,
+}
+
+let id = 0;
+function getId() { return `id-${id++}`; }
+const uploadProps = { action: 'https://api.semi.design/upload' }
+
+const titleStyle = { display:' flex', alignItems: 'center', justifyContent: 'center', columnGap: '10px', padding: '5px 0px', width: 'fit-content' };
+
+function CustomFullRender() {
+ const [message, setMessage] = useState(defaultMessage);
+
+ const customRenderChatBox = useCallback((props) => {
+ const { role, message, defaultNodes, className } = props;
+ let titleNode = null;
+ if (message.role !== 'user') {
+ titleNode = (
+
+ {defaultNodes.title}
+ )
+ }
+ return
+
+ {titleNode}
+
+ {defaultNodes.content}
+
+ {defaultNodes.action}
+
+
+ }, []);
+
+ const onChatsChange = useCallback((chats) => {
+ setMessage(chats)
+ } ,[]);
+
+ const onMessageSend = useCallback((content, attachment) => {
+ const newAssistantMessage = {
+ role: 'assistant',
+ id: getId(),
+ content: `This is a mock response`
+ }
+ setTimeout(() => {
+ setMessage((message) => ([ ...message, newAssistantMessage]));
+ }, 200);
+ }, []);
+
+ return ( );
+}
+
+render(CustomFullRender)
+```
+
+### 自定义渲染输入框
+
+可通过 `renderInputArea` 自定义渲染输入框,参数如下
+
+``` ts
+export interface RenderInputAreaProps {
+ /* 默认节点 */
+ defaultNode?: ReactNode;
+ /* 如果自定义输入框,发送消息时需调用 */
+ onSend?: (content?: string, attachment?: FileItem[]) => void;
+ /* 如果自定义清除上下文按钮,点击清除上下文时需调用 */
+ onClear?: (e?: any) => void;
+}
+```
+
+使用示例如下
+
+```jsx live=true noInline=true dir="column"
+import React, {useState, useCallback} from 'react';
+import { Form, Chat } from '@douyinfe/semi-ui';
+
+const defaultMessage = [
+ {
+ role: 'system',
+ id: '1',
+ createAt: 1715676751919,
+ content: "Hello, I'm your AI assistant.",
+ },
+];
+
+const roleInfo = {
+ user: {
+ name: 'User',
+ avatar: 'https://lf3-static.bytednsdoc.com/obj/eden-cn/ptlz_zlp/ljhwZthlaukjlkulzlp/docs-icon.png'
+ },
+ assistant: {
+ name: 'Assistant',
+ avatar: 'https://lf3-static.bytednsdoc.com/obj/eden-cn/ptlz_zlp/ljhwZthlaukjlkulzlp/other/logo.png'
+ },
+ system: {
+ name: 'System',
+ avatar: 'https://lf3-static.bytednsdoc.com/obj/eden-cn/ptlz_zlp/ljhwZthlaukjlkulzlp/other/logo.png'
+ }
+}
+
+const commonOuterStyle = {
+ border: '1px solid var(--semi-color-border)',
+ borderRadius: '16px',
+ height: 500,
+};
+
+let id = 0;
+function getId() {
+ return `id-${id++}`
+}
+const uploadProps = { action: 'https://api.semi.design/upload' }
+
+const inputStyle = {
+ display: 'flex',
+ flexDirection: 'column',
+ border: '1px solid var(--semi-color-border)',
+ margin: '8px 16px',
+ borderRadius: 8,
+ padding: 8
+}
+
+function CustomInputRender(props) {
+ const { defaultNode, onClear, onSend } = props;
+ const api = useRef();
+ const onSubmit = useCallback(() => {
+ if (api.current) {
+ const values = api.current.getValues();
+ if ((values.name && values.name.length !== 0) || (values.file && values.file.length !== 0)) {
+ onSend(values.name, values.file);
+ api.current.reset();
+ }
+ }
+ }, []);
+
+ return (
+
+
+ } theme="light">
+ 点击上传
+
+
+
+
+ );
+}
+
+function CustomRenderInputArea() {
+ const [message, setMessage] = useState(defaultMessage);
+
+ const onChatsChange = useCallback((chats) => {
+ setMessage(chats);
+ }, []);
+
+ const onMessageSend = useCallback((content, attachment) => {
+ const newAssistantMessage = {
+ role: 'assistant',
+ id: getId(),
+ content: `This is a mock response`
+ }
+ setTimeout(() => {
+ setMessage((message) => ([ ...message, newAssistantMessage]));
+ }, 200);
+ }, []);
+
+ const renderInputArea = useCallback((props) => {
+ return ()
+ }, []);
+
+ return (
+
+ )
+}
+render(CustomRenderInputArea);
+```
+
+### 提示信息
+
+通过 `hints` 可设置提示区域内容, 点击提示内容后,提示内容将成为新的用户输入内容,并触发 `onHintClick` 回调。
+
+```jsx live=true noInline=true dir="column"
+import React, {useState, useCallback} from 'react';
+import { Chat } from '@douyinfe/semi-ui';
+
+const defaultMessage = [
+ {
+ role: 'assistant',
+ id: '1',
+ createAt: 1715676751919,
+ content: 'Semi Design 是由抖音前端团队和MED产品设计团队设计、开发并维护的设计系统,你可以向我提问任何关于 Semi 的问题。',
+ }
+];
+
+const hintsExample = [
+ "告诉我更多",
+ "Semi Design 的组件有哪些?",
+ "我能够通过 DSM 定制自己的主题吗?",
+]
+
+const roleInfo = {
+ user: {
+ name: 'User',
+ avatar: 'https://lf3-static.bytednsdoc.com/obj/eden-cn/ptlz_zlp/ljhwZthlaukjlkulzlp/docs-icon.png'
+ },
+ assistant: {
+ name: 'Assistant',
+ avatar: 'https://lf3-static.bytednsdoc.com/obj/eden-cn/ptlz_zlp/ljhwZthlaukjlkulzlp/other/logo.png'
+ },
+ system: {
+ name: 'System',
+ avatar: 'https://lf3-static.bytednsdoc.com/obj/eden-cn/ptlz_zlp/ljhwZthlaukjlkulzlp/other/logo.png'
+ }
+}
+
+const commonOuterStyle = {
+ border: '1px solid var(--semi-color-border)',
+ borderRadius: '16px',
+ height: 400,
+};
+
+let id = 0;
+function getId() {
+ return `id-${id++}`
+}
+const uploadProps = { action: 'https://api.semi.design/upload' }
+
+function DefaultChat() {
+ const [message, setMessage] = useState(defaultMessage);
+ const [hints, setHints] = useState(hintsExample);
+
+ const onHintClick = useCallback(() => {
+ setHints([]);
+ }, [])
+
+ const onMessageSend = useCallback((content, attachment) => {
+ const newAssistantMessage = {
+ role: 'assistant',
+ id: getId(),
+ createAt: Date.now(),
+ content: "这是一条 mock 回复信息",
+ }
+ setTimeout(() => {
+ setMessage((message) => ([ ...message, newAssistantMessage]));
+ }, 200);
+ }, []);
+
+ const onChatsChange = useCallback((chats) => {
+ setMessage(chats);
+ }, []);
+
+ onClear = useCallback(() => {
+ setHints([]);
+ }, [])
+
+ return (
+
+ )
+}
+
+render(DefaultChat);
+```
+
+### 自定义提示信息渲染
+
+通过 `renderHintBox` 自定义提示区域内容, 参数如下
+
+```ts
+type renderHintBox = (props: {content: string; index: number,onHintClick: () => void}) => React.ReactNode;
+```
+
+使用示例如下:
+
+```jsx live=true noInline=true dir="column"
+import React, {useState, useCallback} from 'react';
+import { Chat } from '@douyinfe/semi-ui';
+
+const defaultMessage = [
+ {
+ role: 'assistant',
+ id: '1',
+ createAt: 1715676751919,
+ content: 'Semi Design 是由抖音前端团队和MED产品设计团队设计、开发并维护的设计系统,你可以向我提问任何关于 Semi 的问题。',
+ }
+];
+
+const hintsExample = [
+ "告诉我更多",
+ "Semi Design 的组件有哪些?",
+ "我能够通过 DSM 定制自己的主题吗?",
+]
+
+const roleInfo = {
+ user: {
+ name: 'User',
+ avatar: 'https://lf3-static.bytednsdoc.com/obj/eden-cn/ptlz_zlp/ljhwZthlaukjlkulzlp/docs-icon.png'
+ },
+ assistant: {
+ name: 'Assistant',
+ avatar: 'https://lf3-static.bytednsdoc.com/obj/eden-cn/ptlz_zlp/ljhwZthlaukjlkulzlp/other/logo.png'
+ },
+ system: {
+ name: 'System',
+ avatar: 'https://lf3-static.bytednsdoc.com/obj/eden-cn/ptlz_zlp/ljhwZthlaukjlkulzlp/other/logo.png'
+ }
+}
+
+const commonOuterStyle = {
+ border: '1px solid var(--semi-color-border)',
+ borderRadius: '16px',
+ height: 400,
+};
+
+let id = 0;
+function getId() {
+ return `id-${id++}`
+}
+const uploadProps = { action: 'https://api.semi.design/upload' }
+
+function DefaultChat() {
+ const [message, setMessage] = useState(defaultMessage);
+ const [hints, setHints] = useState(hintsExample);
+
+ const onHintClick = useCallback(() => {
+ setHints([]);
+ }, [])
+
+ const onMessageSend = useCallback((content, attachment) => {
+ const newAssistantMessage = {
+ role: 'assistant',
+ id: getId(),
+ createAt: Date.now(),
+ content: "这是一条 mock 回复信息",
+ }
+ setTimeout(() => {
+ setMessage((message) => ([ ...message, newAssistantMessage]));
+ }, 200);
+ setHints([]);
+ }, []);
+
+ const onChatsChange = useCallback((chats) => {
+ setMessage(chats);
+ }, []);
+
+ const commonHintStyle = useMemo(() => ({
+ border: '1px solid var(--semi-color-border)',
+ padding: '10px',
+ borderRadius: '10px',
+ color: 'var( --semi-color-text-1)',
+ display: 'flex',
+ justifyContent: 'space-between',
+ alignItems: 'center',
+ cursor: 'pointer',
+ fontSize: '14px'
+ }), []);
+
+ const renderHintBox = useCallback((props) => {
+ const { content, onHintClick, index } = props;
+ return
+ {content}
+ click me
+
+ }, []);
+
+ onClear = useCallback(() => {
+ setHints([]);
+ }, [])
+
+ return (
+
+ )
+}
+
+render(DefaultChat);
+```
+
+### API
+
+| 属性 | 说明 | 类型 | 默认值 |
+|------|--------|-------|-------|
+| align | 对话对齐方式,支持 `leftRight`、`leftAlign` | string | `leftRight` |
+| bottomSlot | 底部插槽 | React.ReactNode | - |
+| chatBoxRenderConfig | chatBox 渲染配置 | ChatBoxRenderConfig | - |
+| chats | 受控对话列表 | Message | - |
+| className | 自定义类名 | string | - |
+| customMarkDownComponents | 自定义 markdown render, 透传给对话内容渲染的 MarkdownRender | MDXProps\['components'\]| - |
+| hints | 提示信息 | string | - |
+| hintCls | 提示区最外层样式类名 | string | - |
+| hintStyle | 提示区最外层样式 | CSSProperties | - |
+| inputBoxStyle | 输入框样式 | CSSProperties | - |
+| inputBoxCls | 输入框类名 | string | - |
+| sendHotKey | 发送输入内容的键盘快捷键,支持 `enter` \| `shift+enter`。前者在单独按下 enter 将发送输入框中的消息, shift 和 enter 按键同时按下时,仅换行,不发送。后者相反 | string | `enter` |
+| mode | 对话模式,支持 `bubble` \| `noBubble` \| `userBubble` | string | `bubble` |
+| roleConfig | 角色信息配置,具体见[RoleConfig](#RoleConfig) | RoleConfig | - |
+| renderHintBox | 自定义渲染提示信息 | (props: {content: string; index: number,onHintClick: () => void}) => React.ReactNode| - |
+| onChatsChange | 对话列表变化时触发 | (chats: Message[]) => void | - |
+| onClear | 清除上下文消息时候触发 | () => void | - |
+| onHintClick | 点击提示信息时触发 | (hint: string) => void | - |
+| onInputChange | 输入区域信息变化时触发 | (props: { value?: string, attachment?: FileItem[] }) => void; | - |
+| onMessageBadFeedback | 消息负向反馈时触发 | (message: Message) => void | - |
+| onMessageCopy | 复制消息时触发 | (message: Message) => void | - |
+| onMessageDelete | 删除消息时触发 | (message: Message) => void | - |
+| onMessageGoodFeedback | 消息正向反馈时触发 | (message: Message) => void | - |
+| onMessageReset | 重置消息时触发 | (message: Message) => void | - |
+| onMessageSend | 发送消息时触发 | (content: string, attachment?: FileItem[]) => void | - |
+| onStopGenerator | 点击停止生成按钮时触发 | (message: Message) => void | - |
+| placeholder | 输入框占位符 | string | - |
+| renderInputArea | 自定义渲染输入框 | (props: RenderInputAreaProps) => React.ReactNode | - |
+| showClearContext | 是否展示清除上下文按钮| boolean | false |
+| showStopGenerate | 是否展示停止生成按钮| boolean | false |
+| topSlot | 顶部插槽 | React.ReactNode | - |
+| uploadProps | 上传组件属性, 详情参考 [Upload](/zh-CN/input/upload#API%20%E5%8F%82%E8%80%83) | UploadProps | - |
+| uploadTipProps | 上传组件提示属性, 详情参考 [Tooltip](/zh-CN/show/tooltip#API%20%E5%8F%82%E8%80%83) | TooltipProps | - |
+
+
+#### RoleConfig
+
+| 属性 | 说明 | 类型 | 默认值 |
+|------|--------|-------|-------|
+| user | 用户信息 | Metadata | - |
+| assistant | 助手信息 | Metadata | - |
+| system | 系统信息 | Metadata | - |
+
+#### Metadata
+
+| 属性 | 说明 | 类型 | 默认值 |
+|------|--------|-------|-------|
+| name | 名称 | string | - |
+| avatar | 头像 | string | - |
+| color | 头像背景色,同 Avatar 组件的 color 参数, 支持 `amber`、 `blue`、 `cyan`、 `green`、 `grey`、 `indigo`、 `light-blue`、 `light-green`、 `lime`、 `orange`、 `pink`、 `purple`、 `red`、 `teal`、 `violet`、 `yellow` | string | `grey` |
+
+#### Message
+
+| 属性 | 说明 | 类型 | 默认值 |
+|------|--------|-------|-------|
+| role | 角色 | string | - |
+| name | 名称 | string | - |
+| id | 唯一标识 | string\| number | - |
+| content | 文本内容 | string| Content[] | - |
+| parentId | 父节点id | string | - |
+| createAt | 创建时间 | number | -|
+| status | 消息状态,可选值为 `loading` \| `incomplete` \| `complete` \| `error` | string | complete |
+
+
+#### Content
+
+| 属性 | 说明 | 类型 | 默认值 |
+|------|--------|-------|-------|
+| type | 类型, 可选值`text` \| `image_url` \| `file_url` | string | - |
+| text | 当类型为 `text` 时的内容数据 | string | - |
+| image_url | 当类型为 `image_url` 时的内容数据 | { url: string } | - |
+| file_url | 当类型为 `file_url` 时的内容数据 | { url: string; name: string; size: string; type: string } | - |
+
+#### Methods
+
+| 方法 | 说明 |
+|------|--------|
+| resetMessage | 重置消息 |
+| scrollToBottom(animation: boolean) | 滚动到最底部, animation 为 true,则有动画,反之无动画 |
+| clearContext | 清除上下文|
+| sendMessage(content: string, attachment: FileItem[]) |发送消息 |
+
+## 设计变量
+
+
+
diff --git a/content/plus/codehighlight/index-en-US.md b/content/plus/codehighlight/index-en-US.md
index 8480092ee1..1021fd2dad 100644
--- a/content/plus/codehighlight/index-en-US.md
+++ b/content/plus/codehighlight/index-en-US.md
@@ -3,7 +3,7 @@ localeCode: en-US
order: 0
category: Plus
title: CodeHighlight
-icon: doc-configprovider
+icon: doc-codehighlight
dir: column
brief: Highlight code blocks in the page according to syntax
---
diff --git a/content/plus/lottie/index-en-US.md b/content/plus/lottie/index-en-US.md
index d4ae2cb554..7aad6181fc 100644
--- a/content/plus/lottie/index-en-US.md
+++ b/content/plus/lottie/index-en-US.md
@@ -3,7 +3,7 @@ localeCode: en-US
order: 23
category: Plus
title: Lottie Animation
-icon: doc-configprovider
+icon: doc-lottie
dir: column
brief: Display Lottie animation on the web page
---
diff --git a/content/plus/markdownrender/index-en-US.md b/content/plus/markdownrender/index-en-US.md
index 7a8d3c9c93..a2ded01232 100644
--- a/content/plus/markdownrender/index-en-US.md
+++ b/content/plus/markdownrender/index-en-US.md
@@ -3,7 +3,7 @@ localeCode: en-US
order: 22
category: Plus
title: Markdown Render
-icon: doc-configprovider
+icon: doc-markdown
dir: column
brief: Instantly render Markdown and MDX in web pages
---
diff --git a/content/show/scrolllist/index-en-US.md b/content/show/scrolllist/index-en-US.md
index 53eef8ca4c..4561f03e88 100644
--- a/content/show/scrolllist/index-en-US.md
+++ b/content/show/scrolllist/index-en-US.md
@@ -18,7 +18,7 @@ import { ScrollList, ScrollItem } from '@douyinfe/semi-ui';
```
### Basic Usage
-The scrolling list provides a scrolling selection mode similar to the IOS operating system, while supporting scrolling to the specified window location selection and click selection.
+The scrolling list provides a scrolling selection mode similar to the iOS operating system, while supporting scrolling to the specified window location selection and click selection.
```jsx live=true
import React from 'react';
diff --git a/content/show/sidesheet/index-en-US.md b/content/show/sidesheet/index-en-US.md
index 8869439aff..4ca5e5cc27 100644
--- a/content/show/sidesheet/index-en-US.md
+++ b/content/show/sidesheet/index-en-US.md
@@ -254,7 +254,7 @@ class Demo extends React.Component {
initValue={'all'}
>
All
- IOS
+ iOS
Android
Web
diff --git a/content/show/sidesheet/index.md b/content/show/sidesheet/index.md
index d87e4bdd85..ff68aa82f7 100644
--- a/content/show/sidesheet/index.md
+++ b/content/show/sidesheet/index.md
@@ -256,7 +256,7 @@ class Demo extends React.Component {
/>
全平台
- IOS
+ iOS
Android
Web
diff --git a/content/start/changelog/index-en-US.md b/content/start/changelog/index-en-US.md
index 683e23d382..dc470f9f47 100644
--- a/content/start/changelog/index-en-US.md
+++ b/content/start/changelog/index-en-US.md
@@ -16,6 +16,16 @@ Version:Major.Minor.Patch (follow the **Semver** specification)
---
+#### 🎉 2.63.0-beta.0 (2024-07-22)
+- 【New Component】
+ - Added `Chat` component for rendering conversation list [#2248](https://github.com/DouyinFE/semi-design/pull/2248)
+- 【Feat】
+ - Form adds stopPropagation to prevent the issue of submit and reset events triggering in multiple levels of containers at the same time in nested Form scenarios [#2355](https://github.com/DouyinFE/semi-design/issues/2355)
+ - Upload support afterUpload return url modification preview link [#2346](https://github.com/DouyinFE/semi-design/pull/2346)
+- 【Fix】
+ - Fixed Form ArrayField addWithInitValue without scope isolation for imported parameter cloning [#2351](https://github.com/DouyinFE/semi-design/issues/2351)
+ - Fixed the problem that the width and height are constant when using renderThumbnail with the Image component in Upload [#2343](https://github.com/DouyinFE/semi-design/issues/2343)
+
#### 🎉 2.62.1 (2024-07-16)
- 【Fix】
- Fixed the issue that when TreeSelect enables showFilteredOnly and the search box is in the trigger, the treeSelect panel does not display correctly when it is opened again after searching [#2345](https://github.com/DouyinFE/semi-design/pull/2345)
@@ -32,9 +42,9 @@ Version:Major.Minor.Patch (follow the **Semver** specification)
#### 🎉 2.62.0-beta.0 (2024-07-05)
- 【New Component】
- - Added new verification code input component pinCode for quickly and conveniently entering verification codes [#2130 ](https://github.com/DouyinFE/semi-design/issues/2130)
- - Added Lottie component for convenient rendering of Lottie animations
- - Added CodeHighlight code highlighting component, used to highlight code displayed in web pages
+ - Added new verification code input component `pinCode` for quickly and conveniently entering verification codes [#2130 ](https://github.com/DouyinFE/semi-design/issues/2130)
+ - Added `Lottie` component for convenient rendering of Lottie animations
+ - Added `CodeHighlight` code highlighting component, used to highlight code displayed in web pages
- 【Feat】
- TreeSelect, Cascader supports closing the popup layer through the esc key
- 【Style】
diff --git a/content/start/changelog/index.md b/content/start/changelog/index.md
index f8dc9e5050..ee896f7a26 100644
--- a/content/start/changelog/index.md
+++ b/content/start/changelog/index.md
@@ -13,6 +13,17 @@ Semi 版本号遵循 **Semver** 规范(主版本号-次版本号-修订版本
- 修订版本号(patch):仅会进行 bugfix,发布时间不限
- 不同版本间的详细关系,可查阅 [FAQ](/zh-CN/start/faq)
+
+#### 🎉 2.63.0-beta.0 (2024-07-22)
+- 【New Component】
+ - 新增 Chat 组件用于渲染对话列表 [#2248](https://github.com/DouyinFE/semi-design/pull/2248)
+- 【Fix】
+ - 修复 Form ArrayField addWithInitValue 时未对入参 clone做作用域隔离的问题 [#2351](https://github.com/DouyinFE/semi-design/issues/2351)
+ - 修复 Upload 使用 renderThumbnail 搭配 Image 组件使用时,宽高度恒定的问题 [#2343](https://github.com/DouyinFE/semi-design/issues/2343)
+- 【Feat】
+ - Form 新增 stopPropagation 可用于阻止嵌套Form场景下,submit 、reset事件同时在多级容器触发的问题 [#2355](https://github.com/DouyinFE/semi-design/issues/2355)
+ - Upload 支持 afterUpload 中 return url 修改预览链接 [#2346](https://github.com/DouyinFE/semi-design/pull/2346)
+
#### 🎉 2.62.1 (2024-07-16)
- 【Fix】
- 修复 TreeSelect 启用 showFilteredOnly 并且搜索框在 trigger 中的 treeSelect 面板,在搜索后再次打开显示不正确问题 [#2345](https://github.com/DouyinFE/semi-design/pull/2345)
diff --git a/content/start/overview/index-en-US.md b/content/start/overview/index-en-US.md
index a6fb4e6dae..5f233d4907 100644
--- a/content/start/overview/index-en-US.md
+++ b/content/start/overview/index-en-US.md
@@ -22,7 +22,8 @@ Typography
```overview
CodeHighlight,
Markdown,
-Lottie
+Lottie,
+Chat
```
diff --git a/content/start/overview/index.md b/content/start/overview/index.md
index b80da9c380..f450e7b377 100644
--- a/content/start/overview/index.md
+++ b/content/start/overview/index.md
@@ -23,7 +23,8 @@ Typography 版式
```overview
CodeHighlight 代码高亮,
Markdown 渲染器,
-Lottie 动画
+Lottie 动画,
+Chat 聊天
```
## 输入类
diff --git a/lerna.json b/lerna.json
index 088aec30f6..bea7835e86 100644
--- a/lerna.json
+++ b/lerna.json
@@ -1,5 +1,5 @@
{
"useWorkspaces": true,
"npmClient": "yarn",
- "version": "2.62.1"
+ "version": "2.63.0-beta.0"
}
\ No newline at end of file
diff --git a/package.json b/package.json
index 192349a3b3..5c3c642e65 100644
--- a/package.json
+++ b/package.json
@@ -50,7 +50,7 @@
"@douyinfe/semi-site-banner": "^0.1.5",
"@douyinfe/semi-site-doc-style": "0.0.4",
"@douyinfe/semi-site-header": "^0.0.29",
- "@douyinfe/semi-site-markdown-blocks": "^0.0.17",
+ "@douyinfe/semi-site-markdown-blocks": "^0.0.18",
"@mdx-js/mdx": "1.6.22",
"@mdx-js/react": "^1.6.22",
"@storybook/react-webpack5": "^7.0.7",
diff --git a/packages/semi-animation-react/package.json b/packages/semi-animation-react/package.json
index b6c175ecab..5587ae231c 100644
--- a/packages/semi-animation-react/package.json
+++ b/packages/semi-animation-react/package.json
@@ -1,6 +1,6 @@
{
"name": "@douyinfe/semi-animation-react",
- "version": "2.62.1",
+ "version": "2.63.0-beta.0",
"description": "motion library for semi-ui-react",
"keywords": [
"motion",
@@ -25,8 +25,8 @@
"prepublishOnly": "npm run build:lib"
},
"dependencies": {
- "@douyinfe/semi-animation": "2.62.1",
- "@douyinfe/semi-animation-styled": "2.62.1",
+ "@douyinfe/semi-animation": "2.63.0-beta.0",
+ "@douyinfe/semi-animation-styled": "2.63.0-beta.0",
"classnames": "^2.2.6"
},
"devDependencies": {
diff --git a/packages/semi-animation-styled/package.json b/packages/semi-animation-styled/package.json
index 2f941e0d74..fbfa2cc35c 100644
--- a/packages/semi-animation-styled/package.json
+++ b/packages/semi-animation-styled/package.json
@@ -1,6 +1,6 @@
{
"name": "@douyinfe/semi-animation-styled",
- "version": "2.62.1",
+ "version": "2.63.0-beta.0",
"description": "semi styled animation",
"keywords": [
"semi",
diff --git a/packages/semi-animation/package.json b/packages/semi-animation/package.json
index dfb049ca9b..41cc480f70 100644
--- a/packages/semi-animation/package.json
+++ b/packages/semi-animation/package.json
@@ -1,6 +1,6 @@
{
"name": "@douyinfe/semi-animation",
- "version": "2.62.1",
+ "version": "2.63.0-beta.0",
"description": "animation base library for semi-ui",
"keywords": [
"animation",
diff --git a/packages/semi-eslint-plugin/package.json b/packages/semi-eslint-plugin/package.json
index bd336f01e1..9729cad455 100644
--- a/packages/semi-eslint-plugin/package.json
+++ b/packages/semi-eslint-plugin/package.json
@@ -1,6 +1,6 @@
{
"name": "eslint-plugin-semi-design",
- "version": "2.62.1",
+ "version": "2.63.0-beta.0",
"description": "semi ui eslint plugin",
"keywords": [
"semi",
diff --git a/packages/semi-foundation/chat/chat.scss b/packages/semi-foundation/chat/chat.scss
new file mode 100644
index 0000000000..721f54e0ba
--- /dev/null
+++ b/packages/semi-foundation/chat/chat.scss
@@ -0,0 +1,598 @@
+@import './variables.scss';
+
+$module: #{$prefix}-chat;
+
+
+@mixin loading-circle-common() {
+ border-radius: 50%;
+ height: $width-chat_chatBox_loading;
+ width: $width-chat_chatBox_loading;
+ background-color: $color-chat_chatBox_loading-bg;
+}
+
+.#{$module} {
+ padding-top: $spacing-chat_paddingY;
+ padding-bottom: $spacing-chat_paddingY;
+ display: flex;
+ flex-direction: column;
+ height: 100%;
+ max-width: $width-chat_max;
+ position: relative;
+ overflow: hidden;
+
+ &-inner {
+ display: flex;
+ flex-direction: column;
+ height: 100%;
+ }
+
+ &-dropArea {
+ position: absolute;
+ top: 0;
+ left: 0;
+ right: 0;
+ bottom: 0;
+ background: $color-chat_dropArea-bg;
+ z-index: $z-chat_dropArea;
+ border: $width-chat_dropArea-border dotted $color-chat_dropArea-border;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ border-radius: $radius-chat_dropArea;
+
+ &-text {
+ font-size: $font-chat_dropArea_text;
+ }
+ }
+
+ &-content {
+ overflow: hidden;
+ flex: 1 1;
+ position: relative;
+ }
+
+ &-toast {
+ position: absolute;
+ top: 0;
+ left: 50%;
+ transform: translateX(-50%);
+ }
+
+ &-container {
+ padding-left: $spacing-chat_container-paddingX;
+ padding-right: $spacing-chat_container-paddingX;
+ height: 100%;
+ overflow: scroll;
+
+ &-scroll-hidden {
+ &::-webkit-scrollbar {
+ display: none;
+ }
+ }
+
+ }
+
+ &-action {
+ position: relative;
+ z-index: $z-chat_action;
+
+ &-content.#{$prefix}-button {
+ position: absolute;
+ bottom: $spacing-chat_action_content-bottom;
+ left: 50%;
+ transform: translateX(-50%);
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ background: $color-chat_action_content-bg;
+ border: $width-chat_action_content-border solid $color-chat_action_content-border;
+ }
+
+ &-content.#{$prefix}-button-light:not(.#{$prefix}-button-disabled):hover {
+ background: $color-chat_action_content-bg-hover;
+ border: $width-chat_action_content-border solid $color-chat_action_content-border;
+ }
+
+ &-backBottom.#{$prefix}-button {
+ width: $width-chat_backBottom_wrapper;
+ height: $width-chat_backBottom_wrapper;
+ border-radius: 50%;
+ }
+
+ &-stop.#{$prefix}-button {
+ user-select: none;
+ height: $height-chat_action_stop;
+ border-radius: calc($height-chat_action_stop / 2);
+ }
+
+ }
+
+ &-divider {
+ color: $color-chat_divider;
+ font-size: $font-chat_divider-fontSize;
+ margin-top: $spacing-chat_divider-marginY;
+ margin-bottom: $spacing-chat_divider-marginY;
+ font-weight: $font-chat_divider-fontWeight;
+ }
+
+ &-chatBox {
+ display: flex;
+ flex-direction: row;
+ margin-top: $spacing-chat_chatBox-marginY;
+ margin-Bottom: $spacing-chat_chatBox-marginY;
+ column-gap: $spacing-chat_chatBox-columnGap;
+
+ &:hover {
+ .#{$module}-chatBox-action:not(.#{$module}-chatBox-action-hidden) {
+ visibility: visible;
+ }
+ }
+
+ &-right {
+ flex-direction: row-reverse;
+
+ .#{$module}-chatBox-wrap {
+ align-items: end;
+ }
+ }
+
+ &-avatar {
+ flex-shrink: 0;
+
+ &-hidden {
+ visibility: hidden;
+ }
+ }
+
+ &-title {
+ line-height: $font-chat_chatBox_title-lineHeight;
+ font-size: $font-chat_chatBox_title-fontSize;
+ color: $color-chat_chatBox_title;
+ font-weight: $font-chat_chatBox_title-fontWeight;
+ text-overflow: ellipsis;
+ }
+
+ &-action {
+ visibility: hidden;
+ display: flex;
+ align-items: center;
+ position: relative;
+ column-gap: $spacing-chat_chatBox_action-columnGap;
+ margin-left: $spacing-chat_chatBox_action-marginX;
+ margin-right: $spacing-chat_chatBox_action-marginX;
+
+ &-btn {
+ &.#{$prefix}-button {
+ height: fit-content;
+ }
+
+ &.#{$prefix}-button.#{$prefix}-button-with-icon-only {
+ padding: $spacing-chat_chatBox_action_btn-padding;
+ }
+ }
+
+ &-icon-flip {
+ transform: scaleY(-1);
+ }
+
+ &-show {
+ visibility: visible;
+ }
+
+ &-delete-wrap {
+ display: inline-flex;
+ }
+
+ &.#{$module}-chatBox-action-hidden, &:hover.#{$module}-chatBox-action-hidden {
+ visibility: hidden;
+ }
+
+ .#{$prefix}-button-borderless:not(.#{$prefix}-button-disabled):hover {
+ background-color: $color-chat_chatBox_action-bg-hover;
+ }
+
+ .#{$prefix}-button-tertiary.#{$prefix}-button-borderless {
+ color: $color-chat_chatBox_action_icon;
+
+ &:hover {
+ color: $color-chat_chatBox_action-icon-hover;
+ }
+ }
+ }
+
+
+ &-wrap {
+ display: flex;
+ flex-direction: column;
+ align-items: start;
+ position: relative;
+ row-gap: $spacing-chat_chatBox_wrap;
+ }
+
+
+ &-content {
+
+ &-bubble, &-userBubble {
+ padding: $spacing-chat_chatBox_content-paddingY $spacing-chat_chatBox_content-paddingX;
+ border-radius: $radius-chat_chatBox_content;
+ background-color: $color-chat_chatBox_content_bg;
+ }
+
+ code {
+ white-space: pre-wrap;
+ }
+
+ .#{$prefix}-typography {
+ color: $color-chat_chatBox_content_text;
+ }
+
+ .#{$module}-attachment-file {
+ background: $color-chat_chatBox_other_attachment_file-bg;
+ }
+
+ .#{$module}-attachment-file, .#{$module}-attachment-img {
+ margin-top: $spacing-chat_chatBox_content_attachment-marginY;
+ margin-bottom: $spacing-chat_chatBox_content_attachment-marginY;
+ }
+
+ &-user {
+ background: $color-chat_chatBox_content_user-bg;
+ color: $color-chat_chatBox_content_user-text;
+
+ .#{$module}-attachment-file {
+ background: $color-chat_chatBox_user_attachment_file-bg;
+ }
+
+ .#{$prefix}-typography, .#{$prefix}-typography code {
+ color: $color-chat_chatBox_content_user-text;
+ }
+
+ .#{$prefix}-markdownRender ul, .#{$prefix}-markdownRender li {
+ color: $color-chat_chatBox_content_user-text;
+ }
+
+ .#{$prefix}-typography a {
+ &, &:visited, &:hover {
+ color: $color-chat_chatBox_content_user-text;
+ }
+ }
+
+ }
+
+ &-error {
+ background: $color-chat_chatBox_content_error-bg;
+ .#{$prefix}-typography {
+ color: $color-chat_chatBox_content_error-text;
+ }
+ }
+
+ &-loading {
+ display: flex;
+ align-items: baseline;
+
+ &-item {
+ @include loading-circle-common();
+ margin: $spacing-chat_chatBox_loading-item-marginY $spacing-chat_chatBox_loading-item-marginX;
+
+ overflow: visible;
+ position: relative;
+
+ animation: #{$module}-loading-flashing .8s infinite alternate;
+ animation-delay: -0.2s;
+ animation-timing-function: ease;
+
+
+ &::before {
+ content: '';
+ @include loading-circle-common();
+
+ position: absolute;
+ top: 0;
+ left: -$spacing-chat_chatBox_loading_item-gap;
+
+ animation: #{$module}-loading-flashing .8s infinite alternate;
+ animation-timing-function: ease;
+ animation-delay: -0.4s;
+ }
+
+ &::after {
+ content: '';
+ @include loading-circle-common();
+ position: absolute;
+ top: 0;
+ left: $spacing-chat_chatBox_loading_item-gap;
+
+ animation: #{$module}-loading-flashing .8s infinite alternate;
+ animation-delay: 0s;
+ animation-timing-function: ease;
+ }
+ }
+ }
+
+ pre {
+ background-color: transparent;
+ }
+
+ &-code {
+ border-radius: $radius-chat_chatBox_content_code;
+ overflow: hidden;
+
+ & .#{$prefix}-codeHighlight pre {
+ word-break: break-all;
+ white-space: pre-wrap;
+ }
+
+ &-topSlot {
+ display: flex;
+ justify-content: space-between;
+ background-color: $color-chat_chatBox_code_topSlot-bg;
+ align-items: center;
+ padding: $spacing-chat_chatBox_content_code_topSlot-paddingX $spacing-chat_chatBox_content_code_topSlot-paddingY;
+ color: $color-chat_chatBox_code_topSlot;
+ font-size: $font-chat_chatBox_code_topSlot;
+
+ &-copy {
+ min-width: $width-chat_chatBox_content_code_topSlot_copy;
+ display: flex;
+ justify-content: flex-end;
+
+ &-wrapper {
+ display: flex;
+ align-items: center;
+ column-gap: $spacing-chat_chatBox_content_code_topSlot_copy-columnGap;
+ cursor: pointer;
+ background: transparent;
+ border: none;
+ color: $color-chat_chatBox_code_topSlot;
+ line-height: $font-chat_chatBox_code_topSlot-lineHeight;
+ padding: $spacing-chat_chatBox_content_code_topSlot_copy-padding;
+ border-radius: $radius-chat_chatBox_content_code_topSlot_copy;
+ }
+
+ }
+
+ &-toCopy {
+ &:hover {
+ background: $color-chat_chatBox_code_topSlot_toCopy-bg-hover;
+ }
+ }
+
+ }
+
+ .semi-codeHighlight-defaultTheme pre[class*=language-] {
+ margin: 0px;
+ background: $color-chat_chatBox_code_content;
+ }
+ }
+ }
+ }
+
+ &-inputBox {
+ padding-left: $spacing-chat_inputBox-paddingX;
+ padding-right: $spacing-chat_inputBox-paddingX;
+ padding-top: $spacing-chat_inputBox-paddingTop;
+ padding-bottom: $spacing-chat_inputBox-paddingBottom;
+
+ &-clearButton.#{$prefix}-button {
+ border-radius: 50%;
+ width: $width-chat_inputBottom_clearButton;
+ height: $width-chat_inputBottom_clearButton;
+ margin-top: $spacing-chat_inputBox-marginY;
+ margin-bottom: $spacing-chat_inputBox-marginY;
+
+ .#{$prefix}-icon {
+ font-size: $font-chat_inputBottom_clearButton_icon-fontSize;
+ }
+
+ &.#{$prefix}-button-primary.#{$prefix}-button-borderless {
+ color: $color-chat_inputBottom_clearButton_icon;
+ }
+
+ }
+
+ &-upload {
+ .#{$prefix}-upload-file-list {
+ display: none;
+ }
+ }
+
+ &-uploadButton.#{$prefix}-button {
+ width: $width-chat_inputBottom_uploadButton;
+ height: $width-chat_inputBottom_uploadButton;
+ &.#{$prefix}-button-primary.#{$prefix}-button-borderless {
+ color: $color-chat_inputBottom_uploadButton_icon;
+ }
+ }
+
+ &-sendButton.#{$prefix}-button{
+ width: $width-chat_inputBottom_sendButton;
+ height: $width-chat_inputBottom_sendButton;
+ &-icon {
+ transform: rotate(45deg);
+ }
+
+ &.#{$prefix}-button-disabled.#{$prefix}-button-borderless {
+ color: $color-chat_inputBottom_sendButton_icon-disable;
+ }
+ }
+
+ &-inner {
+ display: flex;
+ flex-direction: row;
+ align-items: flex-end;
+ column-gap: $spacing-chat_inputBox_inner-columnGap;
+ }
+
+ &-container {
+ display: flex;
+ flex-direction: row;
+ flex-grow: 1;
+ border-radius: $radius-chat_inputBox_container;
+ padding: $spacing-chat_inputBox_container-padding;
+ border: $width-chat_inputBox_container-border solid $color-chat_inputBox_container-border;
+ align-items: end;
+ }
+
+ &-inputArea {
+ flex-grow: 1;
+ display: flex;
+ flex-direction: column;
+ }
+
+ &-textarea {
+ flex-grow: 1;
+
+ &.#{$prefix}-input-textarea-wrapper {
+ &, &:hover, &:active {
+ border: none;
+ background-color: transparent;
+ }
+ }
+ }
+ }
+
+ &-attachment {
+ display: flex;
+ flex-direction: row;
+ flex-wrap: wrap;
+ column-gap: $spacing-chat_attachment-columnGap;
+ row-gap: $spacing-chat_attachment-RowGap;
+
+ &-item {
+ position: relative;
+
+ &:hover {
+ .#{$module}-inputBox-attachment-clear {
+ visibility: visible;
+ }
+ }
+ }
+
+ &-img {
+ border-radius: $radius-chat_attachment_img;
+ vertical-align: top;
+ }
+
+ a {
+ text-decoration: none;
+ color: inherit;
+ }
+
+ &-clear {
+ position: absolute;
+ top: -1 * $spacing-chat_attachment_clear-top;
+ right: -1 * $spacing-chat_attachment_clear-right;
+ color: $color-chat_attachment_clear_icon;
+ }
+
+ &-process.#{$prefix}-progress-circle {
+ position: absolute;
+ top: 50%;
+ left: 50%;
+ transform: translate(-50%, -50%);
+ }
+
+ &-file {
+ display: flex;
+ flex-direction: row;
+ align-items: center;
+ height: $width-chat_attachment_file;
+ column-gap: $spacing-chat_attachment_file-columnGap;
+ padding: $spacing-chat_attachment_file-padding;
+ border-radius: $radius-chat_attachment_file;
+ background: $color-chat_attachment_file-bg;
+ text-decoration: none;
+
+ &-icon {
+ color: $color-chat_attachment_file_icon;
+ }
+
+ &-info {
+ display: flex;
+ flex-direction: column;
+ }
+
+ &-title {
+ font-size: $font-chat_attachment_file_title-fontSize;
+ color: $color-chat_attachment_file_title;
+ max-width: $width-chat_attachment_file_title;
+ text-overflow: ellipsis;
+ overflow: hidden;
+ }
+
+ &-metadata {
+ font-size: $font-chat_attachment_file_metadata-fontSize;
+ color: $color-chat_attachment_file_metadata_text;
+ }
+
+ &-type {
+ text-transform: uppercase;
+ }
+ }
+
+ }
+
+ .#{$prefix}-typography a.#{$module}-attachment-file {
+ display: flex;
+
+ .#{$module}-attachment-file-title {
+ color: $color-chat_attachment_file_title;
+ }
+
+ }
+
+ &-hints {
+ display: flex;
+ flex-direction: column;
+ row-gap: $spacing-chat_hint-rowGap;
+ margin-top: $spacing-chat_hint-marginY;
+ margin-bottom: $spacing-chat_hint-marginY;
+ margin-left: $spacing-chat_hint-marginLeft;
+ }
+
+ &-hint {
+ &-item {
+ cursor: pointer;
+ display: flex;
+ flex-direction: row;
+ column-gap: $spacing-chat_hint_item-columnGap;
+ width: fit-content;
+ // justify-content: space-between;
+ background: $color-chat_hint_item-bg;
+ align-items: center;
+ border: $width-chat_hint_item-border solid $color-chat_hint_item-border;
+ padding: $spacing-chat_hint_item-marginY $spacing-chat_hint_item-marginX;
+ border-radius: $radius-chat_hint_item;
+
+ &:hover {
+ background-color: $color-chat_hint_item-bg-hover;
+ }
+ }
+
+ &-content {
+ font-size: $font-chat_hint_content-fontSize;
+ color: $color-chat_hint_content_text;
+ }
+
+ &-icon {
+ // font-size: $font-chat_hint_icon;
+ color: $color-chat_hint_icon;
+ }
+
+ }
+}
+
+@keyframes #{$module}-loading-flashing {
+ 0% {
+ opacity: 1;;
+ }
+ 50% {
+ opacity: 0.1;
+ }
+ to {
+ opacity: 1;
+ }
+}
+
+
+@import './rtl.scss';
\ No newline at end of file
diff --git a/packages/semi-foundation/chat/chatBoxActionFoundation.ts b/packages/semi-foundation/chat/chatBoxActionFoundation.ts
new file mode 100644
index 0000000000..c6a626fd38
--- /dev/null
+++ b/packages/semi-foundation/chat/chatBoxActionFoundation.ts
@@ -0,0 +1,64 @@
+import BaseFoundation, { DefaultAdapter } from "../base/foundation";
+
+export interface ChatBoxActionAdapter, S = Record> extends DefaultAdapter {
+ notifyDeleteMessage: () => void;
+ notifyMessageCopy: () => void;
+ copyToClipboardAndToast: () => void;
+ notifyLikeMessage: () => void;
+ notifyDislikeMessage: () => void;
+ notifyResetMessage: () => void;
+ setVisible: (visible: boolean) => void;
+ setShowAction: (showAction: boolean) => void;
+ registerClickOutsideHandler(...args: any[]): void;
+ unregisterClickOutsideHandler(...args: any[]): void
+}
+
+export default class ChatBoxActionFoundation
, S = Record> extends BaseFoundation, P, S> {
+ constructor(adapter: ChatBoxActionAdapter) {
+ super({ ...adapter });
+ }
+
+ showDeletePopup = () => {
+ this._adapter.setVisible(true);
+ this._adapter.setShowAction(true);
+ this._adapter.registerClickOutsideHandler(this.hideDeletePopup);
+ }
+
+ hideDeletePopup = () => {
+ /** visible 控制 popConfirm 的显隐
+ * showAction 控制在 popConfirm 显示时候,保证操作区显示
+ * 需要有时间间隔,用 visible 直接控制的话,在 popconfirm 通过取消按钮关闭时会导致操作区显示闪动
+ */
+ this._adapter.setVisible(false);
+ setTimeout(() => {
+ this._adapter.setShowAction(false);
+ }, 150);
+ this._adapter.unregisterClickOutsideHandler();
+ }
+
+ destroy = () => {
+ this._adapter.unregisterClickOutsideHandler();
+ }
+
+ deleteMessage = () => {
+ this._adapter.notifyDeleteMessage();
+ }
+
+ copyMessage = () => {
+ this._adapter.notifyMessageCopy();
+ this._adapter.copyToClipboardAndToast();
+ }
+
+ likeMessage = () => {
+ this._adapter.notifyLikeMessage();
+ }
+
+ dislikeMessage = () => {
+ this._adapter.notifyDislikeMessage();
+ }
+
+ resetMessage = () => {
+ this._adapter.notifyResetMessage();
+ }
+
+}
\ No newline at end of file
diff --git a/packages/semi-foundation/chat/constants.ts b/packages/semi-foundation/chat/constants.ts
new file mode 100644
index 0000000000..83ff70a93e
--- /dev/null
+++ b/packages/semi-foundation/chat/constants.ts
@@ -0,0 +1,68 @@
+import {
+ BASE_CLASS_PREFIX
+} from '../base/constants';
+
+const cssClasses = {
+ PREFIX: `${BASE_CLASS_PREFIX}-chat`,
+ PREFIX_DIVIDER: `${BASE_CLASS_PREFIX}-chat-divider`,
+ PREFIX_CHAT_BOX: `${BASE_CLASS_PREFIX}-chat-chatBox`,
+ PREFIX_CHAT_BOX_ACTION: `${BASE_CLASS_PREFIX}-chat-chatBox-action`,
+ PREFIX_INPUT_BOX: `${BASE_CLASS_PREFIX}-chat-inputBox`,
+ PREFIX_ATTACHMENT: `${BASE_CLASS_PREFIX}-chat-attachment`,
+ PREFIX_HINT: `${BASE_CLASS_PREFIX}-chat-hint`,
+};
+
+const ROLE = {
+ USER: 'user',
+ ASSISTANT: 'assistant',
+ SYSTEM: 'system',
+ DIVIDER: 'divider',
+};
+
+const CHAT_ALIGN = {
+ LEFT_RIGHT: 'leftRight',
+ LEFT_ALIGN: 'leftAlign',
+};
+
+const MESSAGE_STATUS = {
+ LOADING: 'loading',
+ INCOMPLETE: 'incomplete',
+ COMPLETE: 'complete',
+ ERROR: 'error'
+};
+
+const PIC_SUFFIX_ARRAY = ['png', 'jpg', 'jpeg', 'gif', 'bmp', 'webp'];
+
+const PIC_PREFIX = 'image/';
+
+const SCROLL_ANIMATION_TIME = 300;
+const SHOW_SCROLL_GAP = 100;
+
+const MODE = {
+ BUBBLE: 'bubble',
+ NO_BUBBLE: 'noBubble',
+ USER_BUBBLE: 'userBubble'
+};
+
+const SEND_HOT_KEY = {
+ ENTER: 'enter',
+ SHIFT_PLUS_ENTER: 'shift+enter'
+};
+
+const strings = {
+ ROLE,
+ CHAT_ALIGN,
+ MESSAGE_STATUS,
+ PIC_SUFFIX_ARRAY,
+ PIC_PREFIX,
+ SCROLL_ANIMATION_TIME,
+ SHOW_SCROLL_GAP,
+ MODE,
+ SEND_HOT_KEY,
+};
+
+
+export {
+ cssClasses,
+ strings,
+};
\ No newline at end of file
diff --git a/packages/semi-foundation/chat/foundation.ts b/packages/semi-foundation/chat/foundation.ts
new file mode 100644
index 0000000000..a96ef97047
--- /dev/null
+++ b/packages/semi-foundation/chat/foundation.ts
@@ -0,0 +1,306 @@
+import BaseFoundation, { DefaultAdapter } from "../base/foundation";
+import { strings } from "./constants";
+import { Animation } from '@douyinfe/semi-animation';
+import { debounce } from "lodash";
+import { getUuidv4 } from "../utils/uuid";
+import { handlePrevent } from "../utils/a11y";
+
+const { PIC_PREFIX, PIC_SUFFIX_ARRAY, ROLE,
+ SCROLL_ANIMATION_TIME, SHOW_SCROLL_GAP
+} = strings;
+
+export interface Content {
+ type: 'text' | 'image_url' | 'file_url';
+ text?: string;
+ image_url?: {
+ url: string;
+ [x: string]: any
+ };
+ file_url?: {
+ url: string;
+ name: string;
+ size: string;
+ type: string;
+ [x: string]: any
+ }
+}
+
+export interface Message {
+ role?: string;
+ name?: string;
+ id?: string;
+ content?: string | Content[];
+ parentId?: string;
+ createAt?: number;
+ status?: 'loading' | 'incomplete' | 'complete' | 'error';
+ [x: string]: any
+}
+
+export interface ChatAdapter
, S = Record> extends DefaultAdapter {
+ getContainerRef: () => React.RefObject;
+ setWheelScroll: (flag: boolean) => void;
+ notifyChatsChange: (chats: Message[]) => void;
+ notifyLikeMessage: (message: Message) => void;
+ notifyDislikeMessage: (message: Message) => void;
+ notifyCopyMessage: (message: Message) => void;
+ notifyClearContext: () => void;
+ notifyMessageSend: (content: string, attachment: any[]) => void;
+ notifyInputChange: (props: { inputValue: string; attachment: any[]}) => void;
+ setBackBottomVisible: (visible: boolean) => void;
+ registerWheelEvent: () => void;
+ unRegisterWheelEvent: () => void;
+ notifyStopGenerate: (e: any) => void;
+ notifyHintClick: (hint: string) => void;
+ setUploadAreaVisible: (visible: boolean) => void;
+ manualUpload: (e: any) => void;
+ getDropAreaElement: () => HTMLDivElement
+}
+
+
+export default class ChatFoundation , S = Record> extends BaseFoundation, P, S> {
+
+ animation: any;
+
+ constructor(adapter: ChatAdapter) {
+ super({ ...adapter });
+ }
+
+ init = () => {
+ this.scrollToBottomImmediately();
+ this._adapter.registerWheelEvent();
+ }
+
+ destroy = () => {
+ this.animation && this.animation.destroy();
+ this._adapter.unRegisterWheelEvent();
+ }
+
+ stopGenerate = (e: any) => {
+ this._adapter.notifyStopGenerate(e);
+ }
+
+ scrollToBottomImmediately = () => {
+ const containerRef = this._adapter.getContainerRef();
+ const element = containerRef?.current;
+ if (element) {
+ element.scrollTop = element.scrollHeight;
+ }
+ }
+
+ scrollToBottomWithAnimation = () => {
+ const duration = SCROLL_ANIMATION_TIME;
+ const containerRef = this._adapter.getContainerRef();
+ const element = containerRef?.current;
+ if (!element) {
+ return;
+ }
+ const from = element.scrollTop;
+ const to = element.scrollHeight;
+ this.animation = new Animation(
+ {
+ from: { scrollTop: from },
+ to: { scrollTop: to },
+ },
+ {
+ duration,
+ easing: 'easeInOutCubic'
+ }
+ );
+
+ this.animation.on('frame', ({ scrollTop }: { scrollTop: number }) => {
+ element.scrollTop = scrollTop;
+ });
+
+ this.animation.start();
+ }
+
+ containerScroll = (e: any) => {
+ if (e.target !== e.currentTarget) {
+ return;
+ }
+ e.persist();
+ const update = () => {
+ this.getScroll(e.target);
+ };
+ requestAnimationFrame(update);
+ }
+
+ getScroll = debounce((target: any) => {
+ const scrollHeight = target.scrollHeight;
+ const clientHeight = target.clientHeight;
+ const scrollTop = target.scrollTop;
+ const { backBottomVisible } = this.getStates();
+ if (scrollHeight - scrollTop - clientHeight <= SHOW_SCROLL_GAP) {
+ if (backBottomVisible) {
+ this._adapter.setBackBottomVisible(false);
+ }
+ } else {
+ if (!backBottomVisible) {
+ this._adapter.setBackBottomVisible(true);
+ }
+ }
+ return scroll;
+ }, 100)
+
+ clearContext = (e: any) => {
+ const { chats } = this.getStates();
+ if (chats[chats.length - 1].role === ROLE.DIVIDER) {
+ return;
+ }
+ const dividerMessage = {
+ role: ROLE.DIVIDER,
+ id: getUuidv4(),
+ createAt: Date.now(),
+ };
+ const newChats = [...chats, dividerMessage];
+ this._adapter.notifyChatsChange(newChats);
+ this._adapter.notifyClearContext();
+ }
+
+ onMessageSend = (input: string, attachment: any[]) => {
+ let content;
+ if (Boolean(attachment) && attachment.length === 0) {
+ content = input;
+ } else {
+ content = [];
+ input && content.push({ type: 'text', text: input });
+ (attachment ?? []).map(item => {
+ const { fileInstance, name = '', url, size } = item;
+ const suffix = name.split('.').pop();
+ const isImg = fileInstance?.type?.startsWith(PIC_PREFIX) || PIC_SUFFIX_ARRAY.includes(suffix);
+ if (isImg) {
+ content.push({
+ type: 'image_url',
+ image_url: { url: url }
+ });
+ } else {
+ content.push({
+ type: 'file_url',
+ file_url: {
+ url: url,
+ name: name,
+ size: size,
+ type: fileInstance?.type
+ }
+ });
+ }
+ });
+ }
+ if (content) {
+ const newMessage = {
+ role: ROLE.USER,
+ id: getUuidv4(),
+ createAt: Date.now(),
+ content,
+ };
+ this._adapter.notifyChatsChange([...this.getStates().chats, newMessage]);
+ }
+ this._adapter.setWheelScroll(false);
+ this._adapter.registerWheelEvent();
+ this._adapter.notifyMessageSend(input, attachment);
+ }
+
+ onHintClick = (hint: string) => {
+ const { chats } = this.getStates();
+ const newMessage = {
+ role: ROLE.USER,
+ id: getUuidv4(),
+ createAt: Date.now(),
+ content: hint,
+ };
+ const newChats = [...chats, newMessage];
+ this._adapter.notifyChatsChange(newChats);
+ this._adapter.notifyHintClick(hint);
+ }
+
+ onInputChange = (props: { inputValue: string; attachment: any[]}) => {
+ this._adapter.notifyInputChange(props as any);
+ }
+
+ deleteMessage = (message: Message) => {
+ const { onMessageDelete, onChatsChange } = this.getProps();
+ const { chats } = this.getStates();
+ onMessageDelete?.(message);
+ const newChats = chats.filter(item => item.id !== message.id);
+ onChatsChange?.(newChats);
+ }
+
+ likeMessage = (message: Message) => {
+ const { chats } = this.getStates();
+ this._adapter.notifyLikeMessage(message);
+ const index = chats.findIndex(item => item.id === message.id);
+ const newChat = {
+ ...chats[index],
+ like: !chats[index].like,
+ dislike: false,
+ };
+ const newChats = [...chats];
+ newChats.splice(index, 1, newChat);
+ this._adapter.notifyChatsChange(newChats);
+ }
+
+ dislikeMessage = (message: Message) => {
+ const { chats } = this.getStates();
+ this._adapter.notifyDislikeMessage(message);
+ const index = chats.findIndex(item => item.id === message.id);
+ const newChat = {
+ ...chats[index],
+ like: false,
+ dislike: !chats[index].dislike,
+ };
+ const newChats = [...chats];
+ newChats.splice(index, 1, newChat);
+ this._adapter.notifyChatsChange(newChats);
+ }
+
+ resetMessage = (message: Message) => {
+ const { chats } = this.getStates();
+ const lastMessage = chats[chats.length - 1];
+ const newLastChat = {
+ ...lastMessage,
+ status: 'loading',
+ content: '',
+ id: getUuidv4(),
+ createAt: Date.now(),
+ };
+ const newChats = chats.slice(0, -1).concat(newLastChat);
+ this._adapter.notifyChatsChange(newChats);
+ const { onMessageReset } = this.getProps();
+ onMessageReset?.(message);
+ }
+
+ handleDragOver = (e: any) => {
+ this._adapter.setUploadAreaVisible(true);
+ }
+
+ handleContainerDragOver = (e: any) => {
+ handlePrevent(e);
+ }
+
+ handleContainerDrop = (e) => {
+ this._adapter.setUploadAreaVisible(false);
+ this._adapter.manualUpload(e?.dataTransfer?.files);
+ // 禁用默认实现,防止文件被打开
+ //Disable the default implementation, preventing files from being opened
+ handlePrevent(e);
+ }
+
+ handleContainerDragLeave = (e: any) => {
+ handlePrevent(e);
+ // 鼠标移动至 container 的子元素,则不做任何操作
+ // If the mouse moves to the child element of container, no operation will be performed.
+ const dropAreaElement = this._adapter.getDropAreaElement();
+ if (dropAreaElement !== e.target && dropAreaElement.contains(e.target)) {
+ return;
+ }
+ /**
+ * 延迟隐藏 container ,防止父元素的 mouseOver 被触发,导致 container 无法隐藏
+ * Delay hiding of the container to prevent the parent element's mouseOver from being triggered,
+ * causing the container to be unable to be hidden.
+ */
+ setTimeout(() => {
+ this._adapter.setUploadAreaVisible(false);
+ });
+ }
+}
+
diff --git a/packages/semi-foundation/chat/inputboxFoundation.ts b/packages/semi-foundation/chat/inputboxFoundation.ts
new file mode 100644
index 0000000000..567b0b671e
--- /dev/null
+++ b/packages/semi-foundation/chat/inputboxFoundation.ts
@@ -0,0 +1,98 @@
+import { handlePrevent } from "../utils/a11y";
+import BaseFoundation, { DefaultAdapter } from "../base/foundation";
+import { strings } from './constants';
+
+const { SEND_HOT_KEY } = strings;
+
+export interface InputBoxAdapter
, S = Record> extends DefaultAdapter {
+ notifyInputChange: (props: { inputValue: string; attachment: any[]}) => void;
+ setInputValue: (value: string) => void;
+ setAttachment: (attachment: any[]) => void;
+ notifySend: (content: string, attachment: any[]) => void
+}
+
+export default class InputBoxFoundation
, S = Record> extends BaseFoundation, P, S> {
+ constructor(adapter: InputBoxAdapter) {
+ super({ ...adapter });
+ }
+
+ onInputAreaChange = (value: string) => {
+ const attachment = this.getState('attachment');
+ this._adapter.setInputValue(value);
+ this._adapter.notifyInputChange({ inputValue: value, attachment });
+ }
+
+ onAttachmentAdd = (props: any) => {
+ const { fileList } = props;
+ const { uploadProps } = this.getProps();
+ const { onChange } = uploadProps;
+ if (onChange) {
+ onChange(props);
+ }
+ const { content } = this.getStates();
+ let newFileList = [...fileList];
+ this._adapter.setAttachment(newFileList);
+ this._adapter.notifyInputChange({
+ inputValue: content,
+ attachment: newFileList
+ });
+ }
+
+ onAttachmentDelete = (props: any) => {
+ const { content, attachment } = this.getStates();
+ const newAttachMent = attachment.filter(item => item.uid !== props.uid);
+ this._adapter.setAttachment(newAttachMent);
+ this._adapter.notifyInputChange({
+ inputValue: content,
+ attachment: newAttachMent
+ });
+ }
+
+ onSend = (e: any) => {
+ if (this.getDisableSend()) {
+ return;
+ }
+ const { content, attachment } = this.getStates();
+ this._adapter.setInputValue('');
+ this._adapter.setAttachment([]);
+ this._adapter.notifySend(content, attachment);
+ }
+
+ getDisableSend = () => {
+ const { content, attachment } = this.getStates();
+ const { disableSend: disableSendInProps } = this.getProps();
+ const disabledSend = disableSendInProps || (content.length === 0 && attachment.length === 0);
+ return disabledSend;
+ }
+
+ onEnterPress = (e: any) => {
+ const { sendHotKey } = this.getProps();
+ if (sendHotKey === SEND_HOT_KEY.SHIFT_PLUS_ENTER && e.shiftKey === false) {
+ return ;
+ } else if (sendHotKey === SEND_HOT_KEY.ENTER && e.shiftKey === true) {
+ return ;
+ }
+ handlePrevent(e);
+ this.onSend(e);
+ };
+
+ onPaste = (e: any) => {
+ const items = e.clipboardData?.items;
+ const { manualUpload } = this.getProps();
+ let files = [];
+ if (items) {
+ for (const it of items) {
+ const file = it.getAsFile();
+ file && files.push(it.getAsFile());
+ }
+ if (files.length) {
+ // 文件上传,则需要阻止默认粘贴行为
+ // File upload, you need to prevent the default paste behavior
+ manualUpload(files);
+ e.preventDefault();
+ e.stopPropagation();
+ }
+ }
+ }
+
+}
\ No newline at end of file
diff --git a/packages/semi-foundation/chat/rtl.scss b/packages/semi-foundation/chat/rtl.scss
new file mode 100644
index 0000000000..57d6b4af74
--- /dev/null
+++ b/packages/semi-foundation/chat/rtl.scss
@@ -0,0 +1,22 @@
+
+$module: #{$prefix}-chat;
+
+.#{$prefix}-rtl,
+.#{$prefix}-portal-rtl {
+ .#{$module} {
+ direction: rtl;
+
+ &-hint-icon {
+ transform: scaleX(-1);
+ }
+
+ &-inputBox-sendButton-icon {
+ transform: rotate(225deg);
+ }
+
+ &-chatBox-action-icon-redo {
+ transform: scaleX(-1);
+ }
+ }
+
+}
diff --git a/packages/semi-foundation/chat/variables.scss b/packages/semi-foundation/chat/variables.scss
new file mode 100644
index 0000000000..424e8a3865
--- /dev/null
+++ b/packages/semi-foundation/chat/variables.scss
@@ -0,0 +1,125 @@
+// radius
+$radius-chat_chatBox_content: var(--semi-border-radius-large); // 聊天框内容圆角
+$radius-chat_inputBox_container: 16px; // 输入框容器圆角
+$radius-chat_attachment_img: var(--semi-border-radius-medium); // 附件图片圆角
+$radius-chat_attachment_file: var(--semi-border-radius-medium); // 附件文件圆角
+$radius-chat_hint_item: var(--semi-border-radius-large); // 提示条圆角
+$radius-chat_chatBox_content_code: var(--semi-border-radius-large); // 代码块圆角
+$radius-chat_chatBox_content_code_topSlot_copy: var(--semi-border-radius-large); // 代码块顶部复制按钮圆角
+$radius-chat_dropArea: 16px; // 拖拽上传区域圆角
+
+//color
+$color-chat_action_content-bg: var(--semi-color-bg-0); // 返回按钮/停止生成内容按钮背景颜色
+$color-chat_action_content-border: var(--semi-color-border); // 返回按钮/停止生成按钮描边颜色
+$color-chat_divider: var(--semi-color-text-2); // 分割线颜色
+$color-chat_chatBox_title: var(--semi-color-text-0); //聊天框标题颜色
+$color-chat_chatBox_action_icon: var(--semi-color-text-2); // 聊天框操作区域按钮图标颜色
+$color-chat_chatBox_action_icon-hover: var(--semi-color-text-0); // 聊天框操作区域按钮图标hover颜色
+$color-chat_chatBox_action-bg-hover: transparent; // 聊天框操作区域按钮hover背景颜色
+$color-chat_chatBox_content_text: var(--semi-color-text-0); // 聊天框内容文字颜色
+$color-chat_chatBox_content_bg: var(--semi-color-fill-0); // 聊天框内容背景颜色
+$color-chat_chatBox_content_user-bg: var(--semi-color-primary); // 聊天框内容用户背景颜色
+$color-chat_chatBox_content_user-text: var(--semi-color-white); // 聊天框内容用户文字颜色
+$color-chat_chatBox_content_error-bg: var(--semi-color-danger-hover); // 聊天框内容错误背景颜色
+$color-chat_chatBox_content_error-text: var(--semi-color-white); // 聊天框内容错误文字颜色
+$color-chat_inputBottom_clearButton_icon: var(--semi-color-text-2); //清空按钮图标颜色
+$color-chat_inputBottom_uploadButton_icon: var(--semi-color-text-0); // 上传按钮图标颜色
+$color-chat_inputBottom_sendButton_icon-disable: var(--semi-color-primary-disabled); // 发送按钮禁用态图标颜色
+$color-chat_inputBox_container-border: var(--semi-color-border); // 输入框容器边框颜色
+$color-chat_attachment_clear_icon: var(--semi-color-text-2); // 附件清除图标颜色
+$color-chat_attachment_file-bg: var(--semi-color-fill-0); // 附件文件背景颜色
+$color-chat_chatBox_user_attachment_file-bg: var(--semi-color-bg-0); // 用户聊天框附件文件背景颜色
+$color-chat_chatBox_other_attachment_file-bg: var(--semi-color-fill-2); // 聊天框附件文件背景颜色
+$color-chat_attachment_file_icon: var(--semi-color-text-2); // 附件文件图标颜色
+$color-chat_attachment_file_title: var(--semi-color-text-0); // 附件文件标题颜色
+$color-chat_attachment_file_metadata_text: var(--semi-color-text-2); // 附件文件元数据文字颜色
+$color-chat_hint_item-border: var(--semi-color-border); // 提示条边框颜色
+$color-chat_hint_item-bg: transparent; // 提示条背景颜色
+$color-chat_hint_item-bg-hover: var(--semi-color-fill-0); // 提示条hover背景颜色
+$color-chat_hint_content_text: var(--semi-color-text-1); // 提示条文字颜色
+$color-chat_hint_icon: var(--semi-color-text-2); // 提示条图标颜色
+$color-chat_chatBox_loading-bg: var(--semi-color-text-0); // 聊天内容加载图标圆圈颜色
+$color-chat_chatBox_code_topSlot: rgba(var(--semi-white), 1); // 代码块顶部字体颜色
+$color-chat_chatBox_code_topSlot-bg: rgba(var(--semi-grey-4), 1); //代码块顶部背景色
+$color-chat_chatBox_code_topSlot_toCopy-bg-hover: rgba(var(--semi-grey-5), 1); // 代码块顶部复制按钮hover背景色
+$color-chat_chatBox_code_content: var(--semi-color-bg-0); // 代码块内容背景色
+$color-chat_action_content-bg-hover: var(--semi-color-tertiary-light-hover); // 返回按钮/停止生成按钮hover背景颜色
+$color-chat_dropArea-bg: rgba(var(--semi-grey-2), 0.9); // 拖拽区域文字颜色
+$color-chat_dropArea-border: var(--semi-color-border); // 拖拽区域边框颜色
+
+// spacing
+$spacing-chat_paddingY: 12px; // chat组件上下内边距
+$spacing-chat_container-paddingX: 16px; // 消息框水平内边距
+$spacing-chat_action_content-bottom: 0; // 返回按钮/停止生成按钮底部边距
+$spacing-chat_chatBox-marginY: 8px; // 聊天框上下外边距
+$spacing-chat_chatBox-columnGap: 12px; // 聊天框内容列间距
+$spacing-chat_chatBox_action-columnGap: 10px; // 聊天框操作区域按钮列间距
+$spacing-chat_chatBox_action-marginX: 10px; // 聊天框操作区域左右外边距
+$spacing-chat_chatBox_action_btn-padding: 0; // 聊天框操作区域按钮内边距
+$spacing-chat_chatBox_content-paddingY: 8px; // 聊天框内容上下内边距
+$spacing-chat_chatBox_content-paddingX: 12px; // 聊天框内容左右内边距
+$spacing-chat_inputBox-paddingTop: 8px; // 输入框顶部内边距
+$spacing-chat_inputBox-paddingBottom: 8px; // 输入框底部内边距
+$spacing-chat_inputBox-paddingX: 16px; // 输入框左右内边距
+$spacing-chat_inputBox_container-padding: 11px; // 输入框容器内边距
+$spacing-chat_inputBox_inner-columnGap: 4px; // 输入框容器列间距
+// $spacing-chat_inputBox_textarea-marginX: 5px; // 输入框textArea左右内边距
+$spacing-chat_inputBox-marginY: 4px;
+$spacing-chat_attachment-columnGap: 10px; // 附件列间距
+$spacing-chat_attachment-RowGap: 5px; // 附件行间距
+$spacing-chat_attachment_clear-top: 8px; // 附件清除图标顶部间距
+$spacing-chat_attachment_clear-right: 8px; // 附件清除图标右内边距
+$spacing-chat_attachment_file-columnGap: 5px; // 文件附件列间距
+$spacing-chat_attachment_file-padding: 5px; // 文件附件内边距
+$spacing-chat_chatBox_loading_item-gap: 15px; // 聊天内容加载图标间距
+$spacing-chat_divider-marginY: 12px; // 分割线上下外边距
+$spacing-chat_chatBox_content_attachment-marginY: 4px; // 聊天框内容文件/图片上下外间距
+$spacing-chat_chatBox_content_code_topSlot-paddingX: 5px; // 聊天框代码块顶部上下内边距
+$spacing-chat_chatBox_content_code_topSlot-paddingY: 8px; // 聊天框代码块顶部左右内边距
+$spacing-chat_chatBox_content_code_topSlot_copy-columnGap: 5px; // 聊天框代码块顶部复制按钮列间距:
+$spacing-chat_chatBox_content_code_topSlot_copy-padding: 5px; // 聊天框代码块顶部复制按钮列间距:
+$spacing-chat_chatBox_wrap: 8px; // 聊天框外层间距
+$spacing-chat_hint-rowGap: 10px; // 提示条行间距
+$spacing-chat_hint-marginY: 12px; // 提示条容器上下外边距
+$spacing-chat_hint-marginLeft: 34px; // 提示条容器左外边距
+$spacing-chat_hint_item-marginY: 8px; // 提示条上下外边距
+$spacing-chat_hint_item-marginX: 12px; // 提示条左右外边距
+$spacing-chat_hint_item-columnGap: 20px; // 提示条内容列间距
+$spacing-chat_chatBox_loading-item-marginX: 18px; // 聊天内容加载图标中心圆圈左右外边距
+$spacing-chat_chatBox_loading-item-marginY: 6px; // 聊天内容加载图标中心圆圈上下外边距
+
+// width
+$width-chat_backBottom_wrapper: 42px; // 返回按钮宽度
+$width-chat_action_content-border: 1px; // 返回按钮/停止生成按钮描边宽度
+$width-chat_inputBottom_clearButton: 48px; // 清空按钮宽度
+$width-chat_inputBottom_uploadButton: 32px; // 上传按钮宽度
+$width-chat_inputBottom_sendButton: 32px; // 发送按钮宽度
+$width-chat_inputBox_container-border: 1px; // 输入框容器边框宽度
+$width-chat_attachment_file: 50px; // 附件文件宽度
+$width-chat_hint_item-border: 1px; // 提示条边框宽度
+$width-chat_chatBox_loading: 8px; // 加载中单个圆圈图标宽度
+$width-chat_attachment_file_title: 90px; // 附件文件标题最大宽度
+$width-chat_max: 800px; // chat组件最大宽度
+$width-chat_dropArea-border: 5px; // 拖拽上传边框宽度
+$width-chat_chatBox_content_code_topSlot_copy: 150px; // 聊天框代码块顶部复制按钮最小宽度
+// height
+$height-chat_action_stop: 42px; //停止生成按钮高度
+
+//font
+$font-chat_divider-fontWeight: $font-weight-regular; // 分割线字重
+$font-chat_divider-fontSize: $font-size-small; // 分割线字体大小
+$font-chat_chatBox_title-lineHeight: 20px; //聊天框标题行高
+$font-chat_chatBox_title-fontSize: $font-size-header-6; // 聊天框标题字体大小
+$font-chat_chatBox_title-fontWeight: $font-weight-regular; // 聊天框标题字重
+$font-chat_inputBottom_clearButton_icon-fontSize: 30px; // 输入区清空上下文按钮图标大小
+$font-chat_attachment_file_title-fontSize: $font-size-header-6; // 附件文件标题字体大小
+$font-chat_attachment_file_metadata-fontSize: $font-size-regular; // 附件文件元数据字体大小
+$font-chat_hint_content-fontSize: $font-size-regular; // 提示条文字大小
+// $font-chat_hint_icon: 20px; // 提示条图标大小
+$font-chat_chatBox_code_topSlot: 12px; // 代码块顶部字体大小
+$font-chat_chatBox_code_topSlot-lineHeight: 16px; //代码块顶部区域字体行高
+$font-chat_dropArea_text: 48px; // 拖拽上传区域文字大小
+
+//z-index
+$z-chat_dropArea: 10; // 拖拽上传区域z-index
+$z-chat_action: 1; // 返回按钮/停止生成按钮z-index
\ No newline at end of file
diff --git a/packages/semi-foundation/input/textareaFoundation.ts b/packages/semi-foundation/input/textareaFoundation.ts
index 87f5871a04..9bff96089a 100644
--- a/packages/semi-foundation/input/textareaFoundation.ts
+++ b/packages/semi-foundation/input/textareaFoundation.ts
@@ -171,6 +171,11 @@ export default class TextAreaFoundation extends BaseFoundation
}
handleKeyDown(e: any) {
+ const { disabledEnterStartNewLine } = this.getProps();
+ if (disabledEnterStartNewLine && e.key === 'Enter' && !e.shiftKey) {
+ // Prevent default line wrapping behavior
+ e.preventDefault();
+ }
this._adapter.notifyKeyDown(e);
if (e.keyCode === 13) {
this._adapter.notifyPressEnter(e);
diff --git a/packages/semi-foundation/package.json b/packages/semi-foundation/package.json
index 901ddd6943..3a773ab010 100644
--- a/packages/semi-foundation/package.json
+++ b/packages/semi-foundation/package.json
@@ -1,13 +1,13 @@
{
"name": "@douyinfe/semi-foundation",
- "version": "2.62.1",
+ "version": "2.63.0-beta.0",
"description": "",
"scripts": {
"build:lib": "node ./scripts/compileLib.js",
"prepublishOnly": "npm run build:lib"
},
"dependencies": {
- "@douyinfe/semi-animation": "2.62.1",
+ "@douyinfe/semi-animation": "2.63.0-beta.0",
"@mdx-js/mdx": "^3.0.1",
"async-validator": "^3.5.0",
"classnames": "^2.2.6",
diff --git a/packages/semi-foundation/upload/foundation.ts b/packages/semi-foundation/upload/foundation.ts
index ba2dab9cf1..560c19e0f5 100644
--- a/packages/semi-foundation/upload/foundation.ts
+++ b/packages/semi-foundation/upload/foundation.ts
@@ -60,7 +60,8 @@ export interface AfterUploadResult {
autoRemove?: boolean;
status?: string;
validateMessage?: unknown;
- name?: string
+ name?: string;
+ url?: string
}
export interface UploadAdapter, S = Record> extends DefaultAdapter {
@@ -646,7 +647,7 @@ class UploadFoundation
, S = Record> extends
e ? (newFileList[index].event = e) : null;
if (afterUpload && typeof afterUpload === 'function') {
- const { autoRemove, status, validateMessage, name } =
+ const { autoRemove, status, validateMessage, name, url } =
this._adapter.notifyAfterUpload({
response: body,
file: newFileList[index],
@@ -655,6 +656,7 @@ class UploadFoundation, S = Record> extends
status ? (newFileList[index].status = status) : null;
validateMessage ? (newFileList[index].validateMessage = validateMessage) : null;
name ? (newFileList[index].name = name) : null;
+ url ? (newFileList[index].url = url) : null;
autoRemove ? newFileList.splice(index, 1) : null;
}
this._adapter.notifySuccess(body, fileInstance, newFileList);
diff --git a/packages/semi-icons-lab/package.json b/packages/semi-icons-lab/package.json
index 0b8b9789c3..e1b8df7adc 100644
--- a/packages/semi-icons-lab/package.json
+++ b/packages/semi-icons-lab/package.json
@@ -1,6 +1,6 @@
{
"name": "@douyinfe/semi-icons-lab",
- "version": "2.62.1",
+ "version": "2.63.0-beta.0",
"description": "semi icons lab",
"keywords": [
"semi",
diff --git a/packages/semi-icons/package.json b/packages/semi-icons/package.json
index 3c0055c0c0..5275cb8919 100644
--- a/packages/semi-icons/package.json
+++ b/packages/semi-icons/package.json
@@ -1,6 +1,6 @@
{
"name": "@douyinfe/semi-icons",
- "version": "2.62.1",
+ "version": "2.63.0-beta.0",
"description": "semi icons",
"keywords": [
"semi",
diff --git a/packages/semi-illustrations/package.json b/packages/semi-illustrations/package.json
index 1fd1f5097b..8612922599 100644
--- a/packages/semi-illustrations/package.json
+++ b/packages/semi-illustrations/package.json
@@ -1,6 +1,6 @@
{
"name": "@douyinfe/semi-illustrations",
- "version": "2.62.1",
+ "version": "2.63.0-beta.0",
"description": "semi illustrations",
"keywords": [
"semi",
diff --git a/packages/semi-next/package.json b/packages/semi-next/package.json
index ffaafddbe2..1687b0f30e 100644
--- a/packages/semi-next/package.json
+++ b/packages/semi-next/package.json
@@ -1,6 +1,6 @@
{
"name": "@douyinfe/semi-next",
- "version": "2.62.1",
+ "version": "2.63.0-beta.0",
"description": "Plugin that support Semi Design in Next.js",
"author": "伍浩威 ",
"homepage": "",
@@ -22,7 +22,7 @@
"typescript": "^4"
},
"dependencies": {
- "@douyinfe/semi-webpack-plugin": "2.62.1"
+ "@douyinfe/semi-webpack-plugin": "2.63.0-beta.0"
},
"gitHead": "eb34a4f25f002bb4cbcfa51f3df93bed868c831a"
}
diff --git a/packages/semi-rspack/package.json b/packages/semi-rspack/package.json
index c9521c1621..7ea75441b8 100644
--- a/packages/semi-rspack/package.json
+++ b/packages/semi-rspack/package.json
@@ -1,6 +1,6 @@
{
"name": "@douyinfe/semi-rspack-plugin",
- "version": "2.62.1",
+ "version": "2.63.0-beta.0",
"description": "",
"homepage": "",
"license": "MIT",
diff --git a/packages/semi-scss-compile/package.json b/packages/semi-scss-compile/package.json
index 2d37c3cb4c..e94ad6134d 100644
--- a/packages/semi-scss-compile/package.json
+++ b/packages/semi-scss-compile/package.json
@@ -1,6 +1,6 @@
{
"name": "@douyinfe/semi-scss-compile",
- "version": "2.62.1",
+ "version": "2.63.0-beta.0",
"description": "compile semi scss to css",
"author": "daiqiang@bytedance.com",
"license": "MIT",
diff --git a/packages/semi-theme-default/package.json b/packages/semi-theme-default/package.json
index b52a4426f6..8e754266a4 100644
--- a/packages/semi-theme-default/package.json
+++ b/packages/semi-theme-default/package.json
@@ -1,6 +1,6 @@
{
"name": "@douyinfe/semi-theme-default",
- "version": "2.62.1",
+ "version": "2.63.0-beta.0",
"description": "semi-theme-default",
"keywords": [
"semi-theme",
diff --git a/packages/semi-ui/chat/_story/chat.stories.jsx b/packages/semi-ui/chat/_story/chat.stories.jsx
new file mode 100644
index 0000000000..992c112b18
--- /dev/null
+++ b/packages/semi-ui/chat/_story/chat.stories.jsx
@@ -0,0 +1,828 @@
+import { getUuidv4 } from '@douyinfe/semi-foundation/utils/uuid';
+import Chat from '../index';
+import React, { useState, useCallback, useRef, useEffect, useMemo } from 'react';
+import { Form, Button, Avatar, Dropdown, Radio, RadioGroup, Switch, Collapsible, AvatarGroup} from '@douyinfe/semi-ui';
+import { IconUpload, IconForward, IconMoreStroked, IconArrowRight, IconChevronUp } from '@douyinfe/semi-icons';
+import MarkdownRender from '../../markdownRender';
+import { initMessage, roleInfo, commonOuterStyle, hintsExample, infoWithAttachment, simpleInitMessage, semiCode } from './constant';
+
+export default {
+ title: 'Chat',
+ parameters: {
+ chromatic: { disableSnapshot: true },
+ }
+}
+
+const uploadProps = { action: 'https://api.semi.design/upload' }
+
+export const _Chat = () => {
+ const [message, setMessage] = useState(initMessage);
+ const [hints, setHints] = useState(hintsExample);
+ const [mode, setMode] = useState('bubble');
+ const [align, setAlign] = useState('leftRight');
+ const [sendHotKey, setSendHotKey] = useState('enter');
+ const [key, setKey] = useState(1);
+ const [showClearContext, setShowClearContext] = useState(false);
+
+ const onClear = useCallback((clearMessage) => {
+ console.log('onClear');
+ }, []);
+
+ const onMessageSend = useCallback((content, attachment) => {
+ const newAssistantMessage = {
+ role: 'assistant',
+ id: getUuidv4(),
+ content: "这是一条 mock 回复信息",
+ }
+ setMessage((message) => {
+ return [
+ ...message,
+ newAssistantMessage
+ ]
+ })
+ }, []);
+
+ const onMessageDelete = useCallback((message) => {
+ console.log('message delete', message);
+ }, []);
+
+ const onChatsChange = useCallback((chats) => {
+ console.log('onChatsChange', chats);
+ setMessage(chats);
+ }, []);
+
+ const onMessageGoodFeedback = useCallback((message) => {
+ console.log('message good feedback', message);
+ }, []);
+
+ const onMessageBadFeedback = useCallback((message) => {
+ console.log('message bad feedback', message);
+ }, []);
+
+ const onMessageReset = useCallback((message) => {
+ console.log('message reset', message);
+ }, []);
+
+ const onInputChange = useCallback((props) => {
+ console.log('onInputChange', props);
+ }, []);
+
+ const onHintClick = useCallback((hint) => {
+ setHints([]);
+ }, []);
+
+ const onModeChange = useCallback((e) => {
+ setMode(e.target.value);
+ setKey((key) => key + 1);
+ }, []);
+
+ const onAlignChange = useCallback((e) => {
+ setAlign(e.target.value);
+ setKey((key) => key + 1);
+ }, []);
+
+ const onSwitchChange = useCallback(() => {
+ setShowClearContext((showClearContext) => !showClearContext);
+ }, [])
+
+ const onSendHotKeyChange = useCallback((e) => {
+ setSendHotKey(e.target.value);
+ }, []);
+
+ return (
+ <>
+
+
+ 展示清除上下文按钮:
+
+
+
+ 模式:
+
+ 气泡
+ 非气泡
+ 用户会话气泡
+
+
+
+ 布局:
+
+ 左右分布
+ 全左
+
+
+
+ 按键发送策略:
+
+ enter
+ shift+enter
+
+
+
+
+
+
+ >
+ )
+}
+
+export const Attachment = () => {
+ const [message, setMessage] = useState(infoWithAttachment);
+
+ return (
+
+
+
+ )
+}
+
+function CustomInputRender(props) {
+ const { defaultNode, onClear, onSend } = props;
+ const api = useRef();
+ const onSubmit = useCallback(() => {
+ if (api.current) {
+ const values = api.current.getValues();
+ if ((values.name && values.name.length !== 0) || (values.file && values.file.length !== 0)) {
+ onSend(values.name, values.file);
+ api.current.reset();
+ }
+ }
+ }, []);
+
+ return (
+
+
+ } theme="light">
+ 点击上传
+
+
+
+
+
);
+}
+
+export const CustomRenderInputArea = () => {
+ const [message, setMessage] = useState(initMessage.slice(0, 1));
+
+ const onChatsChange = useCallback((chats) => {
+ setMessage(chats);
+ }, []);
+
+ const onMessageSend = useCallback((content, attachment) => {
+ const newUserMessage = {
+ role: 'user',
+ id: getUuidv4(),
+ content: content,
+ attachment: attachment
+ }
+ const newAssistantMessage = {
+ role: 'assistant',
+ id: getUuidv4(),
+ content: `This is a mock response`
+ }
+ setMessage((message) => ([...message, newUserMessage, newAssistantMessage]));
+ }, []);
+
+ const renderInputArea = useCallback((props) => {
+ return ()
+ }, []);
+
+ return (
+
+
+
+ )
+}
+
+export const CustomRenderAvatar = (props) => {
+ const customRenderAvatar = useCallback((props)=> {
+ const { role, defaultAvatar } = props;
+ return {role.name}
+ }, []);
+
+ const customRenderTitle = useCallback((props)=> null, []);
+
+ return (
+
+
);
+}
+
+export const CustomRenderTitle = (props) => {
+ const customRenderTitle = useCallback((props) => {
+ const { role, message, defaultTitle } = props;
+ if (message.role === 'user') {
+ return null;
+ }
+ return
+
+ {defaultTitle}
+
+ }, []);
+
+ const customRenderAvatar = useCallback((props)=> null, []);
+
+ return (
+
+
);
+}
+
+export const CustomFullChatBox = () => {
+ const customRenderChatBox = useCallback((props) => {
+ const { role, message, defaultNodes, className } = props;
+ const date = new Date(message.createAt);
+ const year = date.getFullYear();
+ const month = ('0' + (date.getMonth() + 1)).slice(-2);
+ const day = ('0' + date.getDate()).slice(-2);
+ const hours = ('0' + date.getHours()).slice(-2);
+ const minutes = ('0' + date.getMinutes()).slice(-2);
+ const seconds = ('0' + date.getSeconds()).slice(-2);
+ const formattedDate = `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`;
+
+ return
+
+
{formattedDate}
+
+ {defaultNodes.content}
+
+ {defaultNodes.action}
+
+
+ }, []);
+
+ return (
+
+
);
+}
+
+const CustomActions = React.memo((props) => {
+ const { role, message, defaultActions, className } = props;
+ const myRef = useRef();
+ const getContainer = useCallback(() => {
+ if (myRef.current) {
+ const element = myRef.current;
+ let parentElement = element.parentElement;
+ while (parentElement) {
+ if (parentElement.classList.contains('semi-chat-chatBox-wrap')) {
+ return parentElement;
+ }
+ parentElement = parentElement.parentElement;
+ }
+ }
+ }, [myRef]);
+ return
+ {defaultActions.map((item, index)=> {
+ return {item}
+ })}
+ {
+ }>分享
+
+ }
+ trigger="click"
+ position="top"
+ getPopupContainer={getContainer}
+ >
+ }
+
+});
+
+export const CustomRenderAction = () => {
+ const [message, setMessage] = useState(simpleInitMessage);
+ const customRenderAction = useCallback((props) => {
+ return
+ }, []);
+
+ const onChatsChange = useCallback((chats) => {
+ setMessage(chats);
+ }, []);
+
+ return (
+
+
);
+}
+
+export const CustomRenderContent = () => {
+ const renderContent = useCallback((props) => {
+ const { role, message, defaultNode, className } = props;
+ return
+ ---custom render content---
+
+
+ }, []);
+
+ return (
+
+
);
+}
+
+// const Card = (source) => {
+// return (
+//
+//
+//
+// )
+// }
+
+const SourceCard = (props) => {
+ const [open, setOpen] = useState(true);
+ const [show, setShow] = useState(false);
+ const spanRef = useRef();
+ const onOpen = useCallback(() => {
+ setOpen(false);
+ setShow(true);
+ }, []);
+
+ const onClose = useCallback(() => {
+ setOpen(true);
+ setTimeout(() => {
+ setShow(false);
+ }, 350)
+ }, []);
+
+ return (
+
+ 基于{props.sources.length}个搜索来源
+
+ {props.sources.map((s, index) => ())}
+
+
+
+
+ Source
+
+
+
+ {props.sources.map(s => (
+
+
+
+ {s.title}
+
+ {s.subTitle}
+ {s.content}
+ ))}
+
+
+
+ )
+}
+
+
+export const CustomRenderContentPlus = () => {
+ const chat = [
+ {
+ role: 'assistant',
+ id: '3',
+ createAt: 1715676751919,
+ content: "Semi Design 是由抖音前端团队,MED 产品设计团队设计、开发并维护的设计系统。它作为全面、易用、优质的现代应用 UI 解决方案,从字节跳动各业务线的复杂场景提炼而来,支撑近千计平台产品,服务内外部 10 万+ 用户。",
+ source: [
+ {
+ avatar: 'https://lf3-static.bytednsdoc.com/obj/eden-cn/ptlz_zlp/ljhwZthlaukjlkulzlp/other/logo.png',
+ url: '/zh-CN/start/introduction',
+ title: 'semi Design',
+ subTitle: 'Semi design website',
+ content: 'Semi Design 是由抖音前端团队,MED 产品设计团队设计、开发并维护的设计系统。'
+ },
+ {
+ avatar: 'https://lf3-static.bytednsdoc.com/obj/eden-cn/ptlz_zlp/ljhwZthlaukjlkulzlp/other/logo.png',
+ url: '/dsm/landing',
+ subTitle: 'Semi DSM website',
+ title: 'Semi 设计系统',
+ content: '从 Semi Design,到 Any Design 快速定义你的设计系统,并应用在设计稿和代码中'
+ },
+ {
+ avatar: 'https://lf3-static.bytednsdoc.com/obj/eden-cn/ptlz_zlp/ljhwZthlaukjlkulzlp/other/logo.png',
+ url: '/code/zh-CN/start/introduction',
+ subTitle: 'Semi D2C website',
+ title: '设计稿转代码',
+ content: 'Semi 设计稿转代码(Semi Design to Code,或简称 Semi D2C),是由抖音前端 Semi Design 团队推出的全新的提效工具'
+ },
+ {
+ avatar: 'https://lf3-static.bytednsdoc.com/obj/eden-cn/ptlz_zlp/ljhwZthlaukjlkulzlp/other/logo.png',
+ url: '/zh-CN/start/introduction',
+ title: 'semi Design',
+ subTitle: 'Semi design website',
+ content: 'Semi Design 是由抖音前端团队,MED 产品设计团队设计、开发并维护的设计系统。'
+ },
+ {
+ avatar: 'https://lf3-static.bytednsdoc.com/obj/eden-cn/ptlz_zlp/ljhwZthlaukjlkulzlp/other/logo.png',
+ url: '/dsm/landing',
+ subTitle: 'Semi DSM website',
+ title: 'Semi 设计系统',
+ content: '从 Semi Design,到 Any Design 快速定义你的设计系统,并应用在设计稿和代码中'
+ },
+ {
+ avatar: 'https://lf3-static.bytednsdoc.com/obj/eden-cn/ptlz_zlp/ljhwZthlaukjlkulzlp/other/logo.png',
+ url: '/code/zh-CN/start/introduction',
+ subTitle: 'Semi D2C website',
+ title: '设计稿转代码',
+ content: 'Semi 设计稿转代码(Semi Design to Code,或简称 Semi D2C),是由抖音前端 Semi Design 团队推出的全新的提效工具'
+ },
+ ]
+ }];
+
+ const renderContent = useCallback((props) => {
+ const { role, message, defaultNode, className } = props;
+ return
+
+
+
+ }, []);
+
+ return ();
+}
+
+export const LeftAlign = () => {
+ return (
+
+
);
+}
+
+export const MessageStatus = () => {
+ const messages = [
+ initMessage[1],
+ {
+ ...initMessage[2],
+ content: '请求错误',
+ status: 'error'
+ },
+ {
+ id: 'loading',
+ role: 'assistant',
+ status: 'loading'
+ },
+ ]
+ return (
+
+
);
+}
+
+export const MockResponseMessage = () => {
+ const [message, setMessage] = useState([ initMessage[0]]);
+ const intervalId = useRef();
+
+ const onChatsChange = useCallback((chats) => {
+ console.log('onChatsChange', chats);
+ setMessage(chats);
+ }, []);
+
+ const onMessageSend = useCallback((content, attachment) => {
+ setMessage((message) => {
+ return [
+ ...message,
+ {
+ role: 'user',
+ createAt: Date.now(),
+ id: getUuidv4(),
+ content: content,
+ attachment: attachment,
+ },
+ {
+ role: 'assistant',
+ status: 'loading',
+ createAt: Date.now(),
+ id: getUuidv4()
+ }
+ ]
+ });
+ generateMockResponse(content);
+ },[])
+
+ const generateMockResponse = useCallback((content) => {
+ const id = setInterval(() => {
+ setMessage((message) => {
+ const lastMessage = message[message.length - 1];
+ let newMessage = {};
+ if (lastMessage.status === 'loading') {
+ newMessage = {
+ role: 'assistant',
+ id: getUuidv4(),
+ content: `mock Response for ${content} \n`,
+ status: 'incomplete'
+ }
+ } else if (lastMessage.status === 'incomplete') {
+ if (lastMessage.content.length > 200) {
+ clearInterval(id);
+ intervalId.current = null
+ newMessage = {
+ role: 'assistant',
+ id: getUuidv4(),
+ content: `${lastMessage.content} mock stream message`,
+ status: 'complete'
+ }
+ } else {
+ newMessage = {
+ role: 'assistant',
+ id: getUuidv4(),
+ content: `${lastMessage.content} mock stream message`,
+ status: 'incomplete'
+ }
+ }
+ }
+ return [
+ ...message.slice(0, -1),
+ newMessage
+ ]
+ })
+ }, 400);
+ intervalId.current = id;
+ }, []);
+
+ const onStopGenerator = useCallback(() => {
+ if (intervalId.current) {
+ clearInterval(intervalId.current);
+ setMessage((message) => {
+ const lastMessage = message[message.length - 1];
+ if (lastMessage.status && lastMessage.status !== 'complete') {
+ const lastMessage = message[message.length - 1];
+ let newMessage = {...lastMessage};
+ newMessage.status = 'complete';
+ return [
+ ...message.slice(0, -1),
+ newMessage
+ ]
+ } else {
+ return message;
+ }
+ })
+ }
+ }, [intervalId]);
+
+ return (
+
+
+
+ );
+}
+
+export const CustomRenderHint = () => {
+ const [message, setMessage] = useState(initMessage.slice(0, 3));
+ const [hint, setHint] = useState(hintsExample);
+
+ const commonHintStyle = useMemo(() => ({
+ border: '1px solid var(--semi-color-border)',
+ padding: '10px',
+ borderRadius: '10px',
+ width: 'fit-content',
+ color: 'var( --semi-color-text-1)',
+ display: 'flex',
+ alignItems: 'center',
+ cursor: 'pointer',
+ fontSize: '14px'
+ }), []);
+
+ const renderHintBox = useCallback((props) => {
+ const { content, onHintClick, index } = props;
+ return
+ {content}
+ click me
+
+ }, []);
+
+ const onChatsChange = useCallback((chats) => {
+ setMessage(chats);
+ }, []);
+
+ const onHintClick = useCallback((hint) => {
+ setHint([]);
+ }, []);
+
+ const onClear = useCallback(() => {
+ setHint([]);
+ }, []);
+
+ return
+
+
+}
diff --git a/packages/semi-ui/chat/_story/chat.stories.tsx b/packages/semi-ui/chat/_story/chat.stories.tsx
new file mode 100644
index 0000000000..11b6c46dcc
--- /dev/null
+++ b/packages/semi-ui/chat/_story/chat.stories.tsx
@@ -0,0 +1,90 @@
+import React, { useState, useCallback, } from 'react';
+import { storiesOf } from '@storybook/react';
+import Chat from '@douyinfe/semi-ui/chat';
+import { getUuidv4 } from '@douyinfe/semi-foundation/utils/uuid';
+import { initMessage, roleInfo, commonOuterStyle, hintsExample } from './constant';
+
+
+const stories = storiesOf('Chat', module);
+
+stories.add('Chat 对话', () => {
+ const [message, setMessage] = useState(initMessage);
+ const [hints, setHints] = useState(hintsExample);
+
+ const onClear = useCallback(() => {
+ console.log('onClear');
+ setHints([]);
+ }, []);
+
+ const onMessageSend = useCallback((content, attachment) => {
+ const newUserMessage = {
+ role: 'user',
+ id: getUuidv4(),
+ content: content,
+ attachment: attachment,
+ }
+ const newAssistantMessage = {
+ role: 'assistant',
+ id: getUuidv4(),
+ content: "这是一条 mock 回复信息",
+ }
+ setMessage((message) => {
+ return [
+ ...message,
+ newUserMessage,
+ newAssistantMessage
+ ] as any
+ })
+ }, []);
+
+ const onMessageDelete = useCallback((message) => {
+ console.log('message delete', message);
+ }, []);
+
+ const onChatsChange = useCallback((chats) => {
+ console.log('onChatsChange', chats);
+ setMessage(chats);
+ }, []);
+
+ const onMessageGoodFeedback = useCallback((message) => {
+ console.log('message good feedback', message);
+ }, []);
+
+ const onMessageBadFeedback = useCallback((message) => {
+ console.log('message bad feedback', message);
+ }, []);
+
+ const onMessageReset = useCallback((message) => {
+ console.log('message reset', message);
+ }, []);
+
+ const onInputChange = useCallback((props) => {
+ console.log('onInputChange', props);
+ }, []);
+
+ const onHintClick = useCallback((hint) => {
+ setHints([]);
+ }, []);
+
+ return (
+
+
+
+ )
+});
diff --git a/packages/semi-ui/chat/_story/constant.js b/packages/semi-ui/chat/_story/constant.js
new file mode 100644
index 0000000000..b7b25767fd
--- /dev/null
+++ b/packages/semi-ui/chat/_story/constant.js
@@ -0,0 +1,141 @@
+const semiCode = "以下是一个 \`Semi\` 代码的使用示例:\n```jsx \nimport React from 'react';\nimport { Button } from '@douyinfe/semi-ui';\nconst MyComponent = () => {\n const handleClick = () => {\n console.log('Button clicked');\n};\n return (\n \n
Hello, Semi Design!
\n \n
\n );\n};\nexport default MyComponent;\n```";
+const semiInfo = `
+Semi Design 是由抖音前端团队和MED产品设计团队设计、开发并维护的设计系统。作为一个全面、易用、优质的现代应用UI解决方案,Semi Design从字节跳动各业务线的复杂场景中提炼而来,目前已经支撑了近千个平台产品,服务了内外部超过10万用户[[1]](https://semi.design/zh-CN/start/introduction)。
+
+Semi Design的愿景是成为企业应用前端不可或缺的一半,为企业应用前端提供坚实且优质的基础。设计系统的真正价值在于降低前端的搭建成本,同时提供优秀的设计和工程化标准,充分解放设计师与开发者的生产力,从而不断孵化出优秀的产品[[1]](https://semi.design/zh-CN/start/introduction)。
+
+Semi Design的特点包括:
+
+1. 设计:Semi Design通过提炼简洁轻量、现代化的设计风格,细致打磨原子组件的交互,并在字节跳动的海量业务场景下进行迭代,沉淀了一套优质的默认基础。它将保证Semi Design打造的企业应用产品具有连贯一致的"语言",并且质量优于陈旧系统的基线。此外,Semi Design还充分进行模块化解耦,并开放自定义能力,方便用户进行二次裁剪与定制,搭建适用于不同形态产品的前端资产[[1]](https://semi.design/zh-CN/start/introduction)。
+
+2. 主题化:Semi Design提供了强大的主题化方案,通过对数千个设计变量的分层和梳理,设计师和开发者可以在全局、乃至组件级别对表现层进行深度定制。这使得Semi Design可以轻松实现品牌一键定制,满足业务和品牌多样化的视觉需求。主题化方案还支持从线上到设计工具的实时同步,提高设计和研发的持续对齐效率,降低产研间的沟通成本[[1]](https://semi.design/zh-CN/start/introduction)。
+
+3. 深色模式:为了兼容更多用户群体在不同生产环境下的使用偏好,Semi Design的任意主题均自动支持深色模式,并能在应用运行时动态切换。此外,Semi Design还允许用户在应用内局部区域开启深色模式,以兼容SDK或插件型产品的使用场景。用户还可以通过进阶设置实现应用和系统主题的自动保持一致。为了提升开发体验,Semi Design还提供了将未规范化的存量旧工程一键兼容到Semi暗色模式的CLI工具,通过自动化的方式规避迁移成本[[1]](https://semi.design/zh-CN/start/introduction)。
+
+4. 国际化:Semi Design经过30+版本迭代,已具备完善的国际化特性。它覆盖了简/繁体中文、英语、日语、韩语、葡萄牙语等20+种语言,日期时间组件提供全球时区支持,全部组件可自动适配阿拉伯文RTL布局。同时,Semi Design也支持海外地区的开发者使用,对站点和文档进行了双语适配,以保证开发无障碍[[1]](https://semi.design/zh-CN/start/introduction)。
+
+5. 跨框架技术方案:Semi Design采用了一套跨前端框架技术方案,将每个组件的JavaScript拆分为Foundation和Adapter两部Semi Design 是由抖音前端团队和MED产品设计团队设计、开发并维护的设计系统。作为一个全面、易用、优质的现代应用UI解决方案,Semi Design从字节跳动各业务线的复杂场景中提炼而来,目前已经支撑了近千个平台产品,服务了内外部超过10万用户[[1]](https://semi.design/zh-CN/start/introduction)。
+
+---
+Learn more:
+1. [Introduction 介绍 - Semi Design](https://semi.design/zh-CN/start/introduction)
+2. [Getting Started 快速开始 - Semi Design](https://semi.design/zh-CN/start/getting-started)
+3. [Semi D2C 设计稿转代码的演进之路 - 知乎](https://zhuanlan.zhihu.com/p/667189184)
+`;
+
+const initMessage = [
+ {
+ role: 'system',
+ id: '1',
+ createAt: 1715676751919,
+ content: "Hello, I'm your AI assistant.",
+ },
+ {
+ role: 'user',
+ id: '2',
+ createAt: 1715676751919,
+ content: "介绍一下 semi design",
+ },
+ {
+ role: 'assistant',
+ id: '3',
+ createAt: 1715676751919,
+ content: semiInfo,
+ },
+ {
+ role: 'user',
+ id: '4',
+ createAt: 1715676751919,
+ content: "Semi design Button 使用示例",
+ },
+ {
+ role: 'assistant',
+ id: '5',
+ createAt: 1715676751919,
+ content: semiCode
+ },
+];
+
+const infoWithAttachment = [
+ {
+ role: 'user',
+ id: '2',
+ createAt: 1715676751919,
+ content: [
+ {
+ type: 'text',
+ text: '用于查看附件的样式,不处理任何输入',
+ },
+ {
+ type: 'image_url',
+ image_url: {
+ url: 'https://lf3-static.bytednsdoc.com/obj/eden-cn/ptlz_zlp/ljhwZthlaukjlkulzlp/root-web-sites/edit-bag.jpeg'
+ },
+ }
+ ],
+ },
+ {
+ role: 'assistant',
+ id: '3',
+ createAt: 1715676751919,
+ content: `用于查看附件的样式, 不处理任何输入\n\n![image](https://lf3-static.bytednsdoc.com/obj/eden-cn/ptlz_zlp/ljhwZthlaukjlkulzlp/root-web-sites/edit-bag.jpeg)`,
+ },
+];
+
+
+const roleInfo = {
+ user: {
+ name: 'User Test',
+ avatar: 'https://lf3-static.bytednsdoc.com/obj/eden-cn/ptlz_zlp/ljhwZthlaukjlkulzlp/docs-icon.png'
+ },
+ assistant: {
+ name: 'Assistant Test',
+ avatar: 'https://lf3-static.bytednsdoc.com/obj/eden-cn/ptlz_zlp/ljhwZthlaukjlkulzlp/other/logo.png'
+ },
+ system: {
+ name: 'System Test'
+ }
+};
+
+const commonOuterStyle = {
+ border: '1px solid var(--semi-color-border)',
+ borderRadius: '16px',
+ margin: '8px 16px',
+};
+
+const hintsExample = [
+ "告诉我更多",
+ "Semi Design 的组件有哪些?",
+ "Semi Design 官网及 github 仓库地址是?",
+];
+
+const simpleInitMessage = [
+ {
+ role: 'system',
+ id: '1',
+ createAt: 1715676751919,
+ content: "Hello, I'm your AI assistant.",
+ },
+ {
+ role: 'user',
+ id: '2',
+ createAt: 1715676751919,
+ content: "介绍一下 semi design",
+ },
+ {
+ role: 'assistant',
+ id: '3',
+ createAt: 1715676751919,
+ content: "Semi Design 是由抖音前端团队和MED产品设计团队设计、开发并维护的设计系统。",
+ },
+];
+
+export {
+ initMessage,
+ roleInfo,
+ commonOuterStyle,
+ hintsExample,
+ infoWithAttachment,
+ simpleInitMessage,
+ semiCode
+};
diff --git a/packages/semi-ui/chat/attachment.tsx b/packages/semi-ui/chat/attachment.tsx
new file mode 100644
index 0000000000..7574b13c85
--- /dev/null
+++ b/packages/semi-ui/chat/attachment.tsx
@@ -0,0 +1,97 @@
+import React from "react";
+import { FileItem } from '../upload/interface';
+import Image from '../image';
+import { IconBriefStroked, IconClear } from '@douyinfe/semi-icons';
+import { strings, cssClasses } from '@douyinfe/semi-foundation/chat/constants';
+import cls from 'classnames';
+import { Progress } from "../index";
+
+const { PREFIX_ATTACHMENT, } = cssClasses;
+const { PIC_SUFFIX_ARRAY, PIC_PREFIX } = strings;
+
+interface AttachmentProps {
+ className?: string;
+ attachment?: FileItem[];
+ onClear?: (item: FileItem) => void;
+ showClear?: boolean
+}
+
+interface FileProps {
+ url?: string;
+ name?: string;
+ size?: string;
+ type?: string;
+}
+
+export const FileAttachment = React.memo((props: FileProps) => {
+ const { url, name, size, type } = props;
+ return
+
+
+ {name}
+
+ {type}
+ {type ? ' · ' : ''}{size}
+
+
+
+})
+
+export const ImageAttachment = React.memo((props: {src: string}) => {
+ const { src } = props;
+ return
+})
+
+const Attachment = React.memo((props: AttachmentProps) => {
+ const { attachment, onClear, showClear = true, className } = props;
+
+ return (
+
+ {
+ attachment.map(item => {
+ const { percent, status } = item;
+ const suffix = item?.name.split('.').pop();
+ const isImg = item?.fileInstance?.type?.startsWith(PIC_PREFIX) || PIC_SUFFIX_ARRAY.includes(suffix);
+ const realType = suffix ?? item?.fileInstance?.type?.split('/').pop();
+ const showProcess = !(percent === 100 || typeof percent === 'undefined') && status === 'uploading';
+ return
+ {isImg ? (
+
+ ) : (
+
+ )}
+ {showClear &&
{
+ onClear && onClear(item);
+ }}
+ />}
+ {showProcess && }
+ ;
+ })
+ }
+
+ );
+});
+
+export default Attachment;
diff --git a/packages/semi-ui/chat/chatBox/chatBoxAction.tsx b/packages/semi-ui/chat/chatBox/chatBoxAction.tsx
new file mode 100644
index 0000000000..529906b04a
--- /dev/null
+++ b/packages/semi-ui/chat/chatBox/chatBoxAction.tsx
@@ -0,0 +1,253 @@
+import React, { PureComponent, ReactNode } from 'react';
+import PropTypes from 'prop-types';
+import type { ChatBoxProps, Message } from '../interface';
+import { IconThumbUpStroked,
+ IconDeleteStroked,
+ IconCopyStroked,
+ IconLikeThumb,
+ IconRedoStroked
+} from '@douyinfe/semi-icons';
+import { BaseComponent, Button, Popconfirm } from '../../index';
+import copy from 'copy-text-to-clipboard';
+import { cssClasses, strings } from '@douyinfe/semi-foundation/chat/constants';
+import ChatBoxActionFoundation, { ChatBoxActionAdapter } from '@douyinfe/semi-foundation/chat/chatBoxActionFoundation';
+import LocaleConsumer from "../../locale/localeConsumer";
+import { Locale } from "../../locale/interface";
+import cls from 'classnames';
+
+const { PREFIX_CHAT_BOX_ACTION } = cssClasses;
+const { ROLE, MESSAGE_STATUS } = strings;
+
+interface ChatBoxActionProps extends ChatBoxProps {
+ customRenderFunc?: (props: { message?: Message; defaultActions?: ReactNode | ReactNode[]; className: string }) => ReactNode
+}
+
+interface ChatBoxActionState {
+ visible: boolean;
+ showAction: boolean
+}
+
+class ChatBoxAction extends BaseComponent {
+
+ static propTypes = {
+ role: PropTypes.object,
+ message: PropTypes.object,
+ showReset: PropTypes.bool,
+ onMessageBadFeedback: PropTypes.func,
+ onMessageGoodFeedback: PropTypes.func,
+ onMessageCopy: PropTypes.func,
+ onChatsChange: PropTypes.func,
+ onMessageDelete: PropTypes.func,
+ onMessageReset: PropTypes.func,
+ customRenderFunc: PropTypes.func,
+ }
+
+ copySuccessNode: ReactNode;
+ foundation: ChatBoxActionFoundation;
+ containerRef: React.RefObject;
+ popconfirmTriggerRef: React.RefObject;
+ clickOutsideHandler: any;
+
+ constructor(props: ChatBoxProps) {
+ super(props);
+ this.foundation = new ChatBoxActionFoundation(this.adapter);
+ this.copySuccessNode = null;
+ this.state = {
+ visible: false,
+ showAction: false,
+ };
+ this.clickOutsideHandler = null;
+ this.containerRef = React.createRef();
+ this.popconfirmTriggerRef = React.createRef();
+ }
+
+ componentDidMount(): void {
+ this.copySuccessNode = componentName="Chat" >
+ {(locale: Locale["Chat"]) => locale['copySuccess']}
+ ;
+ }
+
+ componentWillUnmount(): void {
+ this.foundation.destroy();
+ }
+
+ get adapter(): ChatBoxActionAdapter {
+ return {
+ ...super.adapter,
+ notifyDeleteMessage: () => {
+ const { message, onMessageDelete } = this.props;
+ onMessageDelete?.(message);
+ },
+ notifyMessageCopy: () => {
+ const { message, onMessageCopy } = this.props;
+ onMessageCopy?.(message);
+ },
+ copyToClipboardAndToast: () => {
+ const { message = {}, toast } = this.props;
+ if (typeof message.content === 'string') {
+ copy(message.content);
+ } else if (Array.isArray(message.content)) {
+ const content = message.content?.map(item => item.text).join('');
+ copy(content);
+ }
+ toast.success({
+ content: this.copySuccessNode
+ });
+ },
+ notifyLikeMessage: () => {
+ const { message, onMessageGoodFeedback } = this.props;
+ onMessageGoodFeedback?.(message);
+ },
+ notifyDislikeMessage: () => {
+ const { message, onMessageBadFeedback } = this.props;
+ onMessageBadFeedback?.(message);
+ },
+ notifyResetMessage: () => {
+ const { message, onMessageReset } = this.props;
+ onMessageReset?.(message);
+ },
+ setVisible: (visible) => {
+ this.setState({ visible });
+ },
+ setShowAction: (showAction) => {
+ this.setState({ showAction });
+ },
+ registerClickOutsideHandler: (cb: () => void) => {
+ if (this.clickOutsideHandler) {
+ this.adapter.unregisterClickOutsideHandler();
+ }
+ this.clickOutsideHandler = (e: React.MouseEvent): any => {
+ let el = this.popconfirmTriggerRef && this.popconfirmTriggerRef.current;
+ const target = e.target as Element;
+ const path = (e as any).composedPath && (e as any).composedPath() || [target];
+ if (
+ el && !(el as any).contains(target) &&
+ ! path.includes(el)
+ ) {
+ cb();
+ }
+ };
+ window.addEventListener('mousedown', this.clickOutsideHandler);
+ },
+ unregisterClickOutsideHandler: () => {
+ if (this.clickOutsideHandler) {
+ window.removeEventListener('mousedown', this.clickOutsideHandler);
+ this.clickOutsideHandler = null;
+ }
+ },
+ };
+ }
+
+ copyNode = () => {
+ return }
+ type='tertiary'
+ onClick={this.foundation.copyMessage}
+ className={`${PREFIX_CHAT_BOX_ACTION}-btn`}
+ />;
+ }
+
+ likeNode = () => {
+ const { message = {} } = this.props;
+ const { like } = message;
+ return : }
+ type='tertiary'
+ className={`${PREFIX_CHAT_BOX_ACTION}-btn`}
+ onClick={this.foundation.likeMessage}
+ />;
+ }
+
+ dislikeNode = () => {
+ const { message = {} } = this.props;
+ const { dislike } = message;
+ return : }
+ type='tertiary'
+ className={`${PREFIX_CHAT_BOX_ACTION}-btn`}
+ onClick={this.foundation.dislikeMessage}
+ />;
+ }
+
+ resetNode = () => {
+ return