AI 组件

能力介绍

整体介绍 AI 应用场景下的组件能力

Semi AI 组件是一套专为 AI 应用场景设计的创新组件库。面对 AI 正在成为产品主角的时代,传统的设计体系已难以支撑日益复杂的智能交互,Semi AI 组件以「人机智能协同」为核心理念,致力于让用户与 AI 系统协作更高效、可控、可感知、可信赖。Semi AI 组件库包含以下内容:AI Token、AI 基础组件以及 AI Chat 组件,用于服务智能问答与助手、多模态输入与多轮对话等场景。

  • AI Token, 新增加 AI 场景下的基础 20 个颜色的基础 token
  • AI 基础组件, 新增加 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
Purple
General
AI Icon
AI Button
AI Tag
AI
AI
AI
AI
AI
AI
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

User

我想开发一个 Multi Agent 场景下的聊天应用,你能帮我设计一下吗?

产品经理
3 篇资料

收到。为保证方案可落地,我先明确目标与范围:

  • 目标:支持多 Agent 协同回复,用户可选择 Agent 或由系统自动分配
  • MVP 功能:
    1. 基础对话(文本/图片/文件)
    2. Agent 身份标识与头像
    3. 正在输入与流式输出
    4. 引用来源与工具结果展示
  • 约束:先做单会话,不做云端持久化;优先移动端适配

接下来我会整理 PRD 要点并同步给设计与前端。

产品经理

生成的PRD如下,设计师会先根据此摘要出信息架构与关键页面

设计师
paint_key_pages {"file":"PRD"}

设计初稿如下:

  • 信息架构:对话页(历史列表 | 消息流 | 工具卡片区)
  • 视觉:左侧展示 Agent 头像与名称标签,色块区分角色
  • 交互:
    • 输入区支持 @Agent 快速切换与建议提示
    • 流式输出时展示打字气泡与进度占位
    • 工具结果以卡片/步骤条形式插入,可展开详情与复制

我先出低保真线框,稍后补高保真与动效说明。

前端开发

技术方案建议:

  • 技术栈:React + Semi UI,后端采用 WebSocket 或 SSE 支持流式响应
  • 数据模型:消息包含 id、role、name、content、status、references 等字段
  • 组件拆分:AIChatInput + AIChatDialogue;内容采用 Markdown 渲染,支持图片与文件点击
  • 性能:虚拟列表与滚动置底;长文本分块渲染;图片懒加载
  • 可观测性:埋点消息延迟、出错率、工具调用耗时

若确认,我可先搭建页面骨架并接入 mock 数据进行联调。

代码
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 组件搭配使用,优势如下: