如何引入
基本用法
消息状态
动态更新数据
清除上下文
自定义渲染会话框
自定义渲染输入框
提示信息
自定义提示信息渲染
API
设计变量
Plus 组件 · Chat
对话
用于快速搭建对话内容
使用场景
Chat 组件可用于普通会话,AI 会话等场景。
对话内容渲染基于 MarkdownRender 组件,支持 Markdown 和 MDX,可实现图片,表格,链接,加粗,代码区等常用富文本功能。也可通过 JSX 实现更加复杂定制化的文档撰写与展示需求。
代码演示
如何引入
Chat 从 v2.63.0 版本开始支持。
import { Chat } from '@douyinfe/semi-ui';
基本用法
通过设置
chats
和 onChatsChange
,onMessageSend
实现基础对话显示和交互。附件支持通过点击上传按钮,输入框粘贴,拖拽文件至 Chat 区域上传。通过
uploadProps
设置上传参数,详情参考 Upload。上传按钮的提示文案可通过
uploadTipProps
设置,详情参考 Tooltip。对话是多方参与,多轮交互的场景。可通过
roleConfig
传入角色信息(包括名称,头像等),具体参数细节 RoleConfig。使用
align
属性可以设置对话的布局,支持左右分布(leftRight
, 默认)和左对齐(leftAlign
)。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。其中 status 不同,会话样式不同。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
中处理停止生成逻辑。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
方法清除上下文。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 类型如下export interface RenderTitleProps { message?: Message; role?: Metadata; defaultTitle?: ReactNode } export interface RenderAvatarProps { message?: Message; /* Supported in 2.69.0*/ role?: Metadata, defaultAvatar?: ReactNode } export interface RenderContentProps { message?: Message; role?: Metadata; defaultContent?: ReactNode | ReactNode[]; className?: string; } export interface DefaultActionNodeObj { copyNode: ReactNode; likeNode: ReactNode; dislikeNode: ReactNode; resetNode: ReactNode; deleteNode: ReactNode; } export interface RenderActionProps { message?: Message; defaultActions?: ReactNode | ReactNode[]; className: string; defaultActionsObj?: DefaultActionNodeObj; /* Supported in 2.69.0*/ }; export interface FullChatBoxNodes { avatar?: ReactNode; title?: ReactNode; content?: ReactNode; action?: ReactNode } export interface RenderFullChatBoxProps { message?: Message; role?: Metadata; defaultNodes?: FullChatBoxNodes; className: string; } export interface ChatBoxRenderConfig { /* 自定义渲染标题 */ renderChatBoxTitle?: (props: RenderTitleProps) => ReactNode; /* 自定义渲染头像 */ renderChatBoxAvatar?: (props: RenderAvatarProps) => ReactNode; /* 自定义渲染内容区域 */ renderChatBoxContent?: (props: RenderContentProps) => ReactNode; /* 自定义渲染消息操作栏 */ renderChatBoxAction?: (props: RenderActionProps) => ReactNode; /* 完全自定义渲染整个聊天框 */ renderFullChatBox?: (props: RenderFullChatBoxProps) => ReactNode }
自定义渲染头像和标题,可通过
renderChatBoxAvatar
和 renderChatBoxTitle
实现。
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
自定义渲染操作区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
自定义操作区域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
自定义渲染整个会话框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
自定义渲染输入框,参数如下export interface RenderInputAreaProps { /* 默认节点 */ defaultNode?: ReactNode; /* 如果自定义输入框,发送消息时需调用 */ onSend?: (content?: string, attachment?: FileItem[]) => void; /* 如果自定义清除上下文按钮,点击清除上下文时需调用 */ onClear?: (e?: any) => void; /* detailProps 自 2.69.0 版本开始支持 */ detailProps: { /* 清除上下文按钮 */ clearContextNode?: ReactNode; /* 上传按钮 */ uploadNOde?: ReactNode; /* 文本输入框 */ inputNode?: ReactNode; /* 发送按钮 */ sendNode?: ReactNode; /* 点击触发聚焦文本输入框的处理函数*/ onClick?: (e?: MouseEvent) => void; } }
detailProps
的使用示例如下function CustomInputRender(props) { const { detailProps } = props; const { clearContextNode, uploadNode, inputNode, sendNode, onClick } = detailProps; return <div style={{margin: '8px 16px', display: 'flex', flexDirection:'row', alignItems: 'flex-end', borderRadius: 16,padding: 10, border: '1px solid var(--semi-color-border)'}} onClick={onClick} > {uploadNode} {inputNode} {sendNode} </div> }
其他使用示例如下
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
回调。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
自定义提示区域内容, 参数如下type renderHintBox = (props: {content: string; index: number,onHintClick: () => void}) => React.ReactNode;
使用示例如下:
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 | - |
renderDivider | 自定义渲染分割线, 自 v2.67.0 支持 | (message?: Message) => ReactNode | - |
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 | UploadProps | - |
uploadTipProps | 上传组件提示属性, 详情参考 Tooltip | 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[]) | 发送消息 |