Semi AI 组件是一套专为 AI 应用场景设计的创新组件库。面对 AI 正在成为产品主角的时代,传统的设计体系已难以支撑日益复杂的智能交互,Semi AI 组件以「人机智能协同」为核心理念,致力于让用户与 AI 系统协作更高效、可控、可感知、可信赖。Semi AI 组件库包含以下内容:AI Token、AI 基础组件以及 AI Chat 组件,用于服务智能问答与助手、多模态输入与多轮对话等场景。
AI Token, 新增加 AI 场景下的基础 20 个颜色的基础 tokenAI 基础组件, 新增加 AI 场景下的 25 个 icon,以及对Button/Tag/FloatButton新增 AI 风格AI Chat 组件,新增加支持富文本输入、引用,上传、功能配置、及丰富自定义展示的AIChatInput;以及会话展示、选择、编辑、提示、丰富自定义渲染、消息转换的AIChatDialogue,对话组件默认支持 OpenAI 社区 Response / Chat Completion 格式标准,对 GPT-5、GPT-4o 系列模型的响应均支持开箱即用,详见消息数据转换。以及具备产物编辑、产物查看、引用资料展示等能力的多功能侧边栏组件Sidebar。(关于Chat组件和AI Chat组件如何选型见 FAQ)
在未来,我们将支持更多 AI Chat 组件,比如结合 AIChatInput 和 AIChatDialogue 的一体化组件 AIChatBox。用于满足复杂 AI 应用场景下的信息与结果管理需求。
如何引入
AI Chat 组件在独立的 AI 包中,Button、Tag、FloatButton 等基础组件来自 UI 包,图标来自图标包。
pnpm add @fibrous/semi-ai-react
import { AIChatInput, AIChatDialogue, Chat, Sidebar } from '@fibrous/semi-ai-react';
import { Button, Tag, FloatButton } from '@fibrous/semi-ui-react';
import { IconAIFilledLevel1 } from '@fibrous/semi-icons-react';
AI 基础组件
AI 基础组件包括 AI Icon、AI 风格的 Button / Tag / FloatButton, AI 基础组件的 AI 风格是在 AI Token 的基础上实现。
对于 AI Token,我们构建了一套以紫蓝渐变系为核心的品牌色板,在基础色中,新增加了 AI purple 和 AI general 两个色相,20 个颜色的色盘。
对于 AI Icon,支持 Icon 场景下的单色、双色、及多色 Icon,共 25 个图标。
对于 AI 风格的 Button / Tag / FloatButton,可通过组件的 colorful 属性开启。
以下是 AI 基础组件的一些示例,更多示例及使用场景详见 AI Token、AI Icon、AI Button、AI Tag、AI FloatButton。
AI Token
AI Icon
AI Button
AI Tag
AI FloatButton
代码
import { IconAIBellLevel1, IconAIEditLevel2, IconAIFileLevel3, IconAIFilledLevel3, IconAIImageLevel3, IconAISearchLevel3, IconAIStrokedLevel3, IconAIWandLevel3, IconAILoading, IconAIFilledLevel1, IconAIEditLevel1 } from '@fibrous/semi-icons-react';
import { Typography, Button, Tag, FloatButton } from '@fibrous/semi-ui-react';
export default function AIComponentDemo() {
return (
<div style={{ display: 'flex', flexDirection: 'column', rowGap: 20 }}>
<div style={{ display: 'flex', flexDirection: 'column', rowGap: 10 }}>
<Typography.Title heading={5}>AI Token</Typography.Title>
<div style={{ display: 'flex', alignItems: 'center', columnGap: 10 }}>
<span>Purple</span>
<div style={{ display: 'inline-flex', columnGap: 4 }}>
{Array.from({ length: 10 }, (_, index) => (
<div
key={index}
style={{
width: 40,
height: 40,
background: `var(--semi-ai-purple-${index})`,
borderRadius: '50%',
}}
/>
))}
</div>
</div>
<div style={{ display: 'flex', alignItems: 'center', columnGap: 10 }}>
<span>General</span>
<div style={{ display: 'inline-flex', columnGap: 4 }}>
{Array.from({ length: 10 }, (_, index) => (
<div
key={index}
style={{
width: 40,
height: 40,
background: `var(--semi-ai-general-${index})`,
borderRadius: '50%',
}}
/>
))}
</div>
</div>
</div>
<div style={{ display: 'flex', flexDirection: 'column', rowGap: 10 }}>
<Typography.Title heading={5}>AI Icon</Typography.Title>
<div style={{ display: 'flex', columnGap: 10 }}>
<IconAIBellLevel1 size="extra-large" />
<IconAIEditLevel2 size="extra-large" />
<IconAIFileLevel3 size="extra-large" />
<IconAIFilledLevel3 size="extra-large" />
<IconAIImageLevel3 size="extra-large" />
<IconAISearchLevel3 size="extra-large" />
<IconAIStrokedLevel3 size="extra-large" />
<IconAIWandLevel3 fill={['var(--semi-color-danger)', 'var(--semi-color-success)', 'var(--semi-color-primary)', 'var(--semi-color-warning)']} size="extra-large" />
<IconAILoading size="extra-large" />
</div>
</div>
<div style={{ display: 'flex', flexDirection: 'column', rowGap: 10 }}>
<Typography.Title heading={5}>AI Button</Typography.Title>
<div style={{ display: 'flex', rowGap: 16, flexDirection: 'column' }}>
<div style={{ display: 'flex', columnGap: 16 }}>
<Button colorful theme="solid" type="primary" icon={<IconAIFilledLevel1 />}>
Colorful
</Button>
<Button colorful theme="solid" type="primary" loading>
Colorful
</Button>
<Button colorful theme="solid" type="primary" icon={<IconAIFilledLevel1 />} disabled>
Colorful
</Button>
</div>
<div style={{ display: 'flex', columnGap: 16 }}>
<Button colorful theme="solid" type="tertiary" icon={<IconAIFilledLevel3 />}>
Colorful
</Button>
<Button colorful theme="solid" type="tertiary" loading>
Colorful
</Button>
<Button colorful theme="solid" type="tertiary" icon={<IconAIFilledLevel3 />} disabled>
Colorful
</Button>
</div>
<div style={{ display: 'flex', columnGap: 16 }}>
<Button colorful aria-label="AI" theme="solid" type="primary" icon={<IconAIFilledLevel1 />} />
<Button colorful aria-label="AI" theme="solid" type="primary" loading />
<Button colorful aria-label="AI" theme="solid" type="primary" icon={<IconAIFilledLevel1 />} disabled />
<Button colorful aria-label="AI" theme="solid" type="tertiary" icon={<IconAIFilledLevel3 />} />
<Button colorful aria-label="AI" theme="solid" type="tertiary" loading />
<Button colorful aria-label="AI" theme="solid" type="tertiary" icon={<IconAIFilledLevel3 />} disabled />
</div>
</div>
</div>
<div style={{ display: 'flex', flexDirection: 'column', rowGap: 10 }}>
<Typography.Title heading={5} style={{ marginTop: 10 }}>
AI Tag
</Typography.Title>
<div
style={{
display: 'grid',
gridTemplateColumns: 'repeat(3,1fr)',
gridGap: '10px',
width: 'fit-content',
}}
>
<Tag colorful prefixIcon={<IconAIFilledLevel1 size="small" />} type="solid" shape="circle" gradient>
AI
</Tag>
<Tag colorful prefixIcon={<IconAIFilledLevel3 size="small" />} type="light" shape="circle" gradient>
AI
</Tag>
<Tag colorful prefixIcon={<IconAIFilledLevel3 size="small" />} type="ghost" shape="circle" gradient>
AI
</Tag>
<Tag colorful prefixIcon={<IconAIFilledLevel1 size="small" />} type="solid" shape="circle">
AI
</Tag>
<Tag colorful prefixIcon={<IconAIFilledLevel1 size="small" />} type="light" shape="circle">
AI
</Tag>
<Tag colorful prefixIcon={<IconAIFilledLevel1 size="small" />} type="ghost" shape="circle">
AI
</Tag>
</div>
</div>
<div style={{ display: 'flex', flexDirection: 'column', rowGap: 10 }}>
<Typography.Title heading={5} style={{ marginTop: 10 }}>
AI FloatButton
</Typography.Title>
<div
style={{
display: 'grid',
gridTemplateColumns: 'repeat(3,1fr)',
gridGap: '10px',
width: 'fit-content',
}}
>
<FloatButton aria-label="AI edit, VIP" icon={<IconAIEditLevel1 />} colorful size="large" badge={{ count: 'VIP', type: 'danger' }} style={{ position: 'static' }} />
</div>
</div>
</div>
);
}AI Chat 组件构建对话
本例中我们将联合使用 AIChatInput 和 AIChatDialogue 组件,构建一个适用于 Multiple Agent 场景的对话用例。更多示例及使用场景详见 AIChatInput、AIChatDialogue。前后端组合使用用例可参考semi-ai-chat-demo
代码
import { useState, useCallback } from 'react';
import {
AIChatDialogue,
AIChatInput,
chatInputToMessage,
type AIChatDialogueMessage as Message,
type AIChatDialogueAnnotation as Annotation,
type AIChatInputMessageContent as MessageContent,
type AIChatInputReference as Reference,
type AnnotationItemProps,
type DialogueContentItemRendererMap,
} from '@fibrous/semi-ai-react';
import { IconFeishuLogo, IconBookOpenStroked, IconGit, IconFigma, IconWord, IconClose, IconTemplateStroked, IconSearch } from '@fibrous/semi-icons-react';
import { Typography, Button } from '@fibrous/semi-ui-react';
const { Configure } = AIChatInput;
type SideBarContent = { type?: undefined; value?: undefined } | { type: 'annotation'; value: AnnotationItemProps[] } | { type: 'resource'; value: { name: string } };
const simpleIsEqual = (a: unknown, b: unknown) => {
if (a === b) {
return true;
}
if (Number.isNaN(a) && Number.isNaN(b)) {
return true;
}
if (typeof a !== 'object' || a === null || typeof b !== 'object' || b === null) {
return false;
}
const isArrayA = Array.isArray(a);
const isArrayB = Array.isArray(b);
if (isArrayA !== isArrayB) {
return false;
}
const keysA = Object.keys(a);
const keysB = Object.keys(b);
if (keysA.length !== keysB.length) {
return false;
}
for (const key of keysA) {
if (!Object.prototype.hasOwnProperty.call(b, key)) {
return false;
}
if (!simpleIsEqual((a as Record<string, unknown>)[key], (b as Record<string, unknown>)[key])) {
return false;
}
}
return true;
};
const inputOuterStyle = { margin: '12px', minHeight: 150, maxHeight: 300, flexShrink: 0 };
const editingInputOuterStyle = { margin: '12px 0px', maxHeight: 300, flexShrink: 0 };
const dialogueOuterStyle = { flex: 1, overflow: 'auto' };
export default function AIComponentDemo() {
const [sideBarVisible, setSideBarVisible] = useState(false);
const [messages, setMessages] = useState(defaultMessages);
const [generating, setGenerating] = useState(false);
const [references, setReferences] = useState<Reference[]>([]);
const [sideBarContent, setSideBarContent] = useState<SideBarContent>({});
const renderLeftMenu = useCallback(
() => (
<>
<Configure.Select optionList={modelOptions} field="model" initValue="GPT-4o" />
<Configure.Button icon={<IconBookOpenStroked />} field="onlineSearch">
联网搜索
</Configure.Button>
<Configure.Mcp options={mcpOptions} />
<Configure.RadioButton options={radioButtonProps} field="thinkType" initValue="think" />
</>
),
[]
);
const onChatsChange = useCallback((chats: Message[]) => {
setMessages(chats);
}, []);
const onReferenceClick = useCallback((item: Partial<Reference>) => {
setReferences((references) => [...references, { ...item, id: `reference-${Date.now()}` }]);
}, []);
const handleReferenceDelete = useCallback(
(item: Reference) => {
const newReference = references.filter((ref) => ref.id !== item.id);
setReferences(newReference);
},
[references]
);
const onMessageSend = useCallback((props: MessageContent) => {
setGenerating(true);
setMessages((messages) => [
...messages,
{
id: `message-${Date.now()}`,
...chatInputToMessage(props),
},
]);
setReferences([]);
setTimeout(() => {
setGenerating(false);
}, 100);
setTimeout(() => {
setMessages((messages) => {
return [
...messages,
{
id: `message-${Date.now()}`,
role: 'assistant',
name: 'FE',
content: '这是一条 mock 回复信息',
},
];
});
}, 1000);
}, []);
const onEditMessageSend = useCallback(
(props: MessageContent) => {
const index = messages.findIndex((message) => message.editing);
const newMessages = [
...messages.slice(0, index),
{
id: `message-${Date.now()}`,
...chatInputToMessage(props),
},
];
setMessages(newMessages);
},
[messages]
);
const handleEditingReferenceDelete = useCallback(
(item: Reference) => {
const newMessages = messages.map((message) => (message.editing ? { ...message, references: message.references?.filter((ref) => ref.id !== item.id) } : message));
setMessages(newMessages);
},
[messages]
);
const messageEditRender = useCallback(
(props: MessageContent) => {
return (
<AIChatInput
style={editingInputOuterStyle}
generating={false}
references={props.references}
uploadProps={{ ...uploadProps, defaultFileList: props.attachments }}
defaultContent={props.inputContents?.[0].text}
renderConfigureArea={renderLeftMenu}
onMessageSend={onEditMessageSend}
onReferenceDelete={handleEditingReferenceDelete}
/>
);
},
[renderLeftMenu, onEditMessageSend, handleEditingReferenceDelete]
);
const changeSideBarContent = useCallback((content: SideBarContent) => {
setSideBarContent((oldContent) => {
if (!simpleIsEqual(content, oldContent)) {
setSideBarVisible(true);
} else {
setSideBarVisible((v) => !v);
}
return content;
});
}, []);
const onAnnotationClick = useCallback(
(annotations: Annotation[]) => {
changeSideBarContent({
type: 'annotation',
value: annotations,
});
},
[changeSideBarContent]
);
const toggleSideBar = useCallback(() => {
setSideBarVisible((v) => !v);
}, []);
const renderSideBarTitle = useCallback(
({ type }: SideBarContent) => {
return (
<div
style={{
display: 'flex',
alignItems: 'center ',
justifyContent: 'space-between',
padding: 12,
color: 'var(--semi-color-text)',
}}
>
{type === 'annotation' && <div style={{ fontSize: '16px', lineHeight: '22px', fontWeight: 600 }}>参考资料</div>}
{type === 'resource' && <div style={{ fontSize: '16px', lineHeight: '22px', fontWeight: 600 }}>产物列表</div>}
<Button onClick={toggleSideBar} theme="borderless" type="tertiary" aria-label="关闭" icon={<IconClose />} style={{ padding: '0px', width: 24, height: 24 }} />
</div>
);
},
[toggleSideBar]
);
const renderSideBarBody = useCallback((content: SideBarContent) => {
if (content.type === 'annotation') {
return (
<div style={{ display: 'flex', flexDirection: 'column', rowGap: '12px', padding: '12px' }}>
{content.value.map((item, index) => (
<div key={index} style={{ display: 'flex', flexDirection: 'column', rowGap: '8px' }}>
<span style={{ display: 'flex', alignItems: 'center ', columnGap: 4 }}>
<img style={{ width: 20, height: 20, borderRadius: '50%' }} src={item.logo} alt="" />
<span
style={{
fontSize: '14px',
lineHeight: '20px',
fontWeight: 600,
color: 'var(--semi-color-text-0)',
}}
>
{item.title}
</span>
</span>
<Typography.Paragraph ellipsis={{ rows: 3 }} style={{ fontSize: '12px', lineHeight: '16px', color: 'var(--semi-color-text-1)' }}>
{item.detail}
</Typography.Paragraph>
</div>
))}
</div>
);
} else if (content.type === 'resource') {
return (
<div style={{ display: 'flex', flexDirection: 'column', rowGap: '12px', padding: '12px' }}>
<div style={{ display: 'flex', gap: 12, alignItems: 'center' }}>
<IconWord style={{ color: 'var(--semi-color-primary)' }} size="extra-large" /> {content.value.name}
</div>
</div>
);
}
return <div />;
}, []);
const customRender: DialogueContentItemRendererMap = {
resource: (item) => {
return (
<button
type="button"
style={{
display: 'flex',
gap: 8,
width: '100%',
backgroundColor: 'var(--semi-color-fill-0)',
border: 'none',
padding: '12px 16px',
justifyContent: 'center',
alignItems: 'center',
borderRadius: '12px',
color: 'inherit',
font: 'inherit',
cursor: 'pointer',
}}
onClick={() => {
changeSideBarContent({
type: 'resource',
value: item,
});
}}
>
<IconWord style={{ color: 'var(--semi-color-primary)' }} />
{item.name}
</button>
);
},
};
return (
<div style={{ display: 'flex', columnGap: 10 }}>
<div
style={{
display: 'flex',
flexDirection: 'column',
height: 'calc(100vh - 32px)',
overflow: 'hidden',
flexGrow: 1,
}}
>
<AIChatDialogue
style={dialogueOuterStyle}
roleConfig={roleConfig}
showReference
align="leftRight"
mode="bubble"
chats={messages}
onChatsChange={onChatsChange}
onReferenceClick={onReferenceClick}
messageEditRender={messageEditRender}
onAnnotationClick={onAnnotationClick}
renderDialogueContentItem={customRender}
/>
<AIChatInput
style={inputOuterStyle}
placeholder="输入内容或者上传内容"
defaultContent='我是一名<input-slot placeholder="[职业]">程序员</input-slot>,帮我实现<input-slot placeholder="[需求描述]">Multi Agent 场景下的聊天应用</input-slot>需求'
generating={generating}
references={references}
uploadProps={uploadProps}
renderConfigureArea={renderLeftMenu}
onMessageSend={onMessageSend}
onStopGenerate={() => setGenerating(false)}
onReferenceDelete={handleReferenceDelete}
/>
</div>
{sideBarVisible && (
<div
style={{
width: 300,
height: 'calc(100vh - 32px)',
borderRadius: '12px',
border: '1px solid var(--semi-color-border)',
flexShrink: 0,
}}
>
{renderSideBarTitle(sideBarContent)}
{renderSideBarBody(sideBarContent)}
</div>
)}
</div>
);
}
const defaultMessages: Message[] = [
{
id: '1',
role: 'user',
content: '我想开发一个 Multi Agent 场景下的聊天应用,你能帮我设计一下吗?',
status: 'completed',
},
{
id: '2',
role: 'assistant',
name: 'PM',
content: [
{
type: 'message',
content: [
{
type: 'input_text',
text: '收到。为保证方案可落地,我先明确目标与范围:\n\n- 目标:支持多 Agent 协同回复,用户可选择 Agent 或由系统自动分配\n- MVP 功能:\n 1) 基础对话(文本/图片/文件)\n 2) Agent 身份标识与头像\n 3) 正在输入与流式输出\n 4) 引用来源与工具结果展示\n- 约束:先做单会话,不做云端持久化;优先移动端适配\n\n接下来我会整理 PRD 要点并同步给设计与前端。',
annotations: [
{
title: 'Semi Design',
url: 'https://semi.design/zh-CN/start/getting-started',
detail: 'Semi Design 是由抖音前端团队和MED产品设计团队设计、开发并维护的设计系统。作为一个全面、易用、优质的现代应用UI解决方案,Semi Design从字节跳动各业务线的复杂场景中提炼而来,目前已经支撑了近千个平台产品,服务了内外部超过10万用户',
logo: '/assets/placeholder/logo.svg',
},
{
title: 'Semi DSM',
url: 'https://semi.design/zh-CN/start/getting-started',
detail: 'Semi DSM 支持全局、组件级别的样式定制,并在 Figma 和线上代码之间保持同步。使用 DSM,将 Semi Design 适配为 Any Design',
logo: '/assets/placeholder/logo.svg',
},
{
title: 'Semi D2C',
url: 'https://semi.design/zh-CN/start/getting-started',
detail: 'Semi D2C 提供开箱即用的设计稿转代码:支持一键识别 Figma 页面中图层布局 + 设计系统组件,像素级还原设计稿,转译为 React JSX 和 CSS 代码。此外还提供了丰富的扩展能力,基于自定义插件系统快速打造团队专属的设计研发协作工具。',
logo: '/assets/placeholder/logo.svg',
},
],
},
],
},
],
},
{
id: '3',
role: 'assistant',
name: 'PM',
content: [
{
type: 'message',
content: [
{
type: 'input_text',
text: '生成的PRD如下,设计师会先根据此摘要出信息架构与关键页面',
},
{
type: 'resource',
name: 'PRD.doc',
size: '100KB',
},
],
},
],
},
{
id: '4',
role: 'assistant',
name: 'UI',
content: [
{
id: 'rs_02175871288540800000000000000000000ffffac1598778c9aa5',
type: 'reasoning',
summary: [
{
type: 'summary_text',
text: '\n根据产品经理给的 PRD 绘制关键页面,我需要....',
},
],
status: 'completed',
},
{
type: 'function_call',
name: 'paint_key_pages',
arguments: '{"file":"PRD"}',
status: 'completed',
},
{
type: 'message',
content: [
{
type: 'output_text',
text: `设计初稿如下:\n\n- 信息架构:对话页(历史列表 | 消息流 | 工具卡片区)\n- 视觉:左侧展示 Agent 头像与名称标签,色块区分角色\n- 交互:\n - 输入区支持 @Agent 快速切换与建议提示\n - 流式输出时展示打字气泡与进度占位\n - 工具结果以卡片/步骤条形式插入,可展开详情与复制\n\n我先出低保真线框,稍后补高保真与动效说明。`,
},
],
status: 'completed',
},
],
status: 'completed',
},
{
id: '5',
role: 'assistant',
name: 'FE',
content: `技术方案建议:\n\n- 技术栈:React + Semi UI,后端采用 WebSocket 或 SSE 支持流式响应\n- 数据模型:消息包含 id、role、name、content、status、references 等字段\n- 组件拆分:AIChatInput + AIChatDialogue;内容采用 Markdown 渲染,支持图片与文件点击\n- 性能:虚拟列表与滚动置底;长文本分块渲染;图片懒加载\n- 可观测性:埋点消息延迟、出错率、工具调用耗时\n\n若确认,我可先搭建页面骨架并接入 mock 数据进行联调。`,
},
];
const roleConfig = {
user: {
name: 'User',
avatar: '/assets/ai/img/user.png',
},
assistant: new Map([
[
'PM',
{
name: '产品经理',
avatar: '/assets/ai/PM.png',
},
],
[
'UI',
{
name: '设计师',
avatar: '/assets/ai/UI.png',
},
],
[
'FE',
{
name: '前端开发',
avatar: '/assets/ai/FE.png',
},
],
]),
};
const uploadProps = {
action: 'https://api.semi.design/upload',
};
const modelOptions = [
{
value: 'GPT-5',
label: 'GPT-5',
type: 'gpt',
},
{
value: 'GPT-4o',
label: 'GPT-4o',
type: 'gpt',
},
{
value: 'Claude 3.5 Sonnet',
label: 'Claude 3.5 Sonnet',
type: 'claude',
},
];
const mcpOptions = [
{
icon: <IconFeishuLogo />,
label: '飞书文档',
value: 'feishu',
},
{
icon: <IconGit />,
label: 'Github Mcp',
value: 'github',
},
{
icon: <IconFigma />,
label: 'IconFigma Mcp',
value: 'IconFigma',
},
];
const radioButtonProps = [
{ label: <IconTemplateStroked aria-label="快速" />, value: 'fast' },
{ label: <IconSearch aria-label="思考" />, value: 'think' },
];FAQ
- Chat 组件和 AI Chat 系列组件应该如何选型?
- 如果场景比较简单,仅需要普通文字对话和简单文件图片展示,推荐 Chat 组件。
- Chat 组件默认集成 input 输入和消息展示部分,优势是理解简单,能够快速上手;缺点是复杂的输入框或者消息展示需求定制相对困难,工作量大。
- 如果场景相对复杂,推荐将 AIChatInput 和 AIChatDialogue 组件搭配使用,优势如下:
- AIChatInput 支持更复杂的样式定制。
- AIChatDialogue 消息展示的灵活性更高。
- 组件默认支持 OpenAI 的 Response / Chat Completion Object 格式标准,调用内部消息转换函数可轻松进行将 OpenAI 返回的结果转换为组件需要的数据结构。
- 提供根据消息类型的定制展示的 API,方便快速实现消息展示。
- 默认支持消息引用和选择操作。
- 如果场景比较简单,仅需要普通文字对话和简单文件图片展示,推荐 Chat 组件。



