瀏覽代碼

feat: add new plus components - Chat (#2248)

---------

Co-authored-by: pointhalo <[email protected]>
YyumeiZhang 1 年之前
父節點
當前提交
68160ef36c
共有 56 個文件被更改,包括 7301 次插入15 次删除
  1. 1 0
      content/order.js
  2. 1612 0
      content/plus/chat/index-en-US.md
  3. 1616 0
      content/plus/chat/index.md
  4. 1 1
      package.json
  5. 598 0
      packages/semi-foundation/chat/chat.scss
  6. 64 0
      packages/semi-foundation/chat/chatBoxActionFoundation.ts
  7. 68 0
      packages/semi-foundation/chat/constants.ts
  8. 306 0
      packages/semi-foundation/chat/foundation.ts
  9. 98 0
      packages/semi-foundation/chat/inputboxFoundation.ts
  10. 22 0
      packages/semi-foundation/chat/rtl.scss
  11. 125 0
      packages/semi-foundation/chat/variables.scss
  12. 5 0
      packages/semi-foundation/input/textareaFoundation.ts
  13. 828 0
      packages/semi-ui/chat/_story/chat.stories.jsx
  14. 90 0
      packages/semi-ui/chat/_story/chat.stories.tsx
  15. 141 0
      packages/semi-ui/chat/_story/constant.js
  16. 97 0
      packages/semi-ui/chat/attachment.tsx
  17. 253 0
      packages/semi-ui/chat/chatBox/chatBoxAction.tsx
  18. 42 0
      packages/semi-ui/chat/chatBox/chatBoxAvatar.tsx
  19. 88 0
      packages/semi-ui/chat/chatBox/chatBoxContent.tsx
  20. 31 0
      packages/semi-ui/chat/chatBox/chatBoxTitle.tsx
  21. 54 0
      packages/semi-ui/chat/chatBox/code.tsx
  22. 118 0
      packages/semi-ui/chat/chatBox/index.tsx
  23. 58 0
      packages/semi-ui/chat/chatContent.tsx
  24. 53 0
      packages/semi-ui/chat/hint.tsx
  25. 382 0
      packages/semi-ui/chat/index.tsx
  26. 170 0
      packages/semi-ui/chat/inputBox/index.tsx
  27. 126 0
      packages/semi-ui/chat/interface.ts
  28. 3 1
      packages/semi-ui/index.ts
  29. 8 2
      packages/semi-ui/input/textarea.tsx
  30. 9 0
      packages/semi-ui/locale/interface.ts
  31. 9 0
      packages/semi-ui/locale/source/ar.ts
  32. 9 0
      packages/semi-ui/locale/source/de.ts
  33. 9 0
      packages/semi-ui/locale/source/en_GB.ts
  34. 9 0
      packages/semi-ui/locale/source/en_US.ts
  35. 9 0
      packages/semi-ui/locale/source/es.ts
  36. 9 0
      packages/semi-ui/locale/source/fr.ts
  37. 9 0
      packages/semi-ui/locale/source/id_ID.ts
  38. 9 0
      packages/semi-ui/locale/source/it.ts
  39. 9 0
      packages/semi-ui/locale/source/ja_JP.ts
  40. 9 0
      packages/semi-ui/locale/source/ko_KR.ts
  41. 9 0
      packages/semi-ui/locale/source/ms_MY.ts
  42. 9 0
      packages/semi-ui/locale/source/nl_NL.ts
  43. 9 0
      packages/semi-ui/locale/source/pl_PL.ts
  44. 9 0
      packages/semi-ui/locale/source/pt_BR.ts
  45. 9 0
      packages/semi-ui/locale/source/ro.ts
  46. 9 0
      packages/semi-ui/locale/source/ru_RU.ts
  47. 9 0
      packages/semi-ui/locale/source/sv_SE.ts
  48. 9 0
      packages/semi-ui/locale/source/th_TH.ts
  49. 9 0
      packages/semi-ui/locale/source/tr_TR.ts
  50. 9 0
      packages/semi-ui/locale/source/vi_VN.ts
  51. 9 0
      packages/semi-ui/locale/source/zh_CN.ts
  52. 9 0
      packages/semi-ui/locale/source/zh_TW.ts
  53. 0 1
      packages/semi-ui/markdownRender/_story/markdownRender.stories.jsx
  54. 1 1
      packages/semi-ui/upload/index.tsx
  55. 26 0
      src/styles/docDemo.scss
  56. 9 9
      yarn.lock

+ 1 - 0
content/order.js

@@ -80,6 +80,7 @@ const order = [
     'toast',
     'toast',
     'configprovider',
     'configprovider',
     'locale',
     'locale',
+    'chat',
 ];
 ];
 let { exec } = require('child_process');
 let { exec } = require('child_process');
 let fs = require('fs');
 let fs = require('fs');

+ 1612 - 0
content/plus/chat/index-en-US.md

@@ -0,0 +1,1612 @@
+---
+localeCode: en-US
+order: 82
+category: Plus
+title:  Chat
+icon: doc-configprovider
+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    <Button>Click me</Button>\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 (
+        <>
+            <span style={{ display: 'flex', flexDirection: 'column', rowGap: '8px'}}>
+                <span style={{ display: 'flex', alignItems: 'center', columnGap: '10px'}}>
+                    Mode
+                    <RadioGroup onChange={onModeChange} value={mode} type={"button"}>
+                        <Radio value={'bubble'}>bubble</Radio>
+                        <Radio value={'noBubble'}>noBubble</Radio>
+                        <Radio value={'userBubble'}>userBubble</Radio>
+                    </RadioGroup>
+                </span>
+                <span style={{ display: 'flex', alignItems: 'center', columnGap: '10px'}}>
+                    Chat align
+                    <RadioGroup onChange={onAlignChange} value={align} type={"button"}>
+                        <Radio value={'leftRight'}>leftRight</Radio>
+                        <Radio value={'leftAlign'}>leftAlign</Radio>
+                    </RadioGroup>
+                </span>
+            </span>
+            <Chat 
+                key={align + mode}
+                align={align}
+                mode={mode}
+                uploadProps={uploadProps}
+                style={commonOuterStyle}
+                chats={message}
+                roleConfig={roleInfo}
+                onChatsChange={onChatsChange}
+                onMessageSend={onMessageSend}
+                onMessageReset={onMessageReset}
+                uploadTipProps={uploadTipProps}
+            />
+        </>
+    )
+}
+
+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 (
+        <Chat 
+            style={commonOuterStyle}
+            chats={message}
+            roleConfig={roleInfo}
+            onChatsChange={onChatsChange}
+            onMessageSend={onMessageSend}
+            uploadProps={uploadProps}
+        />
+    )
+}
+
+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 (
+        <Chat 
+            chats={message}
+            showStopGenerate={true}
+            style={commonOuterStyle}
+            onStopGenerator={onStopGenerator}
+            roleConfig={roleInfo}
+            onChatsChange={onChatsChange}
+            onMessageSend={onMessageSend}
+            uploadProps={uploadProps}
+        />
+    )
+}
+
+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 (
+        <>
+            <Chat
+                uploadProps={uploadProps}
+                style={commonOuterStyle}
+                chats={message}
+                roleConfig={roleInfo}
+                onChatsChange={onChatsChange}
+                onMessageSend={onMessageSend}
+                onMessageReset={onMessageReset}
+                uploadTipProps={uploadTipProps}
+                showClearContext
+            />
+        </>
+    )
+}
+
+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 <Avatar size="extra-small" shape="square" style={{ flexShrink: '0'}}>{role.name}</Avatar >
+                }
+            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 (<span className="title" >
+                        {role.name}
+                        <span className={'time'}>{formatTime}</span>
+                    </span>)
+            }
+            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 (
+        <>
+            <span style={{ display: 'flex', flexDirection: 'column', rowGap: 8, marginBottom: 5}}>
+                <span style={{ display: 'flex', alignItems: 'center', columnGap: 10}}>
+                    Avatar Render Mode
+                    <RadioGroup onChange={onAvatarChange} value={avatar} type="button">
+                    <Radio value={'default'}>default</Radio>
+                    <Radio value={'null'}>null</Radio>
+                    <Radio value={'custom'}>custom</Radio>
+                </RadioGroup>
+                </span>
+                <span style={{ display: 'flex', alignItems: 'center', columnGap: 10}}>
+                    Title Render mode
+                    <RadioGroup onChange={onTitleChange} value={title} type="button">
+                    <Radio value={'default'}>default</Radio>
+                    <Radio value={'null'}>null</Radio>
+                    <Radio value={'custom'}>custom</Radio>
+                </RadioGroup>
+                </span>
+            </span>
+            <Chat
+                chatBoxRenderConfig={{
+                    renderChatBoxTitle: customRenderTitle,
+                    renderChatBoxAvatar: customRenderAvatar
+                }} 
+                key={`${avatar}${title}`}
+                style={commonOuterStyle}
+                className={'component-chat-demo-custom-render'}
+                chats={message}
+                onChatsChange={onChatsChange}
+                onMessageSend={onMessageSend}
+                roleConfig={roleInfo}
+                uploadProps={uploadProps}
+            />
+        </>
+    );
+}
+
+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 <span 
+        className={className}
+        ref={myRef}
+    >
+        {defaultActions}
+        {<Dropdown
+            key="dropdown"
+            render={
+                <Dropdown.Menu >
+                    <Dropdown.Item icon={<IconForward />}>Share</Dropdown.Item>
+                </Dropdown.Menu>
+            }
+            trigger="click"
+            position="top"
+            getPopupContainer={getContainer}
+        >
+            <Button 
+                className='semi-chat-chatBox-action-btn'
+                icon={<IconMoreStroked/>}
+                theme='borderless'
+                type='tertiary'
+            />
+        </Dropdown>}
+    </span>
+});
+
+function CustomRender() {
+    const [message, setMessage] = useState(defaultMessage);
+
+    const customRenderAction = useCallback((props) => {
+        return <CustomActions {...props} />
+    }, []);
+
+    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 (
+        <Chat
+            chatBoxRenderConfig={{ 
+                renderChatBoxAction: customRenderAction 
+            }}
+            style={commonOuterStyle}
+            chats={message}
+            onChatsChange={onChatsChange}
+            onMessageSend={onMessageSend}
+            roleConfig={roleInfo}
+            uploadProps={uploadProps}
+        />
+    );
+}
+
+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 (<div style={{ 
+            transition: open ? 'height 0.4s ease, width 0.4s ease': 'height 0.4s ease',
+            height: open ? '30px' : '200px',
+            width: open ? '190px': '100%', 
+            background: 'var(--semi-color-tertiary-light-hover)', 
+            borderRadius: 16,
+            boxSizing: 'border-box',
+            marginBottom: 10,
+        }}
+        >
+        <span
+            ref={spanRef} 
+            style={{
+                display: !open ? 'none' : 'flex',
+                width: 'fit-content',
+                columnGap: 10,
+                background: 'var(--semi-color-tertiary-light-hover)', 
+                borderRadius: '16px',
+                padding: '5px 10px',
+                point: 'cursor',
+                fontSize: 14,
+                color: 'var(--semi-color-text-1)',
+            }}
+            onClick={onOpen} 
+        >
+            <span> Got {source.length} sources </span>
+            <AvatarGroup size="extra-extra-small" >
+                {source.map((s, index) => (<Avatar key={index} src={s.avatar}></Avatar>))}        
+            </AvatarGroup>
+        </span>
+        <span 
+            style={{
+                height: '100%',
+                boxSizing: 'border-box',
+                display: !open ? 'flex' : 'none',
+                flexDirection: 'column',
+                background: 'var(--semi-color-tertiary-light-hover)', borderRadius: '16px', padding: 12, boxSize: 'border-box'
+            }}
+            onClick={onClose}
+            >
+            <span style={{display: 'flex', alignItems: 'center', justifyContent: 'space-between',
+                    padding: '5px 10px', columnGap: 10, color: 'var(--semi-color-text-1)'
+            }}>
+                <span style={{fontSize: 14, fontWeight: 500}}>Source</span>
+                <IconChevronUp />
+            </span>
+            <span style={{display: 'flex', flexWrap: 'wrap', gap: 10,  overflow: 'scroll', padding: '5px 10px'}}>
+                {source.map(s => (
+                    <span style={{ 
+                        display: 'flex', 
+                        flexDirection: 'column', 
+                        rowGap: 5, 
+                        flexBasis: 150, 
+                        flexGrow: 1,
+                        border: "1px solid var(--semi-color-border)",
+                        borderRadius: 12,
+                        padding: 12,
+                        fontSize: 12
+                    }}>
+                        <span style={{display: 'flex', columnGap: 5, alignItems: 'center', }}>
+                            <Avatar style={{width: 16, height: 16, flexShrink: 0 }} shape="square" src={s.avatar} />
+                            <span style={{ color: 'var(--semi-color-text-2)', textOverflow: 'ellipsis'}}>{s.title}</span>
+                        </span>
+                        <span style={{
+                            color: 'var(--semi-color-primary)',
+                            fontSize: 12,
+                        }}
+                        >{s.subTitle}</span>
+                        <span style={{
+                            display: '-webkit-box',
+                            "-webkit-box-orient": 'vertical',
+                            WebkitLineClamp: '3', 
+                            textOverflow: 'ellipsis', 
+                            overflow: 'hidden',
+                            color: 'var(--semi-color-text-2)',
+                        }}>{s.content}</span>
+                    </span>))}
+                </span>
+            </span>
+        </div>
+    )
+}
+
+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 <div className={className}>
+            {message.source && <SourceCard source={message.source} />}
+            <MarkdownRender raw={message.content}/>
+        </div>
+    }, []);
+
+    return (
+        <Chat
+            style={commonOuterStyle}
+            chats={message}
+            roleConfig={roleInfo}
+            chatBoxRenderConfig={{ renderChatBoxContent: renderContent }}
+            onChatsChange={onChatsChange}
+            onMessageSend={onMessageSend}
+            uploadProps={uploadProps}
+        />
+    );
+}
+
+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 = (<span style={titleStyle}>
+                <Avatar size="extra-small" shape="square" src={role.avatar} />
+                {defaultNodes.title}
+            </span>)
+        }
+        return <div className={className}>
+            <div style={{ display: 'flex', flexDirection: 'column', rowGap: 4, alignItems: message.role === 'user' ? 'end' : ''}}>
+                {titleNode}
+                <div style={{ width: 'fit-content'}}>
+                    {defaultNodes.content}
+                </div>
+                {defaultNodes.action}
+            </div>
+        </div>
+    }, []);
+
+    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 ( <Chat
+        chatBoxRenderConfig={{ renderFullChatBox: customRenderChatBox }}
+        style={commonOuterStyle} 
+        chats={message}
+        onChatsChange={onChatsChange}
+        onMessageSend={onMessageSend}
+        roleConfig={roleInfo}
+        uploadProps={uploadProps}
+    />);
+}
+
+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 (<div style={inputStyle}>
+        <Form
+            getFormApi={formApi => api.current = formApi}
+        >
+            <strong>Information Chart</strong>
+            <Form.Input
+                field="name"
+                label="Name"
+                style={{ width: 250 }}
+                trigger='blur'
+            />
+            <Form.Upload
+                field='file'
+                label='File'
+                action='https://api.semi.design/upload'
+            >
+                <Button icon={<IconUpload />} theme="light">
+                    Upload
+                </Button>
+            </Form.Upload>
+        </Form>
+        <Button style={{ width: 'fit-content' }} onClick={onSubmit}>Submit</Button>
+    </div>);
+}
+
+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 (<CustomInputRender {...props} />)
+    }, []);    
+
+    return (
+        <Chat
+            renderInputArea={renderInputArea}
+            style={commonOuterStyle}
+            chats={message}
+            roleConfig={roleInfo}
+            onChatsChange={onChatsChange}
+            onMessageSend={onMessageSend}
+            uploadProps={uploadProps}
+        />
+    )
+}
+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 (
+        <Chat 
+            hints={hints}
+            onHintClick={onHintClick}
+            style={commonOuterStyle}
+            chats={message}
+            roleConfig={roleInfo}
+            onChatsChange={onChatsChange}
+            onMessageSend={onMessageSend}
+            onClear={onClear}
+            uploadProps={uploadProps}
+        />
+    )
+}
+
+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 <div style={commonHintStyle} onClick={onHintClick} key={index}>
+            {content}
+            <IconArrowRight style={{ marginLeft: 10 }}>click me</IconArrowRight>
+        </div>
+    }, []);
+
+    onClear = useCallback(() => {
+        setHints([]);
+    }, [])
+
+    return (
+        <Chat 
+            renderHintBox={renderHintBox}
+            hints={hints}
+            onHintClick={onHintClick}
+            style={commonOuterStyle}
+            chats={message}
+            roleConfig={roleInfo}
+            onChatsChange={onChatsChange}
+            onMessageSend={onMessageSend}
+            onClear={onClear}
+            uploadProps={uploadProps}
+        />
+    )
+}
+
+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
+
+<DesignToken/>

+ 1616 - 0
content/plus/chat/index.md

@@ -0,0 +1,1616 @@
+---
+localeCode: zh-CN
+order: 82
+category: Plus
+title:  Chat 对话
+icon: doc-configprovider
+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    <Button>Click me</Button>\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 (
+        <>
+            <span style={{ display: 'flex', flexDirection: 'column', rowGap: '8px'}}>
+                <span style={{ display: 'flex', alignItems: 'center', columnGap: '10px'}}>
+                    模式
+                    <RadioGroup onChange={onModeChange} value={mode} type={"button"}>
+                        <Radio value={'bubble'}>气泡</Radio>
+                        <Radio value={'noBubble'}>非气泡</Radio>
+                        <Radio value={'userBubble'}>用户会话气泡</Radio>
+                    </RadioGroup>
+                </span>
+                <span style={{ display: 'flex', alignItems: 'center', columnGap: '10px'}}>
+                    会话对齐方式
+                    <RadioGroup onChange={onAlignChange} value={align} type={"button"}>
+                        <Radio value={'leftRight'}>左右分布</Radio>
+                        <Radio value={'leftAlign'}>左对齐</Radio>
+                    </RadioGroup>
+                </span>
+            </span>
+            <Chat 
+                key={align + mode}
+                align={align}
+                mode={mode}
+                uploadProps={uploadProps}
+                style={commonOuterStyle}
+                chats={message}
+                roleConfig={roleInfo}
+                onChatsChange={onChatsChange}
+                onMessageSend={onMessageSend}
+                onMessageReset={onMessageReset}
+                uploadTipProps={uploadTipProps}
+            />
+        </>
+    )
+}
+
+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 (
+        <Chat 
+            style={commonOuterStyle}
+            chats={message}
+            roleConfig={roleInfo}
+            onChatsChange={onChatsChange}
+            onMessageSend={onMessageSend}
+            uploadProps={uploadProps}
+        />
+    )
+}
+
+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 (
+        <Chat 
+            chats={message}
+            showStopGenerate={true}
+            style={commonOuterStyle}
+            onStopGenerator={onStopGenerator}
+            roleConfig={roleInfo}
+            onChatsChange={onChatsChange}
+            onMessageSend={onMessageSend}
+            uploadProps={uploadProps}
+        />
+    )
+}
+
+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 (
+        <>
+            <Chat
+                uploadProps={uploadProps}
+                style={commonOuterStyle}
+                chats={message}
+                roleConfig={roleInfo}
+                onChatsChange={onChatsChange}
+                onMessageSend={onMessageSend}
+                onMessageReset={onMessageReset}
+                uploadTipProps={uploadTipProps}
+                showClearContext
+            />
+        </>
+    )
+}
+
+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 <Avatar size="extra-small" shape="square" style={{ flexShrink: '0'}}>{role.name}</Avatar >
+                }
+            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 (<span className="title" >
+                        {role.name}
+                        <span className={'time'}>{formatTime}</span>
+                    </span>)
+            }
+            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 (
+        <>
+            <span style={{ display: 'flex', flexDirection: 'column', rowGap: 8, marginBottom: 5}}>
+                <span style={{ display: 'flex', alignItems: 'center', columnGap: 10}}>
+                    头像渲染模式
+                    <RadioGroup onChange={onAvatarChange} value={avatar} type="button">
+                    <Radio value={'default'}>默认头像</Radio>
+                    <Radio value={'null'}>无头像</Radio>
+                    <Radio value={'custom'}>自定义头像</Radio>
+                </RadioGroup>
+                </span>
+                <span style={{ display: 'flex', alignItems: 'center', columnGap: 10}}>
+                    标题渲染模式
+                    <RadioGroup onChange={onTitleChange} value={title} type="button">
+                    <Radio value={'default'}>默认标题</Radio>
+                    <Radio value={'null'}>无标题</Radio>
+                    <Radio value={'custom'}>自定义标题</Radio>
+                </RadioGroup>
+                </span>
+            </span>
+            <Chat
+                chatBoxRenderConfig={{
+                    renderChatBoxTitle: customRenderTitle,
+                    renderChatBoxAvatar: customRenderAvatar
+                }} 
+                key={`${avatar}${title}`}
+                style={commonOuterStyle}
+                className={'component-chat-demo-custom-render'}
+                chats={message}
+                onChatsChange={onChatsChange}
+                onMessageSend={onMessageSend}
+                roleConfig={roleInfo}
+                uploadProps={uploadProps}
+            />
+        </>
+    );
+}
+
+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 <span 
+        className={className}
+        ref={myRef}
+    >
+        {defaultActions}
+        {<Dropdown
+            key="dropdown"
+            render={
+                <Dropdown.Menu >
+                    <Dropdown.Item icon={<IconForward />}>分享</Dropdown.Item>
+                </Dropdown.Menu>
+            }
+            trigger="click"
+            position="top"
+            getPopupContainer={getContainer}
+        >
+            <Button 
+                className='semi-chat-chatBox-action-btn'
+                icon={<IconMoreStroked/>}
+                theme='borderless'
+                type='tertiary'
+            />
+        </Dropdown>}
+    </span>
+});
+
+function CustomRender() {
+    const [message, setMessage] = useState(defaultMessage);
+
+    const customRenderAction = useCallback((props) => {
+        return <CustomActions {...props} />
+    }, []);
+
+    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 (
+        <Chat
+            chatBoxRenderConfig={{ 
+                renderChatBoxAction: customRenderAction 
+            }}
+            style={commonOuterStyle}
+            chats={message}
+            onChatsChange={onChatsChange}
+            onMessageSend={onMessageSend}
+            roleConfig={roleInfo}
+            uploadProps={uploadProps}
+        />
+    );
+}
+
+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 (<div style={{ 
+            transition: open ? 'height 0.4s ease, width 0.4s ease': 'height 0.4s ease',
+            height: open ? '30px' : '200px',
+            width: open ? '190px': '100%', 
+            background: 'var(--semi-color-tertiary-light-hover)', 
+            borderRadius: 16,
+            boxSizing: 'border-box',
+            marginBottom: 10,
+        }}
+        >
+        <span
+            ref={spanRef} 
+            style={{
+                display: !open ? 'none' : 'flex',
+                width: 'fit-content',
+                columnGap: 10,
+                background: 'var(--semi-color-tertiary-light-hover)', 
+                borderRadius: '16px',
+                padding: '5px 10px',
+                point: 'cursor',
+                fontSize: 14,
+                color: 'var(--semi-color-text-1)',
+            }}
+            onClick={onOpen} 
+        >
+            <span>基于{source.length}个搜索来源</span>
+            <AvatarGroup size="extra-extra-small" >
+                {source.map((s, index) => (<Avatar key={index} src={s.avatar}></Avatar>))}        
+            </AvatarGroup>
+        </span>
+        <span 
+            style={{
+                height: '100%',
+                boxSizing: 'border-box',
+                display: !open ? 'flex' : 'none',
+                flexDirection: 'column',
+                background: 'var(--semi-color-tertiary-light-hover)', borderRadius: '16px', padding: 12, boxSize: 'border-box'
+            }}
+            onClick={onClose}
+            >
+            <span style={{display: 'flex', alignItems: 'center', justifyContent: 'space-between',
+                    padding: '5px 10px', columnGap: 10, color: 'var(--semi-color-text-1)'
+            }}>
+                <span style={{fontSize: 14, fontWeight: 500}}>Source</span>
+                <IconChevronUp />
+            </span>
+            <span style={{display: 'flex', flexWrap: 'wrap', gap: 10,  overflow: 'scroll', padding: '5px 10px'}}>
+                {source.map(s => (
+                    <span style={{ 
+                        display: 'flex', 
+                        flexDirection: 'column', 
+                        rowGap: 5, 
+                        flexBasis: 150, 
+                        flexGrow: 1,
+                        border: "1px solid var(--semi-color-border)",
+                        borderRadius: 12,
+                        padding: 12,
+                        fontSize: 12
+                    }}>
+                        <span style={{display: 'flex', columnGap: 5, alignItems: 'center', }}>
+                            <Avatar style={{width: 16, height: 16, flexShrink: 0 }} shape="square" src={s.avatar} />
+                            <span style={{ color: 'var(--semi-color-text-2)', textOverflow: 'ellipsis'}}>{s.title}</span>
+                        </span>
+                        <span style={{
+                            color: 'var(--semi-color-primary)',
+                            fontSize: 12,
+                        }}
+                        >{s.subTitle}</span>
+                        <span style={{
+                            display: '-webkit-box',
+                            "-webkit-box-orient": 'vertical',
+                            WebkitLineClamp: '3', 
+                            textOverflow: 'ellipsis', 
+                            overflow: 'hidden',
+                            color: 'var(--semi-color-text-2)',
+                        }}>{s.content}</span>
+                    </span>))}
+                </span>
+            </span>
+        </div>
+    )
+}
+
+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 <div className={className}>
+            {message.source && <SourceCard source={message.source} />}
+            <MarkdownRender raw={message.content}/>
+        </div>
+    }, []);
+
+    return (
+        <Chat
+            style={commonOuterStyle}
+            chats={message}
+            roleConfig={roleInfo}
+            chatBoxRenderConfig={{ renderChatBoxContent: renderContent }}
+            onChatsChange={onChatsChange}
+            onMessageSend={onMessageSend}
+            uploadProps={uploadProps}
+        />
+    );
+}
+
+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 = (<span style={titleStyle}>
+                <Avatar size="extra-small" shape="square" src={role.avatar} />
+                {defaultNodes.title}
+            </span>)
+        }
+        return <div className={className}>
+            <div style={{ display: 'flex', flexDirection: 'column', rowGap: 4, alignItems: message.role === 'user' ? 'end' : ''}}>
+                {titleNode}
+                <div style={{ width: 'fit-content'}}>
+                    {defaultNodes.content}
+                </div>
+                {defaultNodes.action}
+            </div>
+        </div>
+    }, []);
+
+    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 ( <Chat
+        chatBoxRenderConfig={{ renderFullChatBox: customRenderChatBox }}
+        style={commonOuterStyle} 
+        chats={message}
+        onChatsChange={onChatsChange}
+        onMessageSend={onMessageSend}
+        roleConfig={roleInfo}
+        uploadProps={uploadProps}
+    />);
+}
+
+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 (<div style={inputStyle}>
+        <Form
+            getFormApi={formApi => api.current = formApi}
+        >
+            <strong>输入信息</strong>
+            <Form.Input
+                field="name"
+                label="名称(Input)"
+                style={{ width: 250 }}
+                trigger='blur'
+            />
+            <Form.Upload
+                field='file'
+                label='文档'
+                action='https://api.semi.design/upload'
+            >
+                <Button icon={<IconUpload />} theme="light">
+                    点击上传
+                </Button>
+            </Form.Upload>
+        </Form>
+        <Button style={{ width: 'fit-content' }} onClick={onSubmit}>提交</Button>
+    </div>);
+}
+
+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 (<CustomInputRender {...props} />)
+    }, []);    
+
+    return (
+        <Chat
+            renderInputArea={renderInputArea}
+            style={commonOuterStyle}
+            chats={message}
+            roleConfig={roleInfo}
+            onChatsChange={onChatsChange}
+            onMessageSend={onMessageSend}
+            uploadProps={uploadProps}
+        />
+    )
+}
+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 (
+        <Chat 
+            hints={hints}
+            onHintClick={onHintClick}
+            style={commonOuterStyle}
+            chats={message}
+            roleConfig={roleInfo}
+            onChatsChange={onChatsChange}
+            onMessageSend={onMessageSend}
+            onClear={onClear}
+            uploadProps={uploadProps}
+        />
+    )
+}
+
+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 <div style={commonHintStyle} onClick={onHintClick} key={index}>
+            {content}
+            <IconArrowRight style={{ marginLeft: 10 }}>click me</IconArrowRight>
+        </div>
+    }, []);
+
+    onClear = useCallback(() => {
+        setHints([]);
+    }, [])
+
+    return (
+        <Chat 
+            renderHintBox={renderHintBox}
+            hints={hints}
+            onHintClick={onHintClick}
+            style={commonOuterStyle}
+            chats={message}
+            roleConfig={roleInfo}
+            onChatsChange={onChatsChange}
+            onMessageSend={onMessageSend}
+            onClear={onClear}
+            uploadProps={uploadProps}
+        />
+    )
+}
+
+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[]) |发送消息 |
+
+## 设计变量
+
+<DesignToken/>
+

+ 1 - 1
package.json

@@ -50,7 +50,7 @@
         "@douyinfe/semi-site-banner": "^0.1.5",
         "@douyinfe/semi-site-banner": "^0.1.5",
         "@douyinfe/semi-site-doc-style": "0.0.4",
         "@douyinfe/semi-site-doc-style": "0.0.4",
         "@douyinfe/semi-site-header": "^0.0.29",
         "@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/mdx": "1.6.22",
         "@mdx-js/react": "^1.6.22",
         "@mdx-js/react": "^1.6.22",
         "@storybook/react-webpack5": "^7.0.7",
         "@storybook/react-webpack5": "^7.0.7",

+ 598 - 0
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';

+ 64 - 0
packages/semi-foundation/chat/chatBoxActionFoundation.ts

@@ -0,0 +1,64 @@
+import BaseFoundation, { DefaultAdapter } from "../base/foundation";
+
+export interface ChatBoxActionAdapter<P = Record<string, any>, S = Record<string, any>> extends DefaultAdapter<P, S> {
+    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 <P = Record<string, any>, S = Record<string, any>> extends BaseFoundation<ChatBoxActionAdapter<P, S>, P, S> {
+    constructor(adapter: ChatBoxActionAdapter<P, S>) {
+        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();
+    }
+
+}

+ 68 - 0
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,
+};

+ 306 - 0
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<P = Record<string, any>, S = Record<string, any>> extends DefaultAdapter<P, S> {
+    getContainerRef: () => React.RefObject<HTMLDivElement>;
+    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 <P = Record<string, any>, S = Record<string, any>> extends BaseFoundation<ChatAdapter<P, S>, P, S> {
+
+    animation: any;
+
+    constructor(adapter: ChatAdapter<P, S>) {
+        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);
+        });
+    }
+}
+

+ 98 - 0
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<P = Record<string, any>, S = Record<string, any>> extends DefaultAdapter<P, S> {
+    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 <P = Record<string, any>, S = Record<string, any>> extends BaseFoundation<InputBoxAdapter<P, S>, P, S> {
+    constructor(adapter: InputBoxAdapter<P, S>) {
+        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();
+            }
+        }
+    }
+
+}

+ 22 - 0
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);
+        } 
+    }
+   
+}

+ 125 - 0
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

+ 5 - 0
packages/semi-foundation/input/textareaFoundation.ts

@@ -171,6 +171,11 @@ export default class TextAreaFoundation extends BaseFoundation<TextAreaAdapter>
     }
     }
 
 
     handleKeyDown(e: any) {
     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);
         this._adapter.notifyKeyDown(e);
         if (e.keyCode === 13) {
         if (e.keyCode === 13) {
             this._adapter.notifyPressEnter(e);
             this._adapter.notifyPressEnter(e);

+ 828 - 0
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 (
+        <>
+            <div style={{margin: 10, display: 'flex', flexDirection: 'column', rowGap: 5}}>
+                <span style={{ display: 'flex', alignItems: 'center', columnGap: '10px'}}>
+                    展示清除上下文按钮:
+                    <Switch checked={showClearContext} onChange={onSwitchChange}/>
+                </span>
+                <span style={{ display: 'flex', alignItems: 'center', columnGap: '10px'}}>
+                    模式:
+                    <RadioGroup onChange={onModeChange} value={mode} type="button">
+                        <Radio value={'bubble'}>气泡</Radio>
+                        <Radio value={'noBubble'}>非气泡</Radio>
+                        <Radio value={'userBubble'}>用户会话气泡</Radio>
+                    </RadioGroup>
+                </span>
+                <span style={{ display: 'flex', alignItems: 'center', columnGap: '10px'}}>
+                    布局:
+                    <RadioGroup onChange={onAlignChange} value={align} type="button">
+                        <Radio value={'leftRight'}>左右分布</Radio>
+                        <Radio value={'leftAlign'}>全左</Radio>
+                    </RadioGroup>
+                </span>
+                <span style={{ display: 'flex', alignItems: 'center', columnGap: '10px'}}>
+                    按键发送策略:
+                    <RadioGroup onChange={onSendHotKeyChange} value={sendHotKey} type="button">
+                        <Radio value={'enter'}>enter</Radio>
+                        <Radio value={'shift+enter'}>shift+enter</Radio>
+                    </RadioGroup>
+                </span>
+            </div>
+            <div style={{ height: 650}}>
+                <Chat
+                    key={key}
+                    style={commonOuterStyle}
+                    chats={message}
+                    hints={hints}
+                    roleConfig={roleInfo}
+                    onClear={onClear}
+                    onMessageSend={onMessageSend}
+                    onMessageDelete={onMessageDelete}
+                    onMessageGoodFeedback={onMessageGoodFeedback}
+                    onMessageBadFeedback={onMessageBadFeedback}
+                    onChatsChange={onChatsChange}
+                    onMessageReset={onMessageReset}
+                    onInputChange={onInputChange}
+                    onHintClick={onHintClick}
+                    uploadProps={uploadProps}
+                    uploadTipProps={{
+                        content: '自定义输入提示'
+                    }}
+                    mode={mode} 
+                    align={align}
+                    sendHotKey={sendHotKey} 
+                    showClearContext={showClearContext}
+                />
+            </div>
+        </>
+    )
+}
+
+export const Attachment = () => {
+    const [message, setMessage] = useState(infoWithAttachment);
+
+    return (
+        <div
+            style={{ height: 600}}
+        >
+            <Chat 
+                placeholder={'不处理输入信息,仅用于展示附件'}
+                style={commonOuterStyle}
+                chats={message}
+                roleConfig={roleInfo}
+                uploadProps={uploadProps}
+            />
+        </div>
+    )
+}
+
+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 (<div style={{   
+        display: 'flex', 
+        flexDirection: 'column', 
+        border: '1px solid var(--semi-color-border)',
+        margin: '8px 16px',
+        borderRadius: 8,
+        padding: 8
+    }}>
+        <Form
+            getFormApi={formApi => api.current = formApi}
+        >
+            <strong>输入信息</strong>
+            <Form.Input
+                field="name"
+                label="名称(Input)"
+                style={{ width: 250 }}
+                trigger='blur'
+            />
+            <Form.Upload
+                field='file'
+                label='文档'
+                action='https://api.semi.design/upload'
+            >
+                <Button icon={<IconUpload />} theme="light">
+                    点击上传
+                </Button>
+            </Form.Upload>
+        </Form>
+        <Button style={{ width: 'fit-content' }} onClick={onSubmit}>提交</Button>
+    </div>);
+}
+
+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 (<CustomInputRender {...props} />)
+    }, []);     
+
+    return (
+        <div
+            style={{ height: 600}}
+        >
+            <Chat 
+                style={commonOuterStyle}
+                chats={message}
+                roleConfig={roleInfo}
+                onChatsChange={onChatsChange}
+                onMessageSend={onMessageSend}
+                renderInputArea={renderInputArea}
+                uploadProps={uploadProps}
+            />
+        </div>
+    )
+}
+
+export const CustomRenderAvatar = (props) => {
+    const customRenderAvatar = useCallback((props)=> {
+        const { role, defaultAvatar } = props;
+        return <Avatar size="extra-small" shape="square" style={{ flexShrink: '0'}}>{role.name}</Avatar >
+    }, []);
+
+    const customRenderTitle =  useCallback((props)=> null, []);
+
+    return (<div
+        style={{ height: 600 }}
+    >
+        <Chat 
+            style={commonOuterStyle}
+            chats={initMessage.slice(0, 4)}
+            roleConfig={roleInfo}
+            chatBoxRenderConfig={{
+                renderChatBoxTitle: customRenderTitle,
+                renderChatBoxAvatar: customRenderAvatar
+            }}
+            uploadProps={uploadProps}
+        />
+    </div>);
+}
+
+export const CustomRenderTitle = (props) => {
+    const customRenderTitle = useCallback((props) => {
+        const { role, message, defaultTitle } = props;
+        if (message.role === 'user') {
+            return null;
+        }
+        return <span style={{ display:' flex', alignItems: 'center', justifyContent: 'center', columnGap: '10px'}}>
+            <Avatar size="extra-small" shape="square" src={role.avatar} />
+            {defaultTitle}
+        </span>
+    }, []);
+
+    const customRenderAvatar = useCallback((props)=> null, []);
+
+    return (<div
+        style={{ height: 600}}
+    >
+        <Chat
+            placeholder={"不处理输入信息,仅用于展示自定义头像和标题"}
+            style={commonOuterStyle}
+            chats={simpleInitMessage}
+            roleConfig={roleInfo}
+            chatBoxRenderConfig={{
+                renderChatBoxTitle: customRenderTitle,
+                renderChatBoxAvatar: customRenderAvatar
+            }}
+            uploadProps={uploadProps}
+        />
+    </div>);
+}
+
+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 <div className={className}>
+            <div style={{ display: 'flex', flexDirection: 'column', rowGap: 5, alignItems: message.role === 'user' ? 'end' : ''}}>
+                <span style={{color: 'var(--semi-color-text-2', fontSize: '12px'}}>{formattedDate}</span>
+                <div style={{ width: 'fit-content'}}>
+                    {defaultNodes.content}
+                </div>
+                {defaultNodes.action}
+            </div>
+        </div>
+    }, []);
+
+    return (<div
+        style={{ height: 600}}
+    >
+        <Chat
+            style={commonOuterStyle} 
+            chats={simpleInitMessage}
+            roleConfig={roleInfo}
+            chatBoxRenderConfig={{
+                renderFullChatBox: customRenderChatBox
+            }}
+            uploadProps={uploadProps}
+        />
+    </div>);
+}
+
+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 <span 
+        className={className}
+        ref={myRef}
+    >
+        {defaultActions.map((item, index)=> {
+            return <span key={index}>{item}</span>
+        })}
+        {<Dropdown
+            key="dropdown"
+            render={
+                <Dropdown.Menu >
+                    <Dropdown.Item icon={<IconForward />}>分享</Dropdown.Item>
+                </Dropdown.Menu>
+            }
+            trigger="click"
+            position="top"
+            getPopupContainer={getContainer}
+        >
+            <Button 
+                className='semi-chat-chatBox-action-btn'
+                icon={<IconMoreStroked/>}
+                theme='borderless'
+                type='tertiary'
+            />
+        </Dropdown>}
+    </span>
+});
+
+export const CustomRenderAction = () => {
+    const [message, setMessage] = useState(simpleInitMessage);
+    const customRenderAction = useCallback((props) => {
+        return <CustomActions {...props} />
+    }, []);
+
+    const onChatsChange = useCallback((chats) => {
+        setMessage(chats);
+    }, []);
+
+    return (<div
+        style={{ height: 600}}
+    >
+        <Chat 
+            chats={message}
+            onChatsChange={onChatsChange}
+            style={commonOuterStyle}
+            roleConfig={roleInfo}
+            chatBoxRenderConfig={{
+                renderChatBoxAction: customRenderAction
+            }}
+            uploadProps={uploadProps}
+        />
+    </div>);
+}
+
+export const CustomRenderContent = () => {
+    const renderContent = useCallback((props) => {
+        const { role, message, defaultNode, className } = props;
+        return <div className={className}>
+            <span>---custom render content---</span>
+            <MarkdownRender raw={message?.content}/>
+        </div>
+    }, []);
+
+    return (<div
+        style={{ height: 600}}
+    >
+        <Chat 
+            style={commonOuterStyle}
+            chats={simpleInitMessage}
+            roleConfig={roleInfo}
+            chatBoxRenderConfig={{
+                renderChatBoxContent: renderContent
+            }}
+            uploadProps={uploadProps}
+        />
+    </div>);
+}
+
+// const Card = (source) => {
+//     return (<span className="demo-card">
+//         <span className="demo-card-title"></span>
+//         <span className="demo-card-link"></span>
+//         <span className="demo-card-content"></span>
+//     </span>)
+// }
+
+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 (<div style={{ 
+            transition: open ? 'height 0.4s ease, width 0.4s ease': 'height 0.4s ease',
+            height: open ? '30px' : '184px',
+            width: open ? '237px': '100%', 
+            background: 'var(--semi-color-tertiary-light-hover)', 
+            borderRadius: 16,
+            boxSizing: 'border-box',
+        }}
+        >
+        <span
+            ref={spanRef} 
+            style={{
+                display: !open ? 'none' : 'flex',
+                width: 'fit-content',
+                columnGap: 10,
+                background: 'var(--semi-color-tertiary-light-hover)', 
+                borderRadius: '16px',
+                padding: '5px 10px',
+                point: 'cursor',
+                fontSize: 14,
+                color: 'var(--semi-color-text-1)',
+            }}
+            onClick={onOpen} 
+        >
+            <span>基于{props.sources.length}个搜索来源</span>
+            <AvatarGroup size="extra-extra-small" >
+                {props.sources.map((s, index) => (<Avatar key={index} src={s.avatar}></Avatar>))}        
+            </AvatarGroup>
+        </span>
+        <span 
+            style={{
+                height: '100%',
+                boxSizing: 'border-box',
+                display: !open ? 'flex' : 'none',
+                flexDirection: 'column',
+                background: 'var(--semi-color-tertiary-light-hover)', borderRadius: '16px', padding: 12, boxSize: 'border-box'
+            }}
+            onClick={onClose}
+            >
+            <span style={{display: 'flex', alignItems: 'center', justifyContent: 'space-between',
+                    padding: '5px 10px', columnGap: 10, color: 'var(--semi-color-text-1)'
+            }}>
+                <span style={{fontSize: 14, fontWeight: 500}}>Source</span>
+                <IconChevronUp />
+            </span>
+            <span style={{display: 'flex', flexWrap: 'wrap', gap: 10,  overflow: 'scroll', padding: '5px 10px'}}>
+                {props.sources.map(s => (
+                    <span style={{ 
+                        display: 'flex', 
+                        flexDirection: 'column', 
+                        rowGap: 5, 
+                        flexBasis: 150, 
+                        flexGrow: 1,
+                        border: "1px solid var(--semi-color-border)",
+                        borderRadius: 12,
+                        padding: 12,
+                        fontSize: 12
+                    }}>
+                        <span style={{display: 'flex', columnGap: 5, alignItems: 'center', }}>
+                            <Avatar style={{width: 16, height: 16, flexShrink: 0 }} shape="square" src={s.avatar} />
+                            <span style={{ color: 'var(--semi-color-text-2)', textOverflow: 'ellipsis'}}>{s.title}</span>
+                        </span>
+                        <span style={{
+                            color: 'var(--semi-color-primary)',
+                            fontSize: 12,
+                        }}
+                        >{s.subTitle}</span>
+                        <span style={{
+                            display: '-webkit-box',
+                            "-webkit-box-orient": 'vertical',
+                            WebkitLineClamp: '3', 
+                            textOverflow: 'ellipsis', 
+                            overflow: 'hidden',
+                            color: 'var(--semi-color-text-2)',
+                        }}>{s.content}</span>
+                    </span>))}
+                </span>
+            </span>
+        </div>
+    )
+}
+
+
+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 <div className={className}>
+            <SourceCard sources={message?.source} />
+            <MarkdownRender raw={message?.content}/>
+        </div>
+    }, []);
+
+    return (<div
+        style={{ height: 600}}
+    >
+        <Chat 
+            style={commonOuterStyle}
+            chats={chat}
+            roleConfig={roleInfo}
+            chatBoxRenderConfig={{
+                renderChatBoxContent: renderContent
+            }}
+            uploadProps={uploadProps}
+        />
+        <div></div>
+    </div>);
+}
+
+export const LeftAlign =  () => {
+    return (<div
+        style={{ height: 600}}
+    >
+        <Chat
+            style={commonOuterStyle} 
+            chats={simpleInitMessage}
+            roleConfig={roleInfo}
+            align='leftAlign'
+            uploadProps={uploadProps}
+        />
+    </div>);
+}
+
+export const MessageStatus = () => {
+    const messages = [
+        initMessage[1],
+        {
+            ...initMessage[2],
+            content: '请求错误',
+            status: 'error'
+        },
+        {
+            id: 'loading',
+            role: 'assistant',
+            status: 'loading'
+        },
+    ]
+    return (<div
+        style={{ height: 600}}
+    >
+        <Chat
+            style={commonOuterStyle} 
+            chats={messages}
+            roleConfig={roleInfo}
+            uploadProps={uploadProps}
+        />
+    </div>);
+}
+
+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 (
+    <div
+        style={{ height: 300}}
+    >
+        <Chat 
+            style={commonOuterStyle}
+            chats={message}
+            roleConfig={roleInfo}
+            onChatsChange={onChatsChange}
+            onMessageSend={onMessageSend}
+            onStopGenerator={onStopGenerator}
+            showStopGenerate={true}
+            uploadProps={uploadProps}
+        />
+    </div>
+    );
+}
+
+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 <div style={commonHintStyle} onClick={onHintClick} key={index}>
+            {content}
+            <IconArrowRight style={{ marginLeft: 10 }}>click me</IconArrowRight>
+        </div>
+    }, []);
+
+    const onChatsChange = useCallback((chats) => {
+        setMessage(chats);
+    }, []);
+
+    const onHintClick = useCallback((hint) => {
+        setHint([]);
+    }, []);
+
+    const onClear = useCallback(() => {
+        setHint([]);
+    }, []);
+
+    return <div
+        style={{ height: 600}}
+    >
+        <Chat 
+            style={commonOuterStyle}
+            chats={message}
+            onChatsChange={onChatsChange}
+            onHintClick={onHintClick}
+            hints={hint}
+            roleConfig={roleInfo}
+            renderHintBox={renderHintBox}
+            onClear={onClear}
+            uploadProps={uploadProps}
+        />
+    </div>
+}

+ 90 - 0
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 (
+        <div
+            style={{ height: 600}}
+        >
+            <Chat 
+                style={commonOuterStyle}
+                chats={message}
+                hints={hints}
+                roleConfig={roleInfo}
+                onClear={onClear}
+                onMessageSend={onMessageSend}
+                onMessageDelete={onMessageDelete}
+                onMessageGoodFeedback={onMessageGoodFeedback}
+                onMessageBadFeedback={onMessageBadFeedback}
+                onChatsChange={onChatsChange}
+                onMessageReset={onMessageReset}
+                onInputChange={onInputChange}
+                onHintClick={onHintClick}
+            />
+        </div>
+    )
+});

+ 141 - 0
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    <div>\n      <h1>Hello, Semi Design!</h1>\n      <Button onClick={handleClick}>Click me</Button>\n    </div>\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
+};

+ 97 - 0
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 <a
+        href={url}
+        target="_blank"
+        className={`${PREFIX_ATTACHMENT}-file`} rel="noreferrer"
+    >
+        <IconBriefStroked size="extra-large" className={`${PREFIX_ATTACHMENT}-file-icon`}/>
+        <div className={`${PREFIX_ATTACHMENT}-file-info`}>
+            <span className={`${PREFIX_ATTACHMENT}-file-title`}>{name}</span>
+            <span className={`${PREFIX_ATTACHMENT}-file-metadata`}>
+                <span className={`${PREFIX_ATTACHMENT}-file-type`}>{type}</span>
+                {type ? ' · ' : ''}{size}
+            </span>
+        </div>
+    </a>
+})
+
+export const ImageAttachment = React.memo((props: {src: string}) => {
+    const { src } = props;
+    return <Image
+    className={`${PREFIX_ATTACHMENT}-img`}
+    width={60}
+    height={60}
+    src={src}
+/>
+})
+
+const Attachment = React.memo((props: AttachmentProps) => {
+    const { attachment, onClear, showClear = true, className } = props;
+
+    return (
+        <div 
+            className={cls(PREFIX_ATTACHMENT, { [className]: className })}
+        >
+            {
+                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 <div 
+                        className={`${PREFIX_ATTACHMENT}-item`}
+                        key={item.uid}
+                    >
+                        {isImg ? (
+                            <ImageAttachment src={item.url} />
+                        ) : (
+                            <FileAttachment
+                                url={item.url}
+                                name={item.name}
+                                size={item.size}
+                                type={realType}
+                            />
+                        )}
+                        {showClear && <IconClear 
+                            size="large" 
+                            className={`${PREFIX_ATTACHMENT}-clear`}
+                            onClick={()=> {
+                                onClear && onClear(item);
+                            }}
+                        />}
+                       {showProcess && <Progress percent={percent}  type="circle" size="small"  width={30} className={`${PREFIX_ATTACHMENT}-process`} aria-label="upload progress" />}
+                    </div>;
+                })
+            }
+        </div>
+    );  
+});
+
+export default Attachment;

+ 253 - 0
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<ChatBoxActionProps, ChatBoxActionState> {
+
+    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<HTMLDivElement>;
+    popconfirmTriggerRef: React.RefObject<HTMLSpanElement>;
+    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<HTMLDivElement>();
+        this.popconfirmTriggerRef = React.createRef<HTMLSpanElement>();
+    }
+
+    componentDidMount(): void {
+        this.copySuccessNode = <LocaleConsumer<Locale["Chat"]> componentName="Chat" >
+            {(locale: Locale["Chat"]) => locale['copySuccess']}
+        </LocaleConsumer>;
+    }
+
+    componentWillUnmount(): void {
+        this.foundation.destroy();
+    }
+
+    get adapter(): ChatBoxActionAdapter<ChatBoxActionProps, ChatBoxActionState> {
+        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 <Button
+            key={'copy'}
+            theme='borderless'
+            icon={<IconCopyStroked />}
+            type='tertiary'
+            onClick={this.foundation.copyMessage}
+            className={`${PREFIX_CHAT_BOX_ACTION}-btn`}
+        />;
+    }
+
+    likeNode = () => {
+        const { message = {} } = this.props;
+        const { like } = message;
+        return <Button
+            key={'like'}
+            theme='borderless'
+            icon={like ? <IconLikeThumb /> : <IconThumbUpStroked /> }
+            type='tertiary'
+            className={`${PREFIX_CHAT_BOX_ACTION}-btn`}
+            onClick={this.foundation.likeMessage}
+        />;
+    }
+
+    dislikeNode = () => {
+        const { message = {} } = this.props;
+        const { dislike } = message;
+        return <Button
+            theme='borderless'
+            key={'dislike'}
+            icon={dislike ? <IconLikeThumb className={`${PREFIX_CHAT_BOX_ACTION}-icon-flip`} /> : <IconThumbUpStroked className={'semi-chat-chatBox-action-icon-flip'} />}
+            type='tertiary'
+            className={`${PREFIX_CHAT_BOX_ACTION}-btn`}
+            onClick={this.foundation.dislikeMessage}
+        />;
+    }
+
+    resetNode = () => {
+        return <Button
+            key={'reset'}
+            theme='borderless'
+            icon={<IconRedoStroked className={`${PREFIX_CHAT_BOX_ACTION}-icon-redo`}/>}
+            type='tertiary'
+            onClick={this.foundation.resetMessage}
+            className={`${PREFIX_CHAT_BOX_ACTION}-btn`}
+        />;
+    }
+
+    deleteNode = () => {
+        const deleteMessage = (<LocaleConsumer<Locale["Chat"]> componentName="Chat" >
+            {(locale: Locale["Chat"]) => locale['deleteConfirm']}
+        </LocaleConsumer>);
+        return (<Popconfirm
+            trigger="custom"
+            visible={this.state.visible}
+            key={'delete'}
+            title={deleteMessage}
+            onConfirm={this.foundation.deleteMessage}
+            onCancel={this.foundation.hideDeletePopup}
+            position='top'
+        >
+            <span 
+                ref={this.popconfirmTriggerRef}
+                className={`${PREFIX_CHAT_BOX_ACTION}-delete-wrap`}
+            >
+                <Button
+                    theme='borderless'
+                    icon={<IconDeleteStroked />}
+                    type='tertiary'
+                    className={`${PREFIX_CHAT_BOX_ACTION}-btn`}
+                    onClick={this.foundation.showDeletePopup}
+                />
+            </span>
+        </Popconfirm>);
+    }
+
+    render() {
+        const { message = {}, lastChat } = this.props;
+        const { showAction } = this.state;
+        const { role, status = MESSAGE_STATUS.COMPLETE } = message;
+        const complete = status === MESSAGE_STATUS.COMPLETE ;
+        const showFeedback = role !== ROLE.USER && complete;
+        const showReset = lastChat && role === ROLE.ASSISTANT;
+        const finished = status !== MESSAGE_STATUS.LOADING && status !== MESSAGE_STATUS.INCOMPLETE;
+        const wrapCls = cls(PREFIX_CHAT_BOX_ACTION, { 
+            [`${PREFIX_CHAT_BOX_ACTION}-show`]: showReset && finished || showAction,
+            [`${PREFIX_CHAT_BOX_ACTION}-hidden`]: !finished,
+        });
+        const { customRenderFunc } = this.props;
+        if (customRenderFunc) {
+            const actionNodes = [];
+            complete && actionNodes.push(this.copyNode());
+            showFeedback && actionNodes.push(this.likeNode());
+            showFeedback && actionNodes.push(this.dislikeNode());
+            showReset && actionNodes.push(this.resetNode());
+            actionNodes.push(this.deleteNode());
+            return customRenderFunc({
+                message,
+                defaultActions: actionNodes,
+                className: wrapCls
+            });
+        }
+        return <div className={wrapCls} ref={this.containerRef}>
+            {complete && this.copyNode()}
+            {showFeedback && this.likeNode()}
+            {showFeedback && this.dislikeNode()}
+            {showReset && this.resetNode()}
+            {this.deleteNode()}
+        </div>;
+    }
+}
+
+export default ChatBoxAction;

+ 42 - 0
packages/semi-ui/chat/chatBox/chatBoxAvatar.tsx

@@ -0,0 +1,42 @@
+import React, { useMemo, ReactNode, ReactElement } from 'react';
+import Avatar from '../../avatar';
+import { Metadata } from '../interface';
+import { cssClasses } from '@douyinfe/semi-foundation/chat/constants';
+import cls from 'classnames';
+
+const { PREFIX_CHAT_BOX } = cssClasses;
+
+interface ChatBoxAvatarProps {
+    children?: string;
+    role?: Metadata;
+    continueSend?: boolean;
+    customRenderFunc?: (props: {role?: Metadata; defaultAvatar?: ReactNode}) => ReactNode
+}
+
+const ChatBoxAvatar = React.memo((props: ChatBoxAvatarProps) => {
+    const { role, customRenderFunc, continueSend } = props;
+
+    const node = useMemo(() => {
+        const { avatar, color } = role;
+        return (<Avatar
+            className={cls(`${PREFIX_CHAT_BOX}-avatar`,
+                {
+                    [`${PREFIX_CHAT_BOX}-avatar-hidden`]: continueSend
+                })}
+            src={avatar}
+            size="extra-small"
+        >
+        </Avatar>);
+    }, [role]);
+
+    if (customRenderFunc && typeof customRenderFunc === 'function') {
+        return customRenderFunc({
+            role, 
+            defaultAvatar: node
+        }) as ReactElement;
+    }
+    return node;
+});
+
+export default ChatBoxAvatar;
+

+ 88 - 0
packages/semi-ui/chat/chatBox/chatBoxContent.tsx

@@ -0,0 +1,88 @@
+import React, { ReactElement, ReactNode, useMemo } from 'react';
+import cls from 'classnames';
+import { Message, Metadata } from '../interface';
+import MarkdownRender from '../../markdownRender';
+import { cssClasses, strings } from '@douyinfe/semi-foundation/chat/constants';
+import { MDXProps } from 'mdx/types';
+import { FileAttachment, ImageAttachment } from '../attachment';
+import Code from './code';
+
+const { PREFIX_CHAT_BOX } = cssClasses;
+const { MESSAGE_STATUS, MODE, ROLE } = strings;
+
+interface ChatBoxContentProps {
+    mode?: 'bubble' | 'noBubble' | 'userBubble';
+    customMarkDownComponents?: MDXProps['components'];
+    children?: string;
+    role?: Metadata;
+    message?: Message;
+    customRenderFunc?: (props: {message?: Message; role?: Metadata; defaultContent?: ReactNode | ReactNode[]; className?: string}) => ReactNode
+}
+
+const ChatBoxContent = (props: ChatBoxContentProps) => {
+    const { message = {}, customRenderFunc, role: roleInfo, customMarkDownComponents, mode } = props;
+    const { content, role, status } = message;
+
+    const markdownComponents = useMemo(() => ({
+        'code': Code,
+        'SemiFile': FileAttachment,
+        'img': ImageAttachment,
+        ...customMarkDownComponents
+    }), [customMarkDownComponents]);
+
+    const wrapCls = useMemo(() => {
+        const isUser = role === ROLE.USER;
+        const bubble = mode === MODE.BUBBLE;
+        const userBubble = mode === MODE.USER_BUBBLE && isUser;
+        return cls(`${PREFIX_CHAT_BOX}-content`, {
+            [`${PREFIX_CHAT_BOX}-content-${mode}`]: bubble || userBubble,
+            [`${PREFIX_CHAT_BOX}-content-user`]: (bubble && isUser) || userBubble,
+            [`${PREFIX_CHAT_BOX}-content-error`]: status === MESSAGE_STATUS.ERROR && (bubble || userBubble)
+        });
+    }, [role, status]);
+
+    const node = useMemo(() => {
+        if (status === MESSAGE_STATUS.LOADING) {
+            return <span className={`${PREFIX_CHAT_BOX}-content-loading`} >
+                <span className={`${PREFIX_CHAT_BOX}-content-loading-item`} />
+            </span>;
+        } else {
+            let realContent = '';
+            if (typeof content === 'string') {
+                realContent = content;
+            } else if (Array.isArray(content)) {
+                realContent = content.map((item)=> {
+                    if (item.type === 'text') {
+                        return item.text;
+                    } else if (item.type === 'image_url') {
+                        return `![image](${item.image_url.url})`;
+                    } else if (item.type === 'file_url') {
+                        const { name, size, url, type } = item.file_url;
+                        const realType = name.split('.').pop() ?? type?.split('/').pop();
+                        return `<SemiFile url={'${url}'} name={'${name}'} size={'${size}'} type={'${realType}'}></SemiFile>`;
+                    }
+                    return '';
+                }).join('\n\n');
+            }
+            return (<>
+                <MarkdownRender
+                    raw={realContent}
+                    components={markdownComponents as any}
+                />
+            </>);
+        }
+    }, [status, content]);
+        
+    if (customRenderFunc) {
+        return customRenderFunc({ 
+            message, 
+            role: roleInfo,
+            defaultContent: node,
+            className: wrapCls,
+        }) as ReactElement;
+    } else {
+        return <div className={wrapCls}>{node}</div>; 
+    } 
+};
+
+export default ChatBoxContent;

+ 31 - 0
packages/semi-ui/chat/chatBox/chatBoxTitle.tsx

@@ -0,0 +1,31 @@
+import React, { useMemo, ReactNode, ReactElement } from 'react';
+import { Message, Metadata } from '../interface';
+import { cssClasses } from '@douyinfe/semi-foundation/chat/constants';
+const { PREFIX_CHAT_BOX } = cssClasses;
+
+interface ChatBoxTitleProps {
+    children?: ReactNode | undefined | any;
+    role?: Metadata;
+    message?: Message;
+    customRenderFunc?: (props: {role?: Metadata; message: Message; defaultTitle?: ReactNode}) => ReactNode
+}
+
+const ChatBoxTitle = React.memo((props: ChatBoxTitleProps) => {
+    const { role, message, customRenderFunc } = props;
+    const title = useMemo(() => {
+        return <span
+            className={`${PREFIX_CHAT_BOX}-title`}
+        >{role?.name}</span>;
+    }, [role]);
+
+    if (customRenderFunc && typeof customRenderFunc === 'function') {
+        return customRenderFunc({
+            role,
+            message,
+            defaultTitle: title
+        }) as ReactElement;
+    }
+    return title;
+});
+
+export default ChatBoxTitle;

+ 54 - 0
packages/semi-ui/chat/chatBox/code.tsx

@@ -0,0 +1,54 @@
+import React, { useCallback, useMemo, useState } from 'react';
+import { PropsWithChildren } from 'react';
+import { cssClasses } from '@douyinfe/semi-foundation/chat/constants';
+import copy from 'copy-text-to-clipboard';
+import { IconCopyStroked, IconTick } from '@douyinfe/semi-icons';
+import { nth } from 'lodash';
+import { code } from '../../markdownRender/components';
+// code's default height type is html/js/css, add jsx & tsx;
+import "prismjs/components/prism-jsx.js";
+import "prismjs/components/prism-tsx.js";
+import LocaleConsumer from "../../locale/localeConsumer";
+import { Locale } from "../../locale/interface";
+
+const { PREFIX_CHAT_BOX } = cssClasses;
+
+const Code = (props: PropsWithChildren<{ className: string }>) => {
+    const [copied, setCopied] = useState(false);
+    const language = useMemo(() => {
+        return nth(props.className?.split("-"), -1);
+    }, [props.className]);
+
+    const onCopyButtonClick = useCallback(() => {
+        copy(props.children as string);
+        setCopied(true);
+        setTimeout(() => {
+            setCopied(false);
+        }, 2000);
+    }, [props.children]);
+    
+    return language ? (<div className={`${PREFIX_CHAT_BOX}-content-code semi-always-dark`}>
+        <div className={`${PREFIX_CHAT_BOX}-content-code-topSlot`}>
+            <span className={`${PREFIX_CHAT_BOX}-content-code-topSlot-type`}>{language}</span>
+            <span className={`${PREFIX_CHAT_BOX}-content-code-topSlot-copy`}>
+                {copied ? (<span className={`${PREFIX_CHAT_BOX}-content-code-topSlot-copy-wrapper`}>
+                    <IconTick />
+                    <LocaleConsumer<Locale["Chat"]> componentName="Chat" >
+                        {(locale: Locale["Chat"]) => locale['copied']}
+                    </LocaleConsumer>
+                </span>) : (<button 
+                    className={`${PREFIX_CHAT_BOX}-content-code-topSlot-copy-wrapper ${PREFIX_CHAT_BOX}-content-code-topSlot-toCopy`} 
+                    onClick={onCopyButtonClick}
+                >
+                    <IconCopyStroked />
+                    <LocaleConsumer<Locale["Chat"]> componentName="Chat" >
+                        {(locale: Locale["Chat"]) => locale['copy']}
+                    </LocaleConsumer>
+                </button>)}
+            </span> 
+        </div>
+        {code(props)}
+    </div>) : (code(props));
+};
+
+export default Code;

+ 118 - 0
packages/semi-ui/chat/chatBox/index.tsx

@@ -0,0 +1,118 @@
+import React, { useMemo, useEffect, ReactElement } from 'react';
+import cls from 'classnames';
+import type { ChatBoxProps } from '../interface';
+import ChatBoxAvatar from './chatBoxAvatar';
+import ChatBoxTitle from './chatBoxTitle';
+import ChatBoxContent from './chatBoxContent';
+import ChatBoxAction from './chatBoxAction';
+import { cssClasses, strings } from '@douyinfe/semi-foundation/chat/constants';
+
+const { PREFIX_CHAT_BOX } = cssClasses;
+const { ROLE, CHAT_ALIGN } = strings;
+
+const ChatBox = React.memo((props: ChatBoxProps) => {
+    const { message, lastChat, align, toast, mode,
+        roleConfig, 
+        onMessageBadFeedback, 
+        onMessageGoodFeedback,
+        onMessageCopy, 
+        onChatsChange,
+        onMessageDelete,
+        onMessageReset,
+        chatBoxRenderConfig = {}, 
+        customMarkDownComponents,
+        previousMessage,
+    } = props;
+    const { renderChatBoxAvatar, renderChatBoxAction, 
+        renderChatBoxContent, renderChatBoxTitle,
+        renderFullChatBox
+    } = chatBoxRenderConfig;
+
+    const continueSend = useMemo(() => {
+        return message?.role === previousMessage?.role;
+    }, [message.role, previousMessage])
+
+    const info = useMemo(() => {
+        let info = {};
+        if (roleConfig) {
+            info = roleConfig[message.role] ?? {};
+        }
+        return info;
+    }, [message.role, roleConfig]);
+
+    const avatarNode = useMemo(() => {
+        return (<ChatBoxAvatar
+            continueSend={continueSend}
+            role={info} 
+            customRenderFunc={renderChatBoxAvatar}
+        />);
+    }, [info, renderChatBoxAvatar]);
+
+    const titleNode = useMemo(() => {
+        return (<ChatBoxTitle 
+            role={info} 
+            message={message}
+            customRenderFunc={renderChatBoxTitle}
+        />);
+    }, [info, message, renderChatBoxTitle]);
+
+    const contentNode = useMemo(() => {
+        return (<ChatBoxContent
+            mode={mode}
+            role={info}
+            message={message}
+            customMarkDownComponents={customMarkDownComponents}
+            customRenderFunc={renderChatBoxContent}
+        />);
+    }, [message, info, renderChatBoxContent]);
+
+    const actionNode = useMemo(() => {
+        return (<ChatBoxAction 
+            toast={toast}
+            role={info} 
+            message={message}
+            lastChat={lastChat}
+            onMessageBadFeedback={onMessageBadFeedback}
+            onMessageCopy={onMessageCopy}
+            onChatsChange={onChatsChange}
+            onMessageDelete={onMessageDelete}
+            onMessageGoodFeedback={onMessageGoodFeedback}
+            onMessageReset={onMessageReset}
+            customRenderFunc={renderChatBoxAction}
+        />);
+    }, [message, info, lastChat, onMessageBadFeedback, onMessageGoodFeedback, onMessageCopy, onChatsChange, onMessageDelete, onMessageReset, renderChatBoxAction]);
+
+    const containerCls = useMemo(() => cls(PREFIX_CHAT_BOX, {
+        [`${PREFIX_CHAT_BOX}-right`]: message.role === ROLE.USER && align === CHAT_ALIGN.LEFT_RIGHT,
+    }
+    ), [message.role, align]);
+
+    if (typeof renderFullChatBox !== 'function') {
+        return (<div
+            className={containerCls}
+        >
+            {avatarNode}
+            <div
+                className={`${PREFIX_CHAT_BOX}-wrap`}
+            >
+                {!continueSend && titleNode}
+                {contentNode}
+                {actionNode}
+            </div>
+        </div>);
+    } else {
+        return renderFullChatBox({
+            message,
+            role: info,
+            defaultNodes: {
+                avatar: avatarNode,
+                title: titleNode,
+                content: contentNode,
+                action: actionNode,
+            },
+            className: containerCls
+        }) as ReactElement;
+    }
+});
+
+export default ChatBox;

+ 58 - 0
packages/semi-ui/chat/chatContent.tsx

@@ -0,0 +1,58 @@
+import React from "react";
+import Divider from '../divider';
+import ChatBox from './chatBox';
+import type { CommonChatsProps } from "./interface";
+import { cssClasses, strings } from "@douyinfe/semi-foundation/chat/constants";
+import LocaleConsumer from "../locale/localeConsumer";
+import { Locale } from "../locale/interface";
+import { Toast } from '../index';
+
+const { PREFIX_DIVIDER, PREFIX } = cssClasses;
+const { ROLE } = strings;
+
+interface ChatContentProps extends CommonChatsProps {}
+
+const ChatContent = React.memo((props: ChatContentProps) => {
+    const { chats, onMessageBadFeedback, onMessageCopy, mode,
+        onChatsChange, onMessageDelete, onMessageGoodFeedback,
+        onMessageReset, roleConfig, chatBoxRenderConfig, align,
+        customMarkDownComponents,
+    } = props;
+
+    const [toast, contextHolder] = Toast.useToast();
+
+    return (
+        <>
+            {chats.map((item, index) => {
+                const lastMessage = index === chats.length - 1;
+                return item.role === ROLE.DIVIDER ? 
+                    <Divider key={item.id} className={PREFIX_DIVIDER}>
+                        <LocaleConsumer<Locale["Chat"]> componentName="Chat" >
+                            {(locale: Locale["Chat"]) => locale['clearContext']}
+                        </LocaleConsumer>
+                    </Divider> :
+                    <ChatBox
+                        previousMessage={index ? chats[index - 1] : undefined}
+                        toast={toast}
+                        align={align} 
+                        mode={mode}
+                        key={item.id} 
+                        message={item}
+                        roleConfig={roleConfig}
+                        onMessageBadFeedback={onMessageBadFeedback}
+                        onMessageCopy={onMessageCopy}
+                        onChatsChange={onChatsChange}
+                        onMessageDelete={onMessageDelete}
+                        onMessageGoodFeedback={onMessageGoodFeedback}
+                        onMessageReset={onMessageReset}
+                        lastChat={lastMessage}
+                        customMarkDownComponents={customMarkDownComponents}
+                        chatBoxRenderConfig={chatBoxRenderConfig}
+                    />;
+            })}
+            <div className={`${PREFIX}-toast`}>{contextHolder as any}</div>
+        </>
+    );  
+});
+
+export default ChatContent;

+ 53 - 0
packages/semi-ui/chat/hint.tsx

@@ -0,0 +1,53 @@
+import React from "react";
+import { IconArrowRight } from "@douyinfe/semi-icons";
+import cls from 'classnames';
+import { cssClasses } from '@douyinfe/semi-foundation/chat/constants';
+const { PREFIX_HINT } = cssClasses;
+
+interface HintProps {
+    className?: string;
+    style?: React.CSSProperties;
+    value?: string[];
+    onHintClick?: (item: string) => void;
+    renderHintBox?: (props: {content: string; index: number; onHintClick: () => void}) => React.ReactNode
+}
+
+const Hint = React.memo((props: HintProps) => {
+    const { value, onHintClick, renderHintBox, className, style } = props;
+    return (
+        <section 
+            className={cls(`${PREFIX_HINT}s`, {
+                [className]: !!className,
+            })}
+            style={style}
+        >
+            {value.map((item, index) => {
+                if (renderHintBox) {
+                    return renderHintBox({
+                        content: item, 
+                        index: index,
+                        onHintClick: () => {
+                            onHintClick?.(item);
+                        }
+                    });
+                }
+                return (
+                    <div 
+                        className={`${PREFIX_HINT}-item`}
+                        key={index} 
+                        onClick={() => {
+                            onHintClick?.(item);
+                        }}
+                    >
+                        <div className={`${PREFIX_HINT}-content`}>
+                            {item}
+                        </div>
+                        <IconArrowRight className={`${PREFIX_HINT}-icon`}/>
+                    </div>
+                );
+            })} 
+        </section> 
+    );
+});
+
+export default Hint;

+ 382 - 0
packages/semi-ui/chat/index.tsx

@@ -0,0 +1,382 @@
+import * as React from 'react';
+import BaseComponent from '../_base/baseComponent';
+import cls from "classnames";
+import PropTypes from 'prop-types';
+import type { ChatProps, ChatState, Message } from './interface';
+import InputBox from './inputBox';
+import "@douyinfe/semi-foundation/chat/chat.scss";
+import Hint from './hint';
+import { IconChevronDown, IconDisc } from '@douyinfe/semi-icons';
+import ChatContent from './chatContent';
+import { getDefaultPropsFromGlobalConfig } from '../_utils';
+import { cssClasses, strings } from '@douyinfe/semi-foundation/chat/constants';
+import ChatFoundation, { ChatAdapter } from '@douyinfe/semi-foundation/chat/foundation';
+import type { FileItem } from '../upload';
+import LocaleConsumer from "../locale/localeConsumer";
+import { Locale } from "../locale/interface";
+import { Button, Upload } from '../index';
+
+const prefixCls = cssClasses.PREFIX;
+const { CHAT_ALIGN, MODE, SEND_HOT_KEY } = strings;
+
+class Chat extends BaseComponent<ChatProps, ChatState> {
+
+    static __SemiComponentName__ = "Chat";
+  
+    containerRef: React.RefObject<HTMLDivElement>;
+    animation: any;
+    wheelEventHandler: any;
+    foundation: ChatFoundation;
+    uploadRef: React.RefObject<Upload>;
+    dropAreaRef: React.RefObject<HTMLDivElement>;
+
+    static propTypes = {
+        className: PropTypes.string,
+        style: PropTypes.object,
+        roleConfig: PropTypes.object,
+        chats: PropTypes.array,
+        hints: PropTypes.array,
+        renderHintBox: PropTypes.func,
+        onChatsChange: PropTypes.func,
+        align: PropTypes.string,
+        chatBoxRenderConfig: PropTypes.object,
+        customMarkDownComponents: PropTypes.object,
+        onClear: PropTypes.func,
+        onMessageDelete: PropTypes.func,
+        onMessageReset: PropTypes.func,
+        onMessageCopy: PropTypes.func,
+        onMessageGoodFeedback: PropTypes.func,
+        onMessageBadFeedback: PropTypes.func,
+        inputContentConvert: PropTypes.func,
+        onMessageSend: PropTypes.func,
+        InputBoxStyle: PropTypes.object,
+        inputBoxCls: PropTypes.string,
+        renderFullInputBox: PropTypes.func,
+        placeholder: PropTypes.string,
+        topSlot: PropTypes.node || PropTypes.array,
+        bottomSlot: PropTypes.node || PropTypes.array,
+        showStopGenerate: PropTypes.bool,
+        showClearContext: PropTypes.bool,
+        hintStyle: PropTypes.object,
+        hintCls: PropTypes.string,
+        uploadProps: PropTypes.object,
+        uploadTipProps: PropTypes.object,
+        mode: PropTypes.string,
+    };
+
+    static defaultProps = getDefaultPropsFromGlobalConfig(Chat.__SemiComponentName__, {
+        align: CHAT_ALIGN.LEFT_RIGHT,
+        showStopGenerate: false,
+        mode: MODE.BUBBLE,
+        showClearContext: false,
+        sendHotKey: SEND_HOT_KEY.ENTER,
+    })
+
+    constructor(props: ChatProps) {
+        super(props);
+
+        this.containerRef = React.createRef();
+        this.uploadRef = React.createRef();
+        this.dropAreaRef = React.createRef();
+        this.wheelEventHandler = null;
+        this.foundation = new ChatFoundation(this.adapter);
+
+        this.state = {
+            backBottomVisible: false,
+            chats: [],
+            cacheHints: [],
+            wheelScroll: false,
+            uploadAreaVisible: false,
+        };
+    }
+
+    get adapter(): ChatAdapter {
+        return {
+            ...super.adapter,
+            getContainerRef: () => this.containerRef,
+            setWheelScroll: (flag: boolean) => {
+                this.setState({
+                    wheelScroll: flag,
+                });
+            },
+            notifyChatsChange: (chats: Message[]) => {
+                const { onChatsChange } = this.props;
+                onChatsChange && onChatsChange(chats);
+            },
+            notifyLikeMessage: (message: Message) => {
+                const { onMessageGoodFeedback } = this.props;
+                onMessageGoodFeedback && onMessageGoodFeedback(message);
+            },
+            notifyDislikeMessage: (message: Message) => {
+                const { onMessageBadFeedback } = this.props;
+                onMessageBadFeedback && onMessageBadFeedback(message);
+            },
+            notifyCopyMessage: (message: Message) => {
+                const { onMessageCopy } = this.props;
+                onMessageCopy && onMessageCopy(message);
+            },
+            notifyClearContext: () => {
+                const { onClear } = this.props;
+                onClear && onClear();
+            },
+            notifyMessageSend: (content: string, attachment: any[]) => {
+                const { onMessageSend } = this.props;
+                onMessageSend && onMessageSend(content, attachment);
+            },
+            notifyInputChange: (props: { inputValue: string; attachment: any[]}) => {
+                const { onInputChange } = this.props;
+                onInputChange && onInputChange(props);
+            },
+            setBackBottomVisible: (visible: boolean) => {
+                this.setState((state) => {
+                    if (state.backBottomVisible !== visible) {
+                        return {
+                            backBottomVisible: visible,
+                        };
+                    }
+                    return null;
+                });
+            },
+            registerWheelEvent: () => {
+                this.adapter.unRegisterWheelEvent();
+                const containerElement = this.containerRef.current;
+                if (!containerElement) {
+                    return ;
+                }
+                this.wheelEventHandler = (e: any) => {
+                    if (e.target !== containerElement) {
+                        return;
+                    }
+                    this.adapter.setWheelScroll(true);
+                    this.adapter.unRegisterWheelEvent();
+                };
+        
+                containerElement.addEventListener('wheel', this.wheelEventHandler);
+            },
+            unRegisterWheelEvent: () => {
+                if (this.wheelEventHandler) {
+                    const containerElement = this.containerRef.current;
+                    if (!containerElement) {
+                        return ;
+                    } else {
+                        containerElement.removeEventListener('wheel', this.wheelEventHandler);
+                    }
+                    this.wheelEventHandler = null;
+                }
+            },
+            notifyStopGenerate: (e: MouseEvent) => {
+                const { onStopGenerator } = this.props;
+                onStopGenerator && onStopGenerator(e);
+            },
+            notifyHintClick: (hint: string) => {
+                const { onHintClick } = this.props;
+                onHintClick && onHintClick(hint);
+            },
+            setUploadAreaVisible: (visible: boolean) => {
+                this.setState({ uploadAreaVisible: visible });
+            },
+            manualUpload: (file: File[]) => {
+                const uploadComponent = this.uploadRef.current;
+                if (uploadComponent) {
+                    uploadComponent.insert(file);
+                }
+            },
+            getDropAreaElement: () => {
+                return this.dropAreaRef?.current;
+            } 
+        };
+    }
+
+    static getDerivedStateFromProps(nextProps: ChatProps, prevState: ChatState) {
+        const { chats, hints } = nextProps;
+        const newState = {} as any;
+        if (chats !== prevState.chats) {
+            newState.chats = chats;
+        }
+        if (hints !== prevState.cacheHints) {
+            newState.cacheHints = hints;
+        }
+        if (Object.keys(newState).length) {
+            return newState;
+        }
+        return null;
+    }
+
+    componentDidMount(): void {
+        this.foundation.init();    
+    }
+
+    componentDidUpdate(prevProps: Readonly<ChatProps>, prevState: Readonly<ChatState>, snapshot?: any): void {
+        const { chats: newChats, hints: newHints } = this.props;
+        const { chats: oldChats, cacheHints } = prevState;
+        const { wheelScroll } = this.state;
+        let shouldScroll = false;
+        if (newChats !== oldChats) {
+            const newLastChat = newChats[newChats.length - 1];
+            const oldLastChat = oldChats[oldChats.length - 1];
+            if (newChats.length > oldChats.length) {
+                if (newLastChat.id !== oldLastChat.id) {
+                    shouldScroll = true;
+                }
+            } else if (newChats.length === oldChats.length &&
+        (
+            newLastChat.status !== 'complete' ||
+          newLastChat.status !== oldLastChat.status
+        )
+            ) {
+                shouldScroll = true;
+            }
+        }
+        if (newHints !== cacheHints) {
+            if (newHints.length > cacheHints.length) {
+                shouldScroll = true;
+            }
+        }
+        if (!wheelScroll && shouldScroll) {
+            this.foundation.scrollToBottomImmediately();
+        }
+    }
+
+    componentWillUnmount(): void {
+        this.foundation.destroy();
+    }
+
+    resetMessage() {
+        this.foundation.resetMessage(null);
+    }
+
+    clearContext() {
+        this.foundation.clearContext(null);
+    }
+
+    scrollToBottom(animation: boolean) {
+        if (animation) {
+            this.foundation.scrollToBottomWithAnimation();
+        } else {
+            this.foundation.scrollToBottomImmediately();
+        }
+    }
+
+    sendMessage(content: string, attachment: FileItem[]) {
+        this.foundation.onMessageSend(content, attachment);
+    }
+
+    render() {
+        const { topSlot, bottomSlot, roleConfig, hints,
+            onChatsChange, onMessageCopy, renderInputArea,
+            chatBoxRenderConfig, align, renderHintBox,
+            style, className, showStopGenerate,
+            customMarkDownComponents, mode, showClearContext,
+            placeholder, inputBoxCls, inputBoxStyle,
+            hintStyle, hintCls, uploadProps, uploadTipProps,
+            sendHotKey,
+        } = this.props;
+        const { backBottomVisible, chats, wheelScroll, uploadAreaVisible } = this.state;
+        let showStopGenerateFlag = false;
+        const lastChat = chats.length > 0 && chats[chats.length - 1];
+        let disableSend = false;
+        if (lastChat && showStopGenerate) {
+            const lastChatOnGoing = lastChat.status && lastChat.status !== 'complete';
+            disableSend = lastChatOnGoing;
+            showStopGenerate && (showStopGenerateFlag = lastChatOnGoing);
+        }
+        return (
+            <div
+                className={cls(`${prefixCls}`, className)} 
+                style={style}
+                onDragOver={this.foundation.handleDragOver}
+            >
+                {uploadAreaVisible && <div
+                    ref={this.dropAreaRef} 
+                    className={`${prefixCls}-dropArea`}
+                    onDragOver={this.foundation.handleContainerDragOver}
+                    onDrop={this.foundation.handleContainerDrop}
+                    onDragLeave={this.foundation.handleContainerDragLeave}
+                >
+                    <span className={`${prefixCls}-dropArea-text`}>
+                        <LocaleConsumer<Locale["Chat"]> componentName="Chat" >
+                            {(locale: Locale["Chat"]) => locale['dropAreaText']}
+                        </LocaleConsumer>
+                    </span>  
+                </div>}
+                <div className={`${prefixCls}-inner`}>
+                    {/* top slot */}
+                    {topSlot}
+                    {/* chat area */}
+                    <div className={`${prefixCls}-content`}>
+                        <div 
+                            className={cls(`${prefixCls}-container`, {
+                                'semi-chat-container-scroll-hidden': !wheelScroll
+                            })}
+                            onScroll={this.foundation.containerScroll}
+                            ref={this.containerRef}
+                        >
+                            <ChatContent 
+                                align={align}
+                                mode={mode}
+                                chats={chats}  
+                                roleConfig={roleConfig}
+                                customMarkDownComponents={customMarkDownComponents}
+                                onMessageDelete={this.foundation.deleteMessage}
+                                onChatsChange={onChatsChange}
+                                onMessageBadFeedback={this.foundation.dislikeMessage}
+                                onMessageGoodFeedback={this.foundation.likeMessage}
+                                onMessageReset={this.foundation.resetMessage}
+                                onMessageCopy={onMessageCopy}
+                                chatBoxRenderConfig={chatBoxRenderConfig}
+                            />
+                            {/* hint area */}
+                            {!!hints?.length && <Hint 
+                                className={hintCls}
+                                style={hintStyle}
+                                value={hints} 
+                                onHintClick={this.foundation.onHintClick}
+                                renderHintBox={renderHintBox}
+                            />}
+                        </div>
+                    </div>
+                    {backBottomVisible && !showStopGenerateFlag && (<span className={`${prefixCls}-action`}>
+                        <Button
+                            className={`${prefixCls}-action-content ${prefixCls}-action-backBottom`} 
+                            icon={<IconChevronDown size="extra-large"/>}
+                            type="tertiary"
+                            onClick={this.foundation.scrollToBottomWithAnimation}
+                        />
+                    </span>)}
+                    {showStopGenerateFlag && (<span className={`${prefixCls}-action`}>
+                        <Button
+                            className={`${prefixCls}-action-content ${prefixCls}-action-stop`} 
+                            icon={<IconDisc size="extra-large" />}
+                            type="tertiary"
+                            onClick={this.foundation.stopGenerate} 
+                        >
+                            <LocaleConsumer<Locale["Chat"]> componentName="Chat" >
+                                {(locale: Locale["Chat"]) => locale['stop']}
+                            </LocaleConsumer>
+                        </Button>
+                    </span>)}
+                    {/* input area */}
+                    <InputBox
+                        showClearContext={showClearContext}
+                        uploadRef={this.uploadRef}
+                        manualUpload={this.adapter.manualUpload}
+                        style={inputBoxStyle}
+                        className={inputBoxCls}
+                        placeholder={placeholder}
+                        disableSend={disableSend}
+                        onClearContext={this.foundation.clearContext}
+                        onSend={this.foundation.onMessageSend}
+                        onInputChange={this.foundation.onInputChange}
+                        renderInputArea={renderInputArea}
+                        uploadProps={uploadProps}
+                        uploadTipProps={uploadTipProps}
+                        sendHotKey={sendHotKey}
+                    />
+                    {bottomSlot}
+                </div>
+            </div>
+        );
+    }
+}
+
+export default Chat;

+ 170 - 0
packages/semi-ui/chat/inputBox/index.tsx

@@ -0,0 +1,170 @@
+import React, { PureComponent } from 'react';
+import cls from 'classnames';
+import PropTypes from 'prop-types';
+import { FileItem } from '../../upload/interface';
+import type { InputBoxProps, InputBoxState } from '../interface';
+import { BaseComponent, Button, Upload, Tooltip, TextArea } from '../../index';
+import { IconDeleteStroked, IconChainStroked, IconArrowUp } from '@douyinfe/semi-icons';
+import { cssClasses, strings } from "@douyinfe/semi-foundation/chat/constants";
+import InputBoxFoundation, { InputBoxAdapter } from '@douyinfe/semi-foundation/chat/inputboxFoundation';
+import Attachment from '../attachment';
+
+const { PREFIX_INPUT_BOX } = cssClasses;
+const { SEND_HOT_KEY } = strings;
+const textAutoSize = { minRows: 1, maxRows: 5 };
+
+class InputBox extends BaseComponent<InputBoxProps, InputBoxState> {
+
+    inputAreaRef: React.RefObject<any>;
+    static propTypes = {
+        uploadProps: PropTypes.object,
+    };
+
+    static defaultProps = {
+        uploadProps: {}
+    };
+
+    constructor(props: InputBoxProps) {
+        super(props);
+        this.inputAreaRef = React.createRef();
+        this.foundation = new InputBoxFoundation(this.adapter);
+
+        this.state = {
+            content: '',
+            attachment: []
+        };
+    }
+
+    get adapter(): InputBoxAdapter<InputBoxProps, InputBoxState> {
+        return {
+            ...super.adapter,
+            notifyInputChange: (props: { inputValue: string; attachment: any[]}) => {
+                const { onInputChange } = this.props;
+                onInputChange && onInputChange(props);
+            },
+            setInputValue: (value) => {
+                this.setState({
+                    content: value
+                });
+            },
+            setAttachment: (attachment: any[]) => {
+                this.setState({
+                    attachment: attachment
+                });
+            },
+            notifySend: (content: string, attachment: FileItem[]) => {
+                const { onSend } = this.props;
+                onSend && onSend(content, attachment);
+            }
+        };
+    }
+
+    onClick = () => {
+        this.inputAreaRef.current?.focus();
+    }
+
+    renderUploadButton = () => {
+        const { uploadProps, uploadRef, uploadTipProps } = this.props;
+        const { attachment } = this.state;
+        const { className, onChange, renderFileItem, children, ...rest } = uploadProps;
+        const realUploadProps = {
+            ...rest,
+            className: cls(`${PREFIX_INPUT_BOX}-upload`, {
+                [className]: className
+            }),
+            onChange: this.foundation.onAttachmentAdd,
+        };
+        const uploadNode = <Upload
+            ref={uploadRef}
+            fileList={attachment}
+            {...realUploadProps}
+        >
+            {children ? children : <Button 
+                className={`${PREFIX_INPUT_BOX}-uploadButton`}
+                icon={<IconChainStroked size="extra-large" />}
+                theme='borderless'
+            />}
+        </Upload>;
+        return (uploadTipProps ? <Tooltip {...uploadTipProps}><span>{uploadNode}</span></Tooltip> : uploadNode);
+    }
+
+    renderInputArea = () => {
+        const { content, attachment } = this.state;
+        const { placeholder, sendHotKey } = this.props;
+        return (<div
+            className={`${PREFIX_INPUT_BOX}-inputArea`}
+        >
+            <TextArea
+                placeholder={placeholder}
+                onEnterPress={this.foundation.onEnterPress}
+                value={content}
+                onChange={this.foundation.onInputAreaChange}
+                ref={this.inputAreaRef}
+                className={`${PREFIX_INPUT_BOX}-textarea`}
+                autosize={textAutoSize} 
+                disabledEnterStartNewLine={sendHotKey === SEND_HOT_KEY.ENTER ? true : false}
+                onPaste={this.foundation.onPaste as any}
+            />
+            <Attachment 
+                attachment={attachment as any} 
+                onClear={this.foundation.onAttachmentDelete}
+            />
+        </div>);
+    }
+
+    renderClearButton = () => {
+        const { onClearContext } = this.props;
+        return (
+            <Button
+                className={`${PREFIX_INPUT_BOX}-clearButton`}
+                theme='borderless'
+                icon={<IconDeleteStroked />}
+                onClick={onClearContext}
+            />
+        );
+    }
+
+    renderSendButton = () => {
+        const disabledSend = this.foundation.getDisableSend();
+        return (
+            <Button
+                disabled={disabledSend}
+                theme='solid'
+                type='primary'
+                className={`${PREFIX_INPUT_BOX}-sendButton`}
+                // icon={<IconSend size="extra-large" className={`${PREFIX_INPUT_BOX}-sendButton-icon`} />}
+                icon={<IconArrowUp size="large" className={`${PREFIX_INPUT_BOX}-sendButton-icon`} />}
+                onClick={this.foundation.onSend}
+            />
+        );
+    }
+
+    render() {
+        const { onClearContext, renderInputArea, onSend, style, className, showClearContext } = this.props;
+        const nodes = (
+            <div className={cls(PREFIX_INPUT_BOX, { [className]: className })} style={style}>
+                <div 
+                    className={`${PREFIX_INPUT_BOX}-inner`}
+                    onClick={this.onClick}
+                >
+                    {showClearContext && this.renderClearButton()}
+                    <div className={`${PREFIX_INPUT_BOX}-container`}>
+                        {this.renderUploadButton()}
+                        {this.renderInputArea()}
+                        {this.renderSendButton()}
+                    </div>
+                </div>
+            </div>
+        );
+        if (renderInputArea) {
+            return renderInputArea({
+                defaultNode: nodes, 
+                onClear: onClearContext,
+                onSend: onSend,
+            });
+        }
+        return nodes; 
+    }
+}
+
+export default InputBox;

+ 126 - 0
packages/semi-ui/chat/interface.ts

@@ -0,0 +1,126 @@
+import React, { ReactNode } from 'react';
+import { MDXProps } from 'mdx/types';
+import { Upload } from '../index';
+import type { FileItem, UploadProps } from '../upload';
+import { Message } from '@douyinfe/semi-foundation/chat/foundation';
+import type { TooltipProps } from '../tooltip';
+
+export { Message };
+export interface CommonChatsProps {
+    align?: 'leftRight' | 'leftAlign';
+    mode?: 'bubble' | 'noBubble' | 'userBubble';
+    chats?: Message[];
+    roleConfig?: RoleConfig;
+    onMessageDelete?: (message?: Message) => void;
+    onChatsChange?: (chats?: Message[]) => void;
+    onMessageBadFeedback?: (message?: Message) => void;
+    onMessageGoodFeedback?: (message?: Message) => void;
+    onMessageReset?: (message?: Message) => void;
+    onMessageCopy?: (message?: Message) => void;
+    chatBoxRenderConfig?: ChatBoxRenderConfig;
+    customMarkDownComponents?: MDXProps['components']
+}
+
+export interface ChatProps extends CommonChatsProps {
+    style?: React.CSSProperties;
+    className?: string;
+    hints?: string[];
+    renderHintBox?: (props: {content: string; index: number;onHintClick: () => void}) => React.ReactNode;
+    onHintClick?: (hint: string) => void;
+    onChatsChange?: (chats?: Message[]) => void;
+    onStopGenerator?: (e?: MouseEvent) => void;
+    customMarkDownComponents?: MDXProps['components'];
+    onClear?: () => void;
+    onInputChange?: (props: { value?: string; attachment?: FileItem[] }) => void;
+    onMessageSend?: (content: string, attachment: FileItem[]) => void;
+    inputBoxStyle?: React.CSSProperties;
+    inputBoxCls?: string;
+    renderInputArea?: (props?: RenderInputAreaProps) => ReactNode;
+    placeholder?: string;
+    topSlot?: ReactNode | ReactNode[];
+    bottomSlot?: ReactNode | ReactNode[];
+    showStopGenerate?: boolean;
+    hintStyle?: React.CSSProperties;
+    hintCls?: string;
+    uploadProps?: UploadProps;
+    uploadTipProps?: TooltipProps;
+    showClearContext?: boolean;
+    sendHotKey?: 'enter' | 'shift+enter'
+}
+
+export interface RenderInputAreaProps {
+    defaultNode?: ReactNode;
+    onSend?: (content?: string, attachment?: FileItem[]) => void;
+    onClear?: (e?: any) => void
+}
+
+export 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
+}
+
+export interface FullChatBoxNodes {
+    avatar?: ReactNode;
+    title?: ReactNode; 
+    content?: ReactNode; 
+    action?: ReactNode
+}
+
+export interface RoleConfig {
+    user?: Metadata;
+    assistant?: Metadata;
+    system?: Metadata;
+    [x: string]: Metadata
+}
+
+export interface Metadata {
+    name?: string;
+    avatar?: string;
+    color?: string;
+    [x: string]: any
+}
+
+export interface ChatState {
+    chats?: Message[];
+    isLoading?: boolean;
+    backBottomVisible?: boolean;
+    scrollVisible?: boolean;
+    wheelScroll?: boolean;
+    cacheHints?: string[];
+    uploadAreaVisible?: boolean
+}
+
+export interface ChatBoxProps extends Omit<CommonChatsProps, "chats"> {
+    toast?: any;
+    style?: React.CSSProperties;
+    className?: string;
+    previousMessage?: Message;
+    message?: Message;
+    lastChat?: boolean;
+    customMarkDownComponents?: MDXProps['components']
+}
+
+export interface InputBoxProps {
+    showClearContext?: boolean;
+    sendHotKey?: 'enter' | 'shift+enter';
+    placeholder: string;
+    className?: string;
+    style?: React.CSSProperties;
+    disableSend?: boolean;
+    uploadRef?: React.RefObject<Upload>;
+    uploadTipProps?: TooltipProps;
+    uploadProps?: UploadProps;
+    manualUpload?: (file: File[]) => void;
+    renderInputArea?: (props: RenderInputAreaProps) => React.ReactNode;
+    onSend?: (content: string, attachment: FileItem[]) => void;
+    onClearContext?: (e: any) => void;
+    onInputChange?: (props: {inputValue: string; attachment: FileItem[]}) => void
+}
+
+export interface InputBoxState {
+    content: string;
+    attachment: FileItem[]
+}

+ 3 - 1
packages/semi-ui/index.ts

@@ -108,4 +108,6 @@ export { default as PinCode } from "./pincode";
 
 
 export { default as MarkdownRender } from "./markdownRender";
 export { default as MarkdownRender } from "./markdownRender";
 export { default as CodeHighlight } from "./codeHighlight";
 export { default as CodeHighlight } from "./codeHighlight";
-export { default as Lottie } from "./lottie";
+export { default as Lottie } from "./lottie";
+
+export { default as Chat } from './chat';

+ 8 - 2
packages/semi-ui/input/textarea.tsx

@@ -56,7 +56,12 @@ export interface TextAreaProps extends Omit<React.TextareaHTMLAttributes<HTMLTex
     onPressEnter?: (e: React.KeyboardEvent<HTMLTextAreaElement>) => void;
     onPressEnter?: (e: React.KeyboardEvent<HTMLTextAreaElement>) => void;
     onResize?: (data: { height: number }) => void;
     onResize?: (data: { height: number }) => void;
     getValueLength?: (value: string) => number;
     getValueLength?: (value: string) => number;
-    forwardRef?: ((instance: HTMLTextAreaElement) => void) | React.MutableRefObject<HTMLTextAreaElement> | null
+    forwardRef?: ((instance: HTMLTextAreaElement) => void) | React.MutableRefObject<HTMLTextAreaElement> | null;
+    /* Inner params for TextArea, Chat use it, 。
+       Used to disable line breaks by pressing the enter key。
+       Press enter + shift at the same time can start new line.
+    */
+    disabledEnterStartNewLine?: boolean
 }
 }
 
 
 export interface TextAreaState {
 export interface TextAreaState {
@@ -85,6 +90,7 @@ class TextArea extends BaseComponent<TextAreaProps, TextAreaState> {
         onClear: PropTypes.func,
         onClear: PropTypes.func,
         onResize: PropTypes.func,
         onResize: PropTypes.func,
         getValueLength: PropTypes.func,
         getValueLength: PropTypes.func,
+        disabledEnterStartNewLine: PropTypes.bool,
         // TODO
         // TODO
         // resize: PropTypes.bool,
         // resize: PropTypes.bool,
     };
     };
@@ -284,7 +290,7 @@ class TextArea extends BaseComponent<TextAreaProps, TextAreaState> {
             [`${prefixCls}-textarea-showClear`]: showClear,
             [`${prefixCls}-textarea-showClear`]: showClear,
         });
         });
         const itemProps = {
         const itemProps = {
-            ...omit(rest, 'insetLabel', 'insetLabelId', 'getValueLength', 'onClear', 'showClear'),
+            ...omit(rest, 'insetLabel', 'insetLabelId', 'getValueLength', 'onClear', 'showClear', 'disabledEnterStartNewLine'),
             autoFocus: autoFocus || this.props['autofocus'],
             autoFocus: autoFocus || this.props['autofocus'],
             className: itemCls,
             className: itemCls,
             disabled,
             disabled,

+ 9 - 0
packages/semi-ui/locale/interface.ts

@@ -168,5 +168,14 @@ export interface Locale {
         downloadTip: string;
         downloadTip: string;
         adaptiveTip: string;
         adaptiveTip: string;
         originTip: string
         originTip: string
+    };
+    Chat: {
+        deleteConfirm: string;
+        clearContext: string;
+        copySuccess: string;
+        stop: string;
+        copy: string;
+        copied: string;
+        dropAreaText: string
     }
     }
 }
 }

+ 9 - 0
packages/semi-ui/locale/source/ar.ts

@@ -170,6 +170,15 @@ const local: Locale = {
         adaptiveTip: "التكيف مع الصفحة",
         adaptiveTip: "التكيف مع الصفحة",
         originTip: "الحجم الأصلي",
         originTip: "الحجم الأصلي",
     },
     },
+    Chat: {
+        deleteConfirm: 'هل ترغب في حذف هذه الجلسة؟',
+        clearContext: 'تم مسح السياق',
+        copySuccess: 'تم النسخ بنجاح',
+        stop: 'توقف',
+        copy: 'نسخ',
+        copied: 'نسخ',
+        dropAreaText: 'ضع الملف هنا',
+    },
 };
 };
 
 
 // [i18n-Arabic]
 // [i18n-Arabic]

+ 9 - 0
packages/semi-ui/locale/source/de.ts

@@ -170,6 +170,15 @@ const local: Locale = {
         adaptiveTip: 'An die Seite anpassen',
         adaptiveTip: 'An die Seite anpassen',
         originTip: 'Originalgröße',
         originTip: 'Originalgröße',
     },
     },
+    Chat: {
+        deleteConfirm: 'Möchten Sie diesen Chat wirklich löschen?',
+        clearContext: 'Der Kontext wurde gelöscht',
+        copySuccess: 'Erfolgreich kopiert',
+        stop: 'stoppen',
+        copy: 'Kopieren',
+        copied: 'Kopiert',
+        dropAreaText: 'Datei hier ablegen',
+    },
 };
 };
 
 
 // [i18n-German]
 // [i18n-German]

+ 9 - 0
packages/semi-ui/locale/source/en_GB.ts

@@ -170,6 +170,15 @@ const local: Locale = {
         adaptiveTip: 'Adapt to the page',
         adaptiveTip: 'Adapt to the page',
         originTip: 'Original size',
         originTip: 'Original size',
     },
     },
+    Chat: {
+        deleteConfirm: 'Are you sure you want to delete this session?',
+        clearContext: 'Context cleared',
+        copySuccess: 'Copy successful.',
+        stop: 'Stop',
+        copy: 'Copy',
+        copied: 'Copied',
+        dropAreaText: 'Put the file here',
+    }
 };
 };
 
 
 // [i18n-English(GB)]
 // [i18n-English(GB)]

+ 9 - 0
packages/semi-ui/locale/source/en_US.ts

@@ -170,6 +170,15 @@ const local: Locale = {
         adaptiveTip: 'Adapt to the page',
         adaptiveTip: 'Adapt to the page',
         originTip: 'Original size',
         originTip: 'Original size',
     },
     },
+    Chat: {
+        deleteConfirm: 'Are you sure you want to delete this session?',
+        clearContext: 'Context cleared',
+        copySuccess: 'Copy successful.',
+        stop: 'Stop',
+        copy: 'Copy',
+        copied: 'Copied',
+        dropAreaText: 'Put the file here',
+    }
 };
 };
 
 
 // [i18n-English(US)]
 // [i18n-English(US)]

+ 9 - 0
packages/semi-ui/locale/source/es.ts

@@ -175,6 +175,15 @@ const locale: Locale = {
         adaptiveTip: 'Adaptarse a la página',
         adaptiveTip: 'Adaptarse a la página',
         originTip: 'Tamaño original',
         originTip: 'Tamaño original',
     },
     },
+    Chat: {
+        deleteConfirm: '¿Estás seguro de que quieres eliminar esta conversación?',
+        clearContext: 'El contexto ha sido eliminado',
+        copySuccess: 'Copiado exitosamente',
+        stop: 'Detener',
+        copy: 'Copiar',
+        copied: 'Copiado',
+        dropAreaText: 'Coloca el archivo aquí',
+    },
 };
 };
 
 
 export default locale;
 export default locale;

+ 9 - 0
packages/semi-ui/locale/source/fr.ts

@@ -170,6 +170,15 @@ const local: Locale = {
         adaptiveTip: 'Adapter à la page',
         adaptiveTip: 'Adapter à la page',
         originTip: 'Taille d\'origine',
         originTip: 'Taille d\'origine',
     },
     },
+    Chat: {
+        deleteConfirm: 'Êtes-vous sûr de vouloir supprimer cette conversation ?',
+        clearContext: 'Le contexte a été effacé',
+        copySuccess: 'Copie réussie',
+        stop: 'Arrêter',
+        copy: 'Copier',
+        copied: 'Copié',
+        dropAreaText: 'Déposez le fichier ici',
+    },
 };
 };
 
 
 // [i18n-French]
 // [i18n-French]

+ 9 - 0
packages/semi-ui/locale/source/id_ID.ts

@@ -170,6 +170,15 @@ const local: Locale = {
         adaptiveTip: 'Beradaptasi dengan halaman',
         adaptiveTip: 'Beradaptasi dengan halaman',
         originTip: 'Ukuran asli',
         originTip: 'Ukuran asli',
     },
     },
+    Chat: {
+        deleteConfirm: 'Apakah Anda yakin ingin menghapus sesi ini??',
+        clearContext: 'Konteks telah dihapus',
+        copySuccess: 'Berhasil disalin',
+        stop: 'Berhenti',
+        copy: 'Salin',
+        copied: 'Disalin',
+        dropAreaText: 'Letakkan file di sini',
+    },
 };
 };
 
 
 // [i18n-Indonesia(ID)]
 // [i18n-Indonesia(ID)]

+ 9 - 0
packages/semi-ui/locale/source/it.ts

@@ -170,6 +170,15 @@ const local: Locale = {
         adaptiveTip: 'Adatta alla pagina',
         adaptiveTip: 'Adatta alla pagina',
         originTip: 'Formato originale',
         originTip: 'Formato originale',
     },
     },
+    Chat: {
+        deleteConfirm: 'Sei sicuro di voler eliminare questa conversazione?',
+        clearContext: 'Il contesto è stato eliminato',
+        copySuccess: 'Copiato con successo',
+        stop: 'Fermare',
+        copy: 'Copia',
+        copied: 'Copiato',
+        dropAreaText: 'Metti il file qui',
+    },
 };
 };
 
 
 // [i18n-Italian]
 // [i18n-Italian]

+ 9 - 0
packages/semi-ui/locale/source/ja_JP.ts

@@ -171,6 +171,15 @@ const local: Locale = {
         adaptiveTip: 'ページに適応',
         adaptiveTip: 'ページに適応',
         originTip: '元のサイズ',
         originTip: '元のサイズ',
     },
     },
+    Chat: {
+        deleteConfirm: 'このセッションを削除してもよろしいですか?',
+        clearContext: 'コンテキストを削除しました',
+        copySuccess: '正常にコピーされました',
+        stop: 'とめる',
+        copy: 'コピー',
+        copied: 'コピーしました',
+        dropAreaText: 'ファイルをここに置いてください',
+    },
 };
 };
 
 
 // [i18n-Japan]
 // [i18n-Japan]

+ 9 - 0
packages/semi-ui/locale/source/ko_KR.ts

@@ -171,6 +171,15 @@ const local: Locale = {
         adaptiveTip: '페이지에 맞게 조정',
         adaptiveTip: '페이지에 맞게 조정',
         originTip: '원래 크기',
         originTip: '원래 크기',
     },
     },
+    Chat: {
+        deleteConfirm: '이 대화를 삭제하시겠습니까?',
+        clearContext: '컨텍스트가 지워졌습니다',
+        copySuccess: '복사 성공',
+        stop: '중지',
+        copy: '복사',
+        copied: '복사했습니다',
+        dropAreaText: '파일을 여기에 놓으세요',
+    },
 };
 };
 
 
 // [i18n-Korea]
 // [i18n-Korea]

+ 9 - 0
packages/semi-ui/locale/source/ms_MY.ts

@@ -170,6 +170,15 @@ const local: Locale = {
         adaptiveTip: 'Menyesuaikan diri dengan halaman',
         adaptiveTip: 'Menyesuaikan diri dengan halaman',
         originTip: 'Saiz asal',
         originTip: 'Saiz asal',
     },
     },
+    Chat: {
+        deleteConfirm: 'Adakah anda pasti ingin memadam sesi ini?',
+        clearContext: 'Konteks telah dibersihkan',
+        copySuccess: 'Berjaya disalin',
+        stop: 'Berhenti',
+        copy: 'Samin',
+        copied: 'Disalin',
+        dropAreaText: 'Letakkan fail di sini',
+    },
 };
 };
 
 
 // [i18n-Malaysia(MY)]
 // [i18n-Malaysia(MY)]

+ 9 - 0
packages/semi-ui/locale/source/nl_NL.ts

@@ -177,6 +177,15 @@ const local: Locale = {
         adaptiveTip: 'Adaptieve weergave',
         adaptiveTip: 'Adaptieve weergave',
         originTip: 'Standaardweergave',
         originTip: 'Standaardweergave',
     },
     },
+    Chat: {
+        deleteConfirm: 'Weet u zeker dat u deze conversatie wilt verwijderen?',
+        clearContext: 'De context is gewist',
+        copySuccess: 'Succesvol gekopieerd',
+        stop: 'Stoppen',
+        copy: 'Kopiëren',
+        copied: 'Gekopieerd',
+        dropAreaText: 'Plaats het bestand hier',
+    },
 };
 };
 
 
 export default local;
 export default local;

+ 9 - 0
packages/semi-ui/locale/source/pl_PL.ts

@@ -178,6 +178,15 @@ const local: Locale = {
         adaptiveTip: 'Dostosowywanie ekranu',
         adaptiveTip: 'Dostosowywanie ekranu',
         originTip: 'Wyświetlacz domyślny',
         originTip: 'Wyświetlacz domyślny',
     },
     },
+    Chat: {
+        deleteConfirm: 'Czy na pewno chcesz usunąć tę rozmowę?',
+        clearContext: 'Kontekst został wyczyszczony',
+        copySuccess: 'Skopiowano pomyślnie',
+        stop: 'Zatrzymać',
+        copy: 'Kopiuj',
+        copied: 'Skopiowano',
+        dropAreaText: 'Umieść plik tutaj',
+    },
 };
 };
 
 
 export default local;
 export default local;

+ 9 - 0
packages/semi-ui/locale/source/pt_BR.ts

@@ -178,6 +178,15 @@ const local: Locale = {
         adaptiveTip: 'Adaptar à página',
         adaptiveTip: 'Adaptar à página',
         originTip: 'Tamanho original',
         originTip: 'Tamanho original',
     },
     },
+    Chat: {
+        deleteConfirm: 'Você tem certeza de que deseja excluir esta sessão?',
+        clearContext: 'Contexto limpo',
+        copySuccess: 'Copiado com sucesso',
+        stop: 'Parar',
+        copy: 'Copiar',
+        copied: 'Cópia bem sucedida',
+        dropAreaText: 'Coloque o arquivo aqui',
+    },
 };
 };
 
 
 // 葡萄牙语
 // 葡萄牙语

+ 9 - 0
packages/semi-ui/locale/source/ro.ts

@@ -170,6 +170,15 @@ const local: Locale = {
         adaptiveTip: 'Afișaj adaptabil',
         adaptiveTip: 'Afișaj adaptabil',
         originTip: 'Afișaj implicit',
         originTip: 'Afișaj implicit',
     },
     },
+    Chat: {
+        deleteConfirm: 'Sunteți sigur că doriți să ștergeți această conversație?',
+        clearContext: 'Contextul a fost șters',
+        copySuccess: 'Copiere reușită',
+        stop: 'Oprire',
+        copy: 'Copiază',
+        copied: 'Copiat',
+        dropAreaText: 'Puneți fișierul aici',
+    },
 };
 };
 
 
 // [i18n-Romanian] 罗马尼亚语
 // [i18n-Romanian] 罗马尼亚语

+ 9 - 0
packages/semi-ui/locale/source/ru_RU.ts

@@ -173,6 +173,15 @@ const local: Locale = {
         adaptiveTip: 'Адаптировать к странице',
         adaptiveTip: 'Адаптировать к странице',
         originTip: 'Исходный размер',
         originTip: 'Исходный размер',
     },
     },
+    Chat: {
+        deleteConfirm: 'Вы уверены, что хотите удалить эту сессию?',
+        clearContext: 'Контекст очищен',
+        copySuccess: 'Скопировано успешно',
+        stop: 'остановить',
+        copy: 'Копировать',
+        copied: 'Скопировано',
+        dropAreaText: 'Положите файл здесь',
+    },
 };
 };
 
 
 // [i18n-Russia] 俄罗斯语
 // [i18n-Russia] 俄罗斯语

+ 9 - 0
packages/semi-ui/locale/source/sv_SE.ts

@@ -175,6 +175,15 @@ const local: Locale = {
         adaptiveTip: 'Adaptiv visning',
         adaptiveTip: 'Adaptiv visning',
         originTip: 'Standardvisning',
         originTip: 'Standardvisning',
     },
     },
+    Chat: {
+        deleteConfirm: 'Är du säker på att du vill ta bort denna konversation?',
+        clearContext: 'Kontexten har rensats',
+        copySuccess: 'Kopiering lyckades',
+        stop: 'Stoppa',
+        copy: 'Kopiera',
+        copied: 'Kopierad',
+        dropAreaText: 'Placera filen här',   
+    }, 
 };
 };
 
 
 export default local;
 export default local;

+ 9 - 0
packages/semi-ui/locale/source/th_TH.ts

@@ -174,6 +174,15 @@ const local: Locale = {
         adaptiveTip: 'ปรับให้เข้ากับหน้า',
         adaptiveTip: 'ปรับให้เข้ากับหน้า',
         originTip: 'ขนาดเดิม',
         originTip: 'ขนาดเดิม',
     },
     },
+    Chat: {
+        deleteConfirm: 'คุณต้องการลบการสนทนานี้ใช่หรือไม่?',
+        clearContext: 'ล้างความเข้าใจเรียบร้อยแล้ว',
+        copySuccess: 'คัดลอกสำเร็จ',
+        stop: 'หยุด',
+        copy: 'สำเนา"',
+        copied: 'คัดลอกสำเร็จ',
+        dropAreaText: 'วางไฟล์ที่นี่',
+    },
 };
 };
 
 
 // [i18n-Thai]
 // [i18n-Thai]

+ 9 - 0
packages/semi-ui/locale/source/tr_TR.ts

@@ -170,6 +170,15 @@ const local: Locale = {
         adaptiveTip: 'Sayfaya uyarla',
         adaptiveTip: 'Sayfaya uyarla',
         originTip: 'Orijinal boyut',
         originTip: 'Orijinal boyut',
     },
     },
+    Chat: {
+        deleteConfirm: 'Bu sohbeti silmek istediğinize emin misiniz?',
+        clearContext: 'Bağlam temizlendi',
+        copySuccess: 'Başarıyla kopyalandı',
+        stop: 'Durmak',
+        copy: 'Kopyala',
+        copied: 'Kopyalama başarılı',
+        dropAreaText: 'Dosyayı buraya yerleştirin',
+    },
 };
 };
 
 
 // [i18n-Turkish] 
 // [i18n-Turkish] 

+ 9 - 0
packages/semi-ui/locale/source/vi_VN.ts

@@ -173,6 +173,15 @@ const local: Locale = {
         adaptiveTip: 'Thích ứng với trang',
         adaptiveTip: 'Thích ứng với trang',
         originTip: 'Kích thước ban đầu',
         originTip: 'Kích thước ban đầu',
     },
     },
+    Chat: {
+        deleteConfirm: 'Bạn có chắc muốn xóa phiên này không?',
+        clearContext: 'Ngữ cảnh đã được xóa',
+        copySuccess: 'Sao chép thành công',
+        stop: 'Dừng',
+        copy: 'Sao chép',
+        copied: 'Đã sao chép',
+        dropAreaText: 'Đặt tệp vào đây',
+    }, 
 };
 };
 
 
 // [i18n-Vietnam] 越南语
 // [i18n-Vietnam] 越南语

+ 9 - 0
packages/semi-ui/locale/source/zh_CN.ts

@@ -171,6 +171,15 @@ const local: Locale = {
         adaptiveTip: '适应页面',
         adaptiveTip: '适应页面',
         originTip: '原始尺寸',
         originTip: '原始尺寸',
     },
     },
+    Chat: {
+        deleteConfirm: '确认删除该会话吗?',
+        clearContext: '上下文已清除',
+        copySuccess: '复制成功',
+        stop: '停止',
+        copy: '复制',
+        copied: '复制成功',
+        dropAreaText: '将文件放到这里',
+    },
 };
 };
 
 
 // 中文
 // 中文

+ 9 - 0
packages/semi-ui/locale/source/zh_TW.ts

@@ -171,6 +171,15 @@ const local: Locale = {
         adaptiveTip: '適應頁面',
         adaptiveTip: '適應頁面',
         originTip: '原始尺寸',
         originTip: '原始尺寸',
     },
     },
+    Chat: {
+        deleteConfirm: '確認刪除該對話嗎?',
+        clearContext: '上下文已清除',
+        copySuccess: '複製成功',
+        stop: '停止',
+        copy: '複制',
+        copied: '複制成功',
+        dropAreaText: '將文件放到這裡',
+    },
 };
 };
 
 
 // 中文
 // 中文

+ 0 - 1
packages/semi-ui/markdownRender/_story/markdownRender.stories.jsx

@@ -5,7 +5,6 @@ export default {
 }
 }
 
 
 
 
-
 export const Basic = ()=>{
 export const Basic = ()=>{
     return <MarkdownRender raw={"# Two 🍰 is: {Math.PI * 2}"} components={semiComponents}/>
     return <MarkdownRender raw={"# Two 🍰 is: {Math.PI * 2}"} components={semiComponents}/>
 }
 }

+ 1 - 1
packages/semi-ui/upload/index.tsx

@@ -384,7 +384,7 @@ class Upload extends BaseComponent<UploadProps, UploadState> {
      * @param index number
      * @param index number
      * @returns
      * @returns
      */
      */
-    insert = (files: Array<CustomFile>, index: number): void => {
+    insert = (files: Array<CustomFile>, index?: number): void => {
         return this.foundation.insertFileToList(files, index);
         return this.foundation.insertFileToList(files, index);
     };
     };
 
 

+ 26 - 0
src/styles/docDemo.scss

@@ -474,6 +474,32 @@ body > .component-list-demo-drag-item {
     will-change: unset!important;
     will-change: unset!important;
 }
 }
 
 
+.component-chat-demo-custom-render {
+    .time {
+        font-size: 12px;
+        color: var(--semi-color-text-2);
+        visibility: hidden;
+    }
+
+    .title {
+        display: flex;
+        align-items: 'center';
+        column-gap: 10px;
+    }
+
+    .semi-chat-chatBox:hover {
+        .time {
+            visibility: visible;
+        }
+    }
+
+    .semi-chat-chatBox-right {
+        .title {
+            flex-direction: row-reverse;
+        }
+    }
+}
+
 
 
 
 
 
 

+ 9 - 9
yarn.lock

@@ -1683,20 +1683,20 @@
     lodash-es "^4.17.21"
     lodash-es "^4.17.21"
     react-i18next "^11.12.0"
     react-i18next "^11.12.0"
 
 
-"@douyinfe/semi-site-markdown-blocks@^0.0.17":
-  version "0.0.17"
-  resolved "https://registry.yarnpkg.com/@douyinfe/semi-site-markdown-blocks/-/semi-site-markdown-blocks-0.0.17.tgz#36b5dd8c521c2767bcf151af4c832166d9631a6e"
-  integrity sha512-uPVEO8dDJMNoUgiq1mrqqXZW1LTFJ1V+3+zW58mAc9ghZABl4v4DZpsGudft+nUuYGetbosGg3rKa3aXmrFFrg==
+"@douyinfe/semi-site-markdown-blocks@^0.0.18":
+  version "0.0.18"
+  resolved "https://registry.yarnpkg.com/@douyinfe/semi-site-markdown-blocks/-/semi-site-markdown-blocks-0.0.18.tgz#e0c0a29fc5b9574d05a22f23cb8fa8851f6ecb6e"
+  integrity sha512-vx1oi8j08lw1iE/RZFyXCcwuWA8XAUQ9IXUufrV/or1ReRRlziBt5MR4PXoKjr/oW+VgtYbyBdowT9huSG3epg==
   dependencies:
   dependencies:
-    "@douyinfe/semi-site-playground" "0.0.13"
+    "@douyinfe/semi-site-playground" "0.0.14"
     classnames "^2.2.6"
     classnames "^2.2.6"
     prism-react-renderer "^1.0.1"
     prism-react-renderer "^1.0.1"
     prop-types "^15.7.2"
     prop-types "^15.7.2"
 
 
-"@douyinfe/[email protected]3":
-  version "0.0.13"
-  resolved "https://registry.yarnpkg.com/@douyinfe/semi-site-playground/-/semi-site-playground-0.0.13.tgz#70adad18d7a99944ad8529596c554ab49897ecf2"
-  integrity sha512-z5Kj53K1dM02oSuR7Ca10aVsoXo6q/E0UOX7VF2VBmi8vShknL1VRMXsxev20IFSVxZOXlAYGO4SkL1KqWxdSg==
+"@douyinfe/[email protected]4":
+  version "0.0.14"
+  resolved "https://registry.yarnpkg.com/@douyinfe/semi-site-playground/-/semi-site-playground-0.0.14.tgz#c3f9a6adb854f93fbc1c4fd4b75533c1718c18ea"
+  integrity sha512-EYm9ns+IuUhGjVLwTx7+kETdbsLdtQHVwdJj66xtoIBaJHcaG1wy7iJzIIhbf8eOZdP4bT0Q8P5kb5aKdVA4+g==
   dependencies:
   dependencies:
     "@douyinfe/semi-icons" "^2.0.0"
     "@douyinfe/semi-icons" "^2.0.0"
     "@douyinfe/semi-ui" "^2.0.0"
     "@douyinfe/semi-ui" "^2.0.0"