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 @fibrae/semi-ai-svelte
<script lang="ts">
import { AIChatInput, AIChatDialogue, Chat, Sidebar } from '@fibrae/semi-ai-svelte';
import { Button, Tag, FloatButton } from '@fibrae/semi-ui-svelte';
import { IconAIFilledLevel1 } from '@fibrae/semi-icons-svelte';
</script>
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
代码
<script lang="ts">
import { IconAIBellLevel1, IconAIEditLevel1, IconAIEditLevel2, IconAIFileLevel3, IconAIFilledLevel1, IconAIFilledLevel3, IconAIImageLevel3, IconAILoading, IconAISearchLevel3, IconAIStrokedLevel3, IconAIWandLevel3 } from '@fibrae/semi-icons-svelte';
import { Button, FloatButton, Tag, TypographyTitle } from '@fibrae/semi-ui-svelte';
const wandFill = ['var(--semi-color-danger)', 'var(--semi-color-success)', 'var(--semi-color-primary)', 'var(--semi-color-warning)'];
const swatchCount = Array.from({ length: 10 }, (_, index) => index);
const colorStyle = (color: string) => `width: 40px; height: 40px; background: ${color}; border-radius: 50%`;
const grid = 'display: grid; grid-template-columns: repeat(3, 1fr); grid-gap: 10px; width: fit-content';
</script>
{#snippet level1()}
<IconAIFilledLevel1 />
{/snippet}
{#snippet level3()}
<IconAIFilledLevel3 />
{/snippet}
{#snippet tagLevel1()}
<IconAIFilledLevel1 size="small" />
{/snippet}
{#snippet tagLevel3()}
<IconAIFilledLevel3 size="small" />
{/snippet}
{#snippet editLevel1()}
<IconAIEditLevel1 />
{/snippet}
<div style="display: flex; flex-direction: column; row-gap: 20px">
<div style="display: flex; flex-direction: column; row-gap: 10px">
<TypographyTitle heading={5}>AI Token</TypographyTitle>
<div style="display: flex; align-items: center; column-gap: 10px">
<span>Purple</span>
<div style="display: inline-flex; column-gap: 4px">
{#each swatchCount as index (index)}
<div style={colorStyle(`var(--semi-ai-purple-${index})`)}></div>
{/each}
</div>
</div>
<div style="display: flex; align-items: center; column-gap: 10px">
<span>General</span>
<div style="display: inline-flex; column-gap: 4px">
{#each swatchCount as index (index)}
<div style={colorStyle(`var(--semi-ai-general-${index})`)}></div>
{/each}
</div>
</div>
</div>
<div style="display: flex; flex-direction: column; row-gap: 10px">
<TypographyTitle heading={5}>AI Icon</TypographyTitle>
<div style="display: flex; column-gap: 10px">
<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={wandFill} size="extra-large" />
<IconAILoading size="extra-large" />
</div>
</div>
<div style="display: flex; flex-direction: column; row-gap: 10px">
<TypographyTitle heading={5}>AI Button</TypographyTitle>
<div style="display: flex; row-gap: 16px; flex-direction: column">
<div style="display: flex; column-gap: 16px">
<Button colorful icon={level1} theme="solid" type="primary">Colorful</Button>
<Button colorful loading theme="solid" type="primary">Colorful</Button>
<Button colorful disabled icon={level1} theme="solid" type="primary">Colorful</Button>
</div>
<div style="display: flex; column-gap: 16px">
<Button colorful icon={level3} theme="solid" type="tertiary">Colorful</Button>
<Button colorful loading theme="solid" type="tertiary">Colorful</Button>
<Button colorful disabled icon={level3} theme="solid" type="tertiary">Colorful</Button>
</div>
<div style="display: flex; column-gap: 16px">
<Button aria-label="AI" colorful icon={level1} theme="solid" type="primary" />
<Button aria-label="AI" colorful loading theme="solid" type="primary" />
<Button aria-label="AI" colorful disabled icon={level1} theme="solid" type="primary" />
<Button aria-label="AI" colorful icon={level3} theme="solid" type="tertiary" />
<Button aria-label="AI" colorful loading theme="solid" type="tertiary" />
<Button aria-label="AI" colorful disabled icon={level3} theme="solid" type="tertiary" />
</div>
</div>
</div>
<div style="display: flex; flex-direction: column; row-gap: 10px">
<TypographyTitle heading={5} style="margin-top: 10px">AI Tag</TypographyTitle>
<div style={grid}>
<Tag colorful gradient prefixIcon={tagLevel1} shape="circle" type="solid">AI</Tag>
<Tag colorful gradient prefixIcon={tagLevel3} shape="circle" type="light">AI</Tag>
<Tag colorful gradient prefixIcon={tagLevel3} shape="circle" type="ghost">AI</Tag>
<Tag colorful prefixIcon={tagLevel1} shape="circle" type="solid">AI</Tag>
<Tag colorful prefixIcon={tagLevel1} shape="circle" type="light">AI</Tag>
<Tag colorful prefixIcon={tagLevel1} shape="circle" type="ghost">AI</Tag>
</div>
</div>
<div style="display: flex; flex-direction: column; row-gap: 10px">
<TypographyTitle heading={5} style="margin-top: 10px">AI FloatButton</TypographyTitle>
<div style={grid}>
<FloatButton aria-label="AI edit, VIP" badge={{ count: 'VIP', type: 'danger' }} colorful icon={editLevel1} size="large" style="position: static" />
</div>
</div>
</div>AI Chat 组件构建对话
本例中我们将联合使用 AIChatInput 和 AIChatDialogue 组件,构建一个适用于 Multiple Agent 场景的对话用例。更多示例及使用场景详见 AIChatInput、AIChatDialogue。前后端组合使用用例可参考semi-ai-chat-demo
代码
<script lang="ts">
import type { Component } from 'svelte';
import { AIChatDialogue, AIChatInput, chatInputToMessage, type AIChatDialogueAnnotation as Annotation, type AIChatDialogueMessage as Message, type AIChatInputMessageContent as MessageContent, type AIChatInputReference as Reference, type AnnotationItemProps } from '@fibrae/semi-ai-svelte';
import { IconBookOpenStroked, IconClose, IconFeishuLogo, IconFigma, IconGit, IconSearch, IconTemplateStroked, IconWord } from '@fibrae/semi-icons-svelte';
import { Button, TypographyParagraph } from '@fibrae/semi-ui-svelte';
const { Configure } = AIChatInput;
type SideBarContent = { type?: undefined; value?: undefined } | { type: 'annotation'; value: AnnotationItemProps[] } | { type: 'resource'; value: { name: string } };
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 defaultInput = '我是一名<input-slot placeholder="[职业]">程序员</input-slot>,帮我实现<input-slot placeholder="[需求描述]">Multi Agent 场景下的聊天应用</input-slot>需求';
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 = [
{
label: '飞书文档',
value: 'feishu',
},
{
label: 'Github Mcp',
value: 'github',
},
{
label: 'IconFigma Mcp',
value: 'IconFigma',
},
];
const mcpIcons: Record<string, Component> = {
feishu: IconFeishuLogo,
github: IconGit,
IconFigma,
};
const radioOptions = [
{ label: '快速', value: 'fast' },
{ label: '思考', value: 'think' },
];
const inputOuterStyle = 'margin: 12px; min-height: 150px; max-height: 300px; flex-shrink: 0';
const editingInputOuterStyle = 'margin: 12px 0px; max-height: 300px; flex-shrink: 0';
const sideBarTitleStyle = 'font-size: 16px; line-height: 22px; font-weight: 600';
const resourceButtonStyle = 'display: flex; gap: 8px; width: 100%; background-color: var(--semi-color-fill-0); border: none; padding: 12px 16px; justify-content: center; align-items: center; border-radius: 12px; color: inherit; font: inherit; cursor: pointer';
let sideBarVisible = $state(false);
let messages = $state.raw(defaultMessages);
let generating = $state(false);
let references = $state.raw<Reference[]>([]);
let sideBarContent = $state.raw<SideBarContent>({});
const onReferenceClick = (item: Partial<Reference>) => {
references = [...references, { ...item, id: `reference-${Date.now()}` }];
};
const onReferenceDelete = (item: Reference) => {
references = references.filter((ref) => ref.id !== item.id);
};
const onMessageSend = (content: MessageContent) => {
generating = true;
messages = [...messages, { id: `message-${Date.now()}`, ...chatInputToMessage(content) }];
references = [];
setTimeout(() => {
generating = false;
}, 100);
setTimeout(() => {
messages = [
...messages,
{
id: `message-${Date.now()}`,
role: 'assistant',
name: 'FE',
content: '这是一条 mock 回复信息',
},
];
}, 1000);
};
const onEditMessageSend = (content: MessageContent) => {
const index = messages.findIndex((message) => message.editing);
messages = [...messages.slice(0, index), { id: `message-${Date.now()}`, ...chatInputToMessage(content) }];
};
const onEditingReferenceDelete = (item: Reference) => {
messages = messages.map((message) => (message.editing ? { ...message, references: message.references?.filter((ref) => ref.id !== item.id) } : message));
};
const changeSideBarContent = (content: SideBarContent) => {
sideBarVisible = JSON.stringify(content) === JSON.stringify(sideBarContent) ? !sideBarVisible : true;
sideBarContent = content;
};
const onAnnotationClick = (annotations: Annotation[]) => {
changeSideBarContent({ type: 'annotation', value: annotations });
};
const resourceOf = (item: unknown) => item as { name: string };
</script>
{#snippet configureArea()}
<Configure.Select field="model" initValue="GPT-4o" optionList={modelOptions} />
<Configure.Button field="onlineSearch">
{#snippet icon()}
<IconBookOpenStroked />
{/snippet}
联网搜索
</Configure.Button>
<Configure.Mcp options={mcpOptions}>
{#snippet optionIcon({ item })}
{@const Icon = mcpIcons[item.value]}
<Icon />
{/snippet}
</Configure.Mcp>
<Configure.RadioButton field="thinkType" initValue="think" options={radioOptions}>
{#snippet label({ option })}
{#if option.value === 'fast'}
<IconTemplateStroked aria-label={String(option.label)} />
{:else}
<IconSearch aria-label={String(option.label)} />
{/if}
{/snippet}
</Configure.RadioButton>
{/snippet}
<div style="display: flex; column-gap: 10px">
<div style="display: flex; flex-direction: column; height: calc(100vh - 32px); overflow: hidden; flex-grow: 1">
<AIChatDialogue align="leftRight" bind:chats={messages} mode="bubble" {onAnnotationClick} {onReferenceClick} {roleConfig} showReference style="flex: 1; overflow: auto">
{#snippet messageEdit(content)}
<AIChatInput
{configureArea}
defaultContent={content.inputContents?.[0].text}
generating={false}
onMessageSend={onEditMessageSend}
onReferenceDelete={onEditingReferenceDelete}
references={content.references}
style={editingInputOuterStyle}
uploadProps={{ ...uploadProps, defaultFileList: content.attachments }}
/>
{/snippet}
{#snippet contentItem({ item, type, defaultItem })}
{#if type === 'resource'}
<button style={resourceButtonStyle} type="button" onclick={() => changeSideBarContent({ type: 'resource', value: resourceOf(item) })}>
<IconWord style="color: var(--semi-color-primary)" />
{resourceOf(item).name}
</button>
{:else}
{@render defaultItem()}
{/if}
{/snippet}
</AIChatDialogue>
<AIChatInput {configureArea} defaultContent={defaultInput} {generating} {onMessageSend} {onReferenceDelete} onStopGenerate={() => (generating = false)} placeholder="输入内容或者上传内容" {references} style={inputOuterStyle} {uploadProps} />
</div>
{#if sideBarVisible}
<div style="width: 300px; height: calc(100vh - 32px); border-radius: 12px; border: 1px solid var(--semi-color-border); flex-shrink: 0">
<div style="display: flex; align-items: center; justify-content: space-between; padding: 12px; color: var(--semi-color-text)">
{#if sideBarContent.type === 'annotation'}
<div style={sideBarTitleStyle}>参考资料</div>
{/if}
{#if sideBarContent.type === 'resource'}
<div style={sideBarTitleStyle}>产物列表</div>
{/if}
<Button aria-label="关闭" style="padding: 0px; width: 24px; height: 24px" theme="borderless" type="tertiary" onclick={() => (sideBarVisible = !sideBarVisible)}>
{#snippet icon()}
<IconClose />
{/snippet}
</Button>
</div>
{#if sideBarContent.type === 'annotation'}
<div style="display: flex; flex-direction: column; row-gap: 12px; padding: 12px">
{#each sideBarContent.value as item, index (index)}
<div style="display: flex; flex-direction: column; row-gap: 8px">
<span style="display: flex; align-items: center; column-gap: 4px">
<img alt="" src={item.logo} style="width: 20px; height: 20px; border-radius: 50%" />
<span style="font-size: 14px; line-height: 20px; font-weight: 600; color: var(--semi-color-text-0)">
{item.title}
</span>
</span>
<TypographyParagraph ellipsis={{ rows: 3 }} style="font-size: 12px; line-height: 16px; color: var(--semi-color-text-1)">
{item.detail}
</TypographyParagraph>
</div>
{/each}
</div>
{:else if sideBarContent.type === 'resource'}
<div style="display: flex; flex-direction: column; row-gap: 12px; padding: 12px">
<div style="display: flex; gap: 12px; align-items: center">
<IconWord size="extra-large" style="color: var(--semi-color-primary)" />
{sideBarContent.value.name}
</div>
</div>
{/if}
</div>
{/if}
</div>FAQ
- Chat 组件和 AI Chat 系列组件应该如何选型?
- 如果场景比较简单,仅需要普通文字对话和简单文件图片展示,推荐 Chat 组件。
- Chat 组件默认集成 input 输入和消息展示部分,优势是理解简单,能够快速上手;缺点是复杂的输入框或者消息展示需求定制相对困难,工作量大。
- 如果场景相对复杂,推荐将 AIChatInput 和 AIChatDialogue 组件搭配使用,优势如下:
- AIChatInput 支持更复杂的样式定制。
- AIChatDialogue 消息展示的灵活性更高。
- 组件默认支持 OpenAI 的 Response / Chat Completion Object 格式标准,调用内部消息转换函数可轻松进行将 OpenAI 返回的结果转换为组件需要的数据结构。
- 提供根据消息类型的定制展示的 API,方便快速实现消息展示。
- 默认支持消息引用和选择操作。
- 如果场景比较简单,仅需要普通文字对话和简单文件图片展示,推荐 Chat 组件。