Plus 组件

对话

用于快速搭建对话内容

使用场景

Chat 组件可用于普通会话,AI 会话等场景。

对话内容渲染基于 MarkdownRender 组件,支持 Markdown 和 MDX (注:Chat 中的 MarkdownRender 的默认 format 模式是 md,如果需要使用 MDX 格式,可通过 markdownRenderProps API 设置),可实现图片,表格,链接,加粗,代码区等常用富文本功能。也可通过 JSX 实现更加复杂定制化的文档撰写与展示需求。代码区的高亮语言需先通过 CodeHighlight 的 CodeHighlight.register 注册。

代码演示

如何引入

import { Chat } from '@fibrous/semi-ai-solid';

基本用法

通过设置 chats、onChatsChange 和 onMessageSend 实现基础对话显示和交互。chats 是受控属性,只有在 onChatsChange 中写回新的列表,对话列表才会变化。

附件支持通过点击上传按钮,输入框粘贴,拖拽文件至 Chat 区域上传。通过 uploadProps 设置上传参数,详情参考 Upload。

上传按钮的提示文案可通过 uploadTipProps 设置,content 为提示文字,详情参考 Tooltip。

对话是多方参与,多轮交互的场景。可通过 roleConfig 传入角色信息(包括名称,头像等),具体参数细节 RoleConfig。

使用 align 属性可以设置对话的布局,支持左右分布(leftRight, 默认)和左对齐(leftAlign)。

模式
会话布局方式
System

Hello, I'm your AI assistant.

User

给一个 Semi Design 的 Button 组件的使用示例

Assistant

以下是一个 Semi 代码的使用示例:

jsx
import React from 'react';
import { Button } from '@fibrous/semi-ui-react';

const MyComponent = () => {
  return (
    <Button>Click me</Button>
 );
};
export default MyComponent;
代码
import { createSignal } from 'solid-js';
import { Chat, type ChatMessage as Message, type ChatProps } from '@fibrous/semi-ai-solid';
import { Radio, RadioGroup } from '@fibrous/semi-ui-solid';

const defaultMessage: Message[] = [
    {
        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 '@fibrous/semi-ui-react';\n\nconst MyComponent = () => {\n  return (\n    <Button>Click me</Button>\n );\n};\nexport default MyComponent;\n```\n",
    },
];

const roleInfo = {
    user: {
        name: 'User',
        avatar: '/assets/docs-icon.png',
    },
    assistant: {
        name: 'Assistant',
        avatar: '/assets/placeholder/logo.svg',
    },
    system: {
        name: 'System',
        avatar: '/assets/placeholder/logo.svg',
    },
};

const commonOuterStyle = {
    border: '1px solid var(--semi-color-border)',
    'border-radius': '16px',
    margin: '8px 16px',
    height: '550px',
};

let id = 0;
function getId() {
    return `id-${id++}`;
}

const uploadProps = { action: 'https://api.semi.design/upload' };
const uploadTipProps = { content: '自定义上传按钮提示信息' };

export default function ChatDemo() {
    const [chats, setChats] = createSignal<Message[]>(defaultMessage);
    const [mode, setMode] = createSignal<NonNullable<ChatProps['mode']>>('bubble');
    const [align, setAlign] = createSignal<NonNullable<ChatProps['align']>>('leftRight');

    const onMessageSend = () => {
        const newAssistantMessage: Message = {
            role: 'assistant',
            id: getId(),
            createAt: Date.now(),
            content: '这是一条 mock 回复信息',
        };
        setTimeout(() => {
            setChats((list) => [...list, newAssistantMessage]);
        }, 200);
    };

    const onMessageReset = () => {
        setTimeout(() => {
            setChats((list) => {
                const lastMessage = list[list.length - 1];
                const newLastMessage: Message = {
                    ...lastMessage,
                    status: 'complete',
                    content: 'This is a mock reset message.',
                };
                return [...list.slice(0, -1), newLastMessage];
            });
        }, 200);
    };

    return (
        <>
            <span style={{ display: 'flex', 'flex-direction': 'column', 'row-gap': '8px' }}>
                <span style={{ display: 'flex', 'align-items': 'center', 'column-gap': '10px' }}>
                    模式
                    <RadioGroup onChange={(e) => setMode(e.target.value)} value={mode()} type="button">
                        <Radio value="bubble">气泡</Radio>
                        <Radio value="noBubble">非气泡</Radio>
                        <Radio value="userBubble">用户会话气泡</Radio>
                    </RadioGroup>
                </span>
                <span style={{ display: 'flex', 'align-items': 'center', 'column-gap': '10px' }}>
                    会话布局方式
                    <RadioGroup onChange={(e) => setAlign(e.target.value)} value={align()} type="button">
                        <Radio value="leftRight">左右分布</Radio>
                        <Radio value="leftAlign">左对齐</Radio>
                    </RadioGroup>
                </span>
            </span>
            <Chat align={align()} mode={mode()} uploadProps={uploadProps} style={commonOuterStyle} chats={chats()} roleConfig={roleInfo} onChatsChange={(next) => setChats(next)} onMessageSend={onMessageSend} onMessageReset={onMessageReset} uploadTipProps={uploadTipProps} />
        </>
    );
}

消息状态

chats 类型为 Message[], Message 包含对话的各种信息,如角色(role)、内容(content)、附件(attachment)、状态(status)、唯一标识(id)、创建时间(createAt)等,具体见 Message。其中 status 不同,会话样式不同。

Assistant

请求成功

请求错误

代码
import { createSignal } from 'solid-js';
import { Chat, type ChatMessage as Message } from '@fibrous/semi-ai-solid';

const defaultMessage: Message[] = [
    {
        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: '/assets/docs-icon.png',
    },
    assistant: {
        name: 'Assistant',
        avatar: '/assets/placeholder/logo.svg',
    },
    system: {
        name: 'System',
        avatar: '/assets/placeholder/logo.svg',
    },
};

const commonOuterStyle = {
    border: '1px solid var(--semi-color-border)',
    'border-radius': '16px',
    height: '400px',
};

let id = 0;
function getId() {
    return `id-${id++}`;
}
const uploadProps = { action: 'https://api.semi.design/upload' };

export default function ChatDemo() {
    const [chats, setChats] = createSignal<Message[]>(defaultMessage);

    const onMessageSend = () => {
        const newAssistantMessage: Message = {
            role: 'assistant',
            id: getId(),
            createAt: Date.now(),
            content: '这是一条 mock 回复信息',
        };
        setTimeout(() => {
            setChats((list) => [...list, newAssistantMessage]);
        }, 200);
    };

    return <Chat style={commonOuterStyle} chats={chats()} roleConfig={roleInfo} onChatsChange={(next) => setChats(next)} onMessageSend={onMessageSend} uploadProps={uploadProps} />;
}

动态更新数据

对于后台返回 Serve Side Event 数据情况,可将获取到的数据用于更新 chats,对话内容将实时更新。

showStopGenerate 参数可用于设置是否展示停止生成按钮,默认为 false。可以在 onStopGenerator 中处理停止生成逻辑。

System

Hello, I'm your AI assistant.

User

介绍一下 Semi design

Assistant

Semi Design 是由抖音前端团队和MED产品设计团队设计、开发并维护的设计系统。作为一个全面、易用、优质的现代应用UI解决方案,Semi Design从字节跳动各业务线的复杂场景中提炼而来,目前已经支撑了近千个平台产品,服务了内外部超过10万用户[1]。

Semi Design的特点包括:

  1. 设计简洁、现代化。
  2. 提供主题方案,可深度样式定制。
  3. 提供明暗色两套模式,切换方便。
  4. 国际化,覆盖了简/繁体中文、英语、日语、韩语、葡萄牙语等20+种语言,日期时间组件提供全球时区支持,全部组件可自动适配阿拉伯文RTL布局。
  5. 采用 Foundation 和 Adapter 跨框架技术方案,方便扩展。

Learn more:

  1. Introduction 介绍 - Semi Design
  2. Getting Started 快速开始 - Semi Design
  3. Semi D2C 设计稿转代码的演进之路 - 知乎
代码
import { createSignal, onCleanup, onMount } from 'solid-js';
import { Chat, type ChatMessage as Message } from '@fibrous/semi-ai-solid';

const defaultMessage: Message[] = [
    {
        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: '/assets/docs-icon.png',
    },
    assistant: {
        name: 'Assistant',
        avatar: '/assets/placeholder/logo.svg',
    },
    system: {
        name: 'System',
        avatar: '/assets/placeholder/logo.svg',
    },
};

const commonOuterStyle = {
    border: '1px solid var(--semi-color-border)',
    'border-radius': '16px',
    height: '600px',
};

let id = 0;
function getId() {
    return `id-${id++}`;
}
const uploadProps = { action: 'https://api.semi.design/upload' };

export default function ChatDemo() {
    const [chats, setChats] = createSignal<Message[]>(defaultMessage);
    // the interval outlives the send that started it, so it is cleared when the demo unmounts too
    let intervalId: number | undefined;
    // registered on mount: a server render disposes without mounting and has no window
    onMount(() => onCleanup(() => window.clearInterval(intervalId)));

    const generateMockResponse = (content: string) => {
        const id = window.setInterval(() => {
            setChats((list) => {
                const lastMessage = list[list.length - 1];
                let newMessage: Message = { ...lastMessage };
                if (lastMessage.status === 'loading') {
                    newMessage = {
                        ...newMessage,
                        content: `mock Response for ${content} \n`,
                        status: 'incomplete',
                    };
                } else if (lastMessage.status === 'incomplete') {
                    if ((lastMessage.content?.length ?? 0) > 200) {
                        window.clearInterval(id);
                        intervalId = undefined;
                        newMessage = {
                            ...newMessage,
                            content: `${lastMessage.content} mock stream message`,
                            status: 'complete',
                        };
                    } else {
                        newMessage = {
                            ...newMessage,
                            content: `${lastMessage.content} mock stream message`,
                        };
                    }
                }
                return [...list.slice(0, -1), newMessage];
            });
        }, 400);
        intervalId = id;
    };

    const onMessageSend = (content: string) => {
        setChats((list) => [
            ...list,
            {
                role: 'assistant',
                status: 'loading',
                createAt: Date.now(),
                id: getId(),
            },
        ]);
        generateMockResponse(content);
    };

    const onStopGenerator = () => {
        if (intervalId) {
            window.clearInterval(intervalId);
            setChats((list) => {
                const lastMessage = list[list.length - 1];
                if (lastMessage.status && lastMessage.status !== 'complete') {
                    return [...list.slice(0, -1), { ...lastMessage, status: 'complete' }];
                }
                return list;
            });
        }
    };

    return <Chat chats={chats()} showStopGenerate style={commonOuterStyle} onStopGenerator={onStopGenerator} roleConfig={roleInfo} onChatsChange={(next) => setChats(next)} onMessageSend={onMessageSend} uploadProps={uploadProps} />;
}

清除上下文

通过 showClearContext 可以开启在输入框中显示清除上下文按钮,默认为 false。也可以通过 ref 调用 clearContext 方法清除上下文。

System

Hello, I'm your AI assistant.

User

介绍一下 semi design

Assistant

Semi Design 是由抖音前端团队和MED产品设计团队设计、开发并维护的设计系统

代码
import { createSignal } from 'solid-js';
import { Chat, type ChatMessage as Message } from '@fibrous/semi-ai-solid';

const defaultMessage: Message[] = [
    {
        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: '/assets/docs-icon.png',
    },
    assistant: {
        name: 'Assistant',
        avatar: '/assets/placeholder/logo.svg',
    },
    system: {
        name: 'System',
        avatar: '/assets/placeholder/logo.svg',
    },
};

const commonOuterStyle = {
    border: '1px solid var(--semi-color-border)',
    'border-radius': '16px',
    margin: '8px 16px',
    height: '550px',
};

let id = 0;
function getId() {
    return `id-${id++}`;
}

const uploadProps = { action: 'https://api.semi.design/upload' };
const uploadTipProps = { content: '自定义上传按钮提示信息' };

export default function ChatDemo() {
    const [chats, setChats] = createSignal<Message[]>(defaultMessage);

    const onMessageSend = () => {
        const newAssistantMessage: Message = {
            role: 'assistant',
            id: getId(),
            createAt: Date.now(),
            content: '这是一条 mock 回复信息',
        };
        setTimeout(() => {
            setChats((list) => [...list, newAssistantMessage]);
        }, 200);
    };

    const onMessageReset = () => {
        setTimeout(() => {
            setChats((list) => {
                const lastMessage = list[list.length - 1];
                const newLastMessage: Message = {
                    ...lastMessage,
                    status: 'complete',
                    content: 'This is a mock reset message.',
                };
                return [...list.slice(0, -1), newLastMessage];
            });
        }, 200);
    };

    return <Chat uploadProps={uploadProps} style={commonOuterStyle} chats={chats()} roleConfig={roleInfo} onChatsChange={(next) => setChats(next)} onMessageSend={onMessageSend} onMessageReset={onMessageReset} uploadTipProps={uploadTipProps} showClearContext />;
}

自定义渲染会话框

通过 chatBoxRenderConfig 传入自定义渲染配置, chatBoxRenderConfig 类型如下

export interface RenderTitleProps {
    readonly message?: Message;
    readonly role?: Metadata;
    /* 创建默认标题,在插入处调用 */
    readonly defaultTitle?: () => JSX.Element;
}

export interface RenderAvatarProps {
    readonly message?: Message;
    readonly role?: Metadata;
    /* 创建默认头像,在插入处调用 */
    readonly defaultAvatar?: () => JSX.Element;
}

export interface RenderRoleAvatarProps {
    readonly message?: Message;
    readonly role?: Metadata;
}

export interface RenderContentProps {
    readonly message?: Message;
    readonly role?: Metadata;
    /* 创建默认内容,在插入处调用 */
    readonly defaultContent?: () => JSX.Element;
    readonly class?: string;
}

/* 与默认操作栏显示的按钮一致,按钮不显示时(如消息未完成)对应的键不存在;值是按钮的渲染函数 */
export interface DefaultActionNodeObj {
    readonly copyNode?: () => JSX.Element;
    readonly likeNode?: () => JSX.Element;
    readonly dislikeNode?: () => JSX.Element;
    readonly resetNode?: () => JSX.Element;
    readonly deleteNode?: () => JSX.Element;
}

export interface RenderActionProps {
    readonly message?: Message;
    /* 默认按钮的渲染函数,按显示顺序排列;在插入处调用 */
    readonly defaultActions?: (() => JSX.Element)[];
    readonly class: string;
    defaultActionsObj?: DefaultActionNodeObj;
}

export interface FullChatBoxNodes {
    readonly avatar?: JSX.Element;
    readonly title?: JSX.Element;
    readonly content?: JSX.Element;
    readonly action?: JSX.Element;
}

export interface RenderFullChatBoxProps {
    readonly message?: Message;
    readonly role?: Metadata;
    defaultNodes?: FullChatBoxNodes;
    readonly class: string;
}

export interface ChatBoxRenderConfig {
    /* 自定义渲染标题 */
    renderChatBoxTitle?: (props: RenderTitleProps) => JSX.Element;
    /* 自定义渲染头像 */
    renderChatBoxAvatar?: (props: RenderAvatarProps) => JSX.Element;
    /* 渲染内置头像里的内容,用于 avatar 图片地址之外的头像 */
    renderRoleAvatar?: (props: RenderRoleAvatarProps) => JSX.Element;
    /* 自定义渲染内容区域 */
    renderChatBoxContent?: (props: RenderContentProps) => JSX.Element;
    /* 自定义渲染消息操作栏 */
    renderChatBoxAction?: (props: RenderActionProps) => JSX.Element;
    /* 完全自定义渲染整个聊天框 */
    renderFullChatBox?: (props: RenderFullChatBoxProps) => JSX.Element;
}

每个渲染函数对每条消息调用一次,替换函数时重新调用。参数的字段是 getter,返回的 JSX 会随消息流式更新。默认节点是渲染函数(defaultTitle、defaultAvatar、defaultContent、defaultNodes 的各项、操作栏按钮 defaultActions 的各项与 defaultActionsObj 的各值),每次调用都会新建节点,只在插入处调用,没有调用的节点不会创建,例如 {props.defaultTitle?.()}、<For each={props.defaultActions}>{(action) => action()}</For>。

头像渲染模式
标题渲染模式

Hello, I'm your AI assistant.

这张图片里有什么?

图片

图片中是一个有卡通画像装饰的黄色背包。

代码
import { createMemo, createSignal } from 'solid-js';
import { Chat, type ChatMessage as Message, type ChatRenderAvatarProps as RenderAvatarProps, type ChatRenderTitleProps as RenderTitleProps } from '@fibrous/semi-ai-solid';
import { Avatar, Radio, RadioGroup } from '@fibrous/semi-ui-solid';

const defaultMessage: Message[] = [
    {
        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: '/assets/root-web-sites/edit-bag.jpeg',
                },
            },
        ],
    },
    {
        role: 'assistant',
        id: '3',
        createAt: 1715676751919,
        content: '图片中是一个有卡通画像装饰的黄色背包。',
    },
];

const roleInfo = {
    user: {
        name: 'User',
        avatar: '/assets/docs-icon.png',
    },
    assistant: {
        name: 'Assistant',
        avatar: '/assets/placeholder/logo.svg',
    },
    system: {
        name: 'System',
        avatar: '/assets/placeholder/logo.svg',
    },
};

const commonOuterStyle = {
    border: '1px solid var(--semi-color-border)',
    'border-radius': '16px',
    height: '400px',
};

let id = 0;
function getId() {
    return `id-${id++}`;
}
const uploadProps = { action: 'https://api.semi.design/upload' };

const formatTime = (createAt?: number) => {
    const date = new Date(createAt ?? Date.now());
    const hours = ('0' + date.getHours()).slice(-2);
    const minutes = ('0' + date.getMinutes()).slice(-2);
    return `${hours}:${minutes}`;
};

export default function ChatDemo() {
    const [title, setTitle] = createSignal('null');
    const [avatar, setAvatar] = createSignal('null');
    const [chats, setChats] = createSignal<Message[]>(defaultMessage);

    const customRenderAvatar = createMemo(() => {
        if (avatar() === 'custom') {
            return (props: RenderAvatarProps) => (
                <Avatar size="extra-small" shape="square" style={{ 'flex-shrink': '0' }}>
                    {props.role?.name}
                </Avatar>
            );
        }
        if (avatar() === 'null') {
            return () => null;
        }
        return undefined;
    });

    const customRenderTitle = createMemo(() => {
        if (title() === 'custom') {
            return (props: RenderTitleProps) => (
                <span class="title">
                    {props.role?.name}
                    <span class="time">{formatTime(props.message?.createAt)}</span>
                </span>
            );
        }
        if (title() === 'null') {
            return () => null;
        }
        return undefined;
    });

    const onMessageSend = () => {
        const newAssistantMessage: Message = {
            role: 'assistant',
            id: getId(),
            content: `This is a mock response`,
        };
        setTimeout(() => {
            setChats((list) => [...list, newAssistantMessage]);
        }, 200);
    };

    return (
        <>
            <span style={{ display: 'flex', 'flex-direction': 'column', 'row-gap': '8px', 'margin-bottom': '5px' }}>
                <span style={{ display: 'flex', 'align-items': 'center', 'column-gap': '10px' }}>
                    头像渲染模式
                    <RadioGroup onChange={(e) => setAvatar(e.target.value)} value={avatar()} type="button">
                        <Radio value="default">默认头像</Radio>
                        <Radio value="null">无头像</Radio>
                        <Radio value="custom">自定义头像</Radio>
                    </RadioGroup>
                </span>
                <span style={{ display: 'flex', 'align-items': 'center', 'column-gap': '10px' }}>
                    标题渲染模式
                    <RadioGroup onChange={(e) => setTitle(e.target.value)} 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(),
                }}
                style={commonOuterStyle}
                class="component-chat-demo-custom-render"
                chats={chats()}
                onChatsChange={(next) => setChats(next)}
                onMessageSend={onMessageSend}
                roleConfig={roleInfo}
                uploadProps={uploadProps}
            />
        </>
    );
}

鼠标移动到会话上,即可显示会话操作区,通过 renderChatBoxAction 自定义渲染操作区

System

Hello, I'm your AI assistant.

User

介绍一下 semi design

Assistant

Semi Design 是由抖音前端团队和MED产品设计团队设计、开发并维护的设计系统

代码
import { For, createSignal } from 'solid-js';
import { Chat, type ChatMessage as Message, type ChatRenderActionProps as RenderActionProps } from '@fibrous/semi-ai-solid';
import { IconForward, IconMoreStroked } from '@fibrous/semi-icons-solid';
import { Button, Dropdown, DropdownItem, DropdownMenu } from '@fibrous/semi-ui-solid';

const defaultMessage: Message[] = [
    {
        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: '/assets/docs-icon.png',
    },
    assistant: {
        name: 'Assistant',
        avatar: '/assets/placeholder/logo.svg',
    },
    system: {
        name: 'System',
        avatar: '/assets/placeholder/logo.svg',
    },
};

const commonOuterStyle = {
    border: '1px solid var(--semi-color-border)',
    'border-radius': '16px',
    height: '400px',
};

let id = 0;
function getId() {
    return `id-${id++}`;
}
const uploadProps = { action: 'https://api.semi.design/upload' };

const renderChatBoxAction = (props: RenderActionProps) => (
    <span class={props.class}>
        <For each={props.defaultActions}>{(action) => action()}</For>

        <Dropdown
            render={
                <DropdownMenu>
                    <DropdownItem icon={<IconForward />}>分享</DropdownItem>
                </DropdownMenu>
            }
            trigger="click"
            position="top"
        >
            {(trigger) => <Button {...trigger()} class="semi-chat-chatBox-action-btn" aria-label="更多" icon={<IconMoreStroked />} theme="borderless" type="tertiary" />}
        </Dropdown>
    </span>
);

export default function ChatDemo() {
    const [chats, setChats] = createSignal<Message[]>(defaultMessage);

    const onMessageSend = () => {
        const newAssistantMessage: Message = {
            role: 'assistant',
            id: getId(),
            content: `This is a mock response`,
        };
        setTimeout(() => {
            setChats((list) => [...list, newAssistantMessage]);
        }, 200);
    };

    return <Chat chatBoxRenderConfig={{ renderChatBoxAction }} style={commonOuterStyle} chats={chats()} onChatsChange={(next) => setChats(next)} onMessageSend={onMessageSend} roleConfig={roleInfo} uploadProps={uploadProps} />;
}

通过 renderChatBoxContent 自定义内容区域

Assistant
代码
import { For, Show, createSignal } from 'solid-js';
import { Chat, type ChatMessage as Message, type ChatRenderContentProps as RenderContentProps } from '@fibrous/semi-ai-solid';
import { IconChevronUp } from '@fibrous/semi-icons-solid';
import { Avatar, AvatarGroup, MarkdownRender } from '@fibrous/semi-ui-solid';

interface Source {
    avatar: string;
    url: string;
    title: string;
    subTitle: string;
    content: string;
}

const defaultMessage: Message[] = [
    {
        role: 'assistant',
        id: '3',
        createAt: 1715676751919,
        content: 'Semi Design 是由抖音前端团队,MED 产品设计团队设计、开发并维护的设计系统。它作为全面、易用、优质的现代应用 UI 解决方案,从字节跳动各业务线的复杂场景提炼而来,支撑近千计平台产品,服务内外部 10 万+ 用户。',
        source: [
            {
                avatar: '/assets/placeholder/logo.svg',
                url: '/zh-CN/start/introduction',
                title: 'semi Design',
                subTitle: 'Semi design website',
                content: 'Semi Design 是由抖音前端团队,MED 产品设计团队设计、开发并维护的设计系统。',
            },
            {
                avatar: '/assets/placeholder/logo.svg',
                url: '/dsm/landing',
                subTitle: 'Semi DSM website',
                title: 'Semi 设计系统',
                content: '从 Semi Design,到 Any Design 快速定义你的设计系统,并应用在设计稿和代码中',
            },
            {
                avatar: '/assets/placeholder/logo.svg',
                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: '/assets/docs-icon.png',
    },
    assistant: {
        name: 'Assistant',
        avatar: '/assets/placeholder/logo.svg',
    },
    system: {
        name: 'System',
        avatar: '/assets/placeholder/logo.svg',
    },
};

const commonOuterStyle = {
    border: '1px solid var(--semi-color-border)',
    'border-radius': '16px',
    height: '500px',
};

let id = 0;
function getId() {
    return `id-${id++}`;
}
const uploadProps = { action: 'https://api.semi.design/upload' };

const SourceCard = (props: { source: Source[] }) => {
    const [open, setOpen] = createSignal(true);

    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)',
                'border-radius': '16px',
                'box-sizing': 'border-box',
                'margin-bottom': '10px',
            }}
        >
            <button
                type="button"
                style={{
                    display: open() ? 'flex' : 'none',
                    width: 'fit-content',
                    'column-gap': '10px',
                    border: 'none',
                    background: 'var(--semi-color-tertiary-light-hover)',
                    'border-radius': '16px',
                    padding: '5px 10px',
                    cursor: 'pointer',
                    'font-family': 'inherit',
                    'font-size': '14px',
                    'line-height': 'inherit',
                    color: 'var(--semi-color-text-1)',
                }}
                onClick={() => setOpen(false)}
            >
                <span>基于{props.source.length}个搜索来源</span>
                <AvatarGroup size="extra-extra-small">
                    <For each={props.source}>{(source) => <Avatar src={source.avatar} alt="" />}</For>
                </AvatarGroup>
            </button>
            <button
                type="button"
                style={{
                    width: '100%',
                    height: '100%',
                    'box-sizing': 'border-box',
                    display: open() ? 'none' : 'flex',
                    'flex-direction': 'column',
                    border: 'none',
                    background: 'var(--semi-color-tertiary-light-hover)',
                    'border-radius': '16px',
                    padding: '12px',
                    'text-align': 'left',
                    'font-family': 'inherit',
                    'font-size': 'inherit',
                    'line-height': 'inherit',
                    color: 'inherit',
                }}
                onClick={() => setOpen(true)}
            >
                <span
                    style={{
                        display: 'flex',
                        'align-items': 'center',
                        'justify-content': 'space-between',
                        padding: '5px 10px',
                        'column-gap': '10px',
                        color: 'var(--semi-color-text-1)',
                    }}
                >
                    <span style={{ 'font-size': '14px', 'font-weight': '500' }}>Source</span>
                    <IconChevronUp />
                </span>
                <span style={{ display: 'flex', 'flex-wrap': 'wrap', gap: '10px', overflow: 'scroll', padding: '5px 10px' }}>
                    <For each={props.source}>
                        {(source) => (
                            <span
                                style={{
                                    display: 'flex',
                                    'flex-direction': 'column',
                                    'row-gap': '5px',
                                    'flex-basis': '150px',
                                    'flex-grow': '1',
                                    border: '1px solid var(--semi-color-border)',
                                    'border-radius': '12px',
                                    padding: '12px',
                                    'font-size': '12px',
                                }}
                            >
                                <span style={{ display: 'flex', 'column-gap': '5px', 'align-items': 'center' }}>
                                    <Avatar style={{ width: '16px', height: '16px', 'flex-shrink': '0' }} shape="square" src={source.avatar} alt="" />
                                    <span style={{ color: 'var(--semi-color-text-2)', 'text-overflow': 'ellipsis' }}>{source.title}</span>
                                </span>
                                <span
                                    style={{
                                        color: 'var(--semi-color-primary)',
                                        'font-size': '12px',
                                    }}
                                >
                                    {source.subTitle}
                                </span>
                                <span
                                    style={{
                                        display: '-webkit-box',
                                        '-webkit-box-orient': 'vertical',
                                        '-webkit-line-clamp': '3',
                                        'text-overflow': 'ellipsis',
                                        overflow: 'hidden',
                                        color: 'var(--semi-color-text-2)',
                                    }}
                                >
                                    {source.content}
                                </span>
                            </span>
                        )}
                    </For>
                </span>
            </button>
        </div>
    );
};

export default function ChatDemo() {
    const [chats, setChats] = createSignal<Message[]>(defaultMessage);

    const onMessageSend = () => {
        const newAssistantMessage: Message = {
            role: 'assistant',
            id: getId(),
            content: `This is a mock response`,
        };
        setTimeout(() => {
            setChats((list) => [...list, newAssistantMessage]);
        }, 200);
    };

    const renderChatBoxContent = (props: RenderContentProps) => (
        <div class={props.class}>
            <Show when={props.message?.source} keyed>
                {(source) => <SourceCard source={source} />}
            </Show>
            <MarkdownRender raw={typeof props.message?.content === 'string' ? props.message.content : ''} />
        </div>
    );

    return <Chat style={commonOuterStyle} chats={chats()} roleConfig={roleInfo} chatBoxRenderConfig={{ renderChatBoxContent }} onChatsChange={(next) => setChats(next)} onMessageSend={onMessageSend} uploadProps={uploadProps} />;
}

使用 renderFullChatBox 自定义渲染整个会话框

System

Hello, I'm your AI assistant.

介绍一下 semi design

Assistant

Semi Design 是由抖音前端团队和MED产品设计团队设计、开发并维护的设计系统

代码
import { Show, createSignal } from 'solid-js';
import { Chat, type ChatMessage as Message, type ChatRenderFullChatBoxProps as RenderFullChatBoxProps } from '@fibrous/semi-ai-solid';
import { Avatar } from '@fibrous/semi-ui-solid';

const defaultMessage: Message[] = [
    {
        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: '/assets/docs-icon.png',
    },
    assistant: {
        name: 'Assistant',
        avatar: '/assets/placeholder/logo.svg',
    },
    system: {
        name: 'System',
        avatar: '/assets/placeholder/logo.svg',
    },
};

const commonOuterStyle = {
    border: '1px solid var(--semi-color-border)',
    'border-radius': '16px',
    height: '400px',
};

let id = 0;
function getId() {
    return `id-${id++}`;
}
const uploadProps = { action: 'https://api.semi.design/upload' };

const titleStyle = {
    display: 'flex',
    'align-items': 'center',
    'justify-content': 'center',
    'column-gap': '10px',
    padding: '5px 0px',
    width: 'fit-content',
};

const renderFullChatBox = (props: RenderFullChatBoxProps) => (
    <div class={props.class}>
        <div
            style={{
                display: 'flex',
                'flex-direction': 'column',
                'row-gap': '4px',
                'align-items': props.message?.role === 'user' ? 'end' : '',
            }}
        >
            <Show when={props.message?.role !== 'user'}>
                <span style={titleStyle}>
                    <Avatar size="extra-small" shape="square" src={typeof props.role?.avatar === 'string' ? props.role.avatar : undefined} alt="" />
                    {props.defaultNodes?.title?.()}
                </span>
            </Show>
            <div style={{ width: 'fit-content' }}>{props.defaultNodes?.content?.()}</div>
            {props.defaultNodes?.action?.()}
        </div>
    </div>
);

export default function ChatDemo() {
    const [chats, setChats] = createSignal<Message[]>(defaultMessage);

    const onMessageSend = () => {
        const newAssistantMessage: Message = {
            role: 'assistant',
            id: getId(),
            content: `This is a mock response`,
        };
        setTimeout(() => {
            setChats((list) => [...list, newAssistantMessage]);
        }, 200);
    };

    return <Chat chatBoxRenderConfig={{ renderFullChatBox }} style={commonOuterStyle} chats={chats()} onChatsChange={(next) => setChats(next)} onMessageSend={onMessageSend} roleConfig={roleInfo} uploadProps={uploadProps} />;
}

自定义渲染输入框

可通过 renderInputArea 自定义渲染输入框,参数如下

export interface RenderInputAreaProps {
    /* 默认节点 */
    readonly defaultNode?: () => JSX.Element;
    /* 如果自定义输入框,发送消息时需调用 */
    onSend?: (content?: string, attachment?: FileItem[]) => void;
    /* 如果自定义清除上下文按钮,点击清除上下文时需调用 */
    onClear?: (e?: MouseEvent) => void;
    detailProps?: {
        /* 清除上下文按钮 */
        readonly clearContextNode?: () => JSX.Element;
        /* 上传按钮 */
        readonly uploadNode?: () => JSX.Element;
        /* 文本输入框 */
        readonly inputNode?: () => JSX.Element;
        /* 发送按钮 */
        readonly sendNode?: () => JSX.Element;
        /* 点击触发聚焦文本输入框的处理函数 */
        onClick?: () => void;
    };
}

节点字段(如 defaultNode、detailProps 里的 inputNode)是渲染函数,每次调用都会新建节点,只在插入处调用,例如 {props.detailProps?.inputNode?.()}。detailProps 的使用示例如下

function CustomInputRender(props) {
    return (
        <div
            style={{
                margin: '8px 16px',
                display: 'flex',
                'flex-direction': 'row',
                'align-items': 'flex-end',
                'border-radius': '16px',
                padding: '10px',
                border: '1px solid var(--semi-color-border)',
            }}
            onClick={() => props.detailProps?.onClick?.()}
        >
            {props.detailProps?.uploadNode?.()}
            {props.detailProps?.inputNode?.()}
            {props.detailProps?.sendNode?.()}
        </div>
    );
}

其他使用示例如下

System

Hello, I'm your AI assistant.

输入信息
代码
import { type JSX, createSignal } from 'solid-js';
import { Chat, type ChatMessage as Message, type ChatRenderInputAreaProps as RenderInputAreaProps } from '@fibrous/semi-ai-solid';
import { IconUpload } from '@fibrous/semi-icons-solid';
import { Button, Form, type FileItem, type FormApi, FormInput, FormUpload } from '@fibrous/semi-ui-solid';

interface InputValues {
    name?: string;
    file?: FileItem[];
}

const defaultMessage: Message[] = [
    {
        role: 'system',
        id: '1',
        createAt: 1715676751919,
        content: "Hello, I'm your AI assistant.",
    },
];

const roleInfo = {
    user: {
        name: 'User',
        avatar: '/assets/docs-icon.png',
    },
    assistant: {
        name: 'Assistant',
        avatar: '/assets/placeholder/logo.svg',
    },
    system: {
        name: 'System',
        avatar: '/assets/placeholder/logo.svg',
    },
};

const commonOuterStyle = {
    border: '1px solid var(--semi-color-border)',
    'border-radius': '16px',
    height: '500px',
};

let id = 0;
function getId() {
    return `id-${id++}`;
}
const uploadProps = { action: 'https://api.semi.design/upload' };

const inputStyle: JSX.CSSProperties = {
    display: 'flex',
    'flex-direction': 'column',
    border: '1px solid var(--semi-color-border)',
    margin: '8px 16px',
    'border-radius': '8px',
    padding: '8px',
};

function CustomInputRender(props: RenderInputAreaProps) {
    let api: FormApi<InputValues> | undefined;

    const onSubmit = () => {
        if (api) {
            const values = api.getValues();
            if ((values.name && values.name.length !== 0) || (values.file && values.file.length !== 0)) {
                props.onSend?.(values.name, values.file);
                api.reset();
            }
        }
    };

    return (
        <div style={inputStyle}>
            <Form getFormApi={(formApi) => (api = formApi)}>
                <strong>输入信息</strong>
                <FormInput field="name" label="名称(Input)" style={{ width: '250px' }} trigger="blur" />
                <FormUpload field="file" label="文档" action="https://api.semi.design/upload">
                    <Button icon={<IconUpload />} theme="light">
                        点击上传
                    </Button>
                </FormUpload>
            </Form>
            <Button style={{ width: 'fit-content' }} onClick={onSubmit}>
                提交
            </Button>
        </div>
    );
}

export default function ChatDemo() {
    const [chats, setChats] = createSignal<Message[]>(defaultMessage);

    const onMessageSend = () => {
        const newAssistantMessage: Message = {
            role: 'assistant',
            id: getId(),
            content: `This is a mock response`,
        };
        setTimeout(() => {
            setChats((list) => [...list, newAssistantMessage]);
        }, 200);
    };

    return <Chat renderInputArea={CustomInputRender} style={commonOuterStyle} chats={chats()} roleConfig={roleInfo} onChatsChange={(next) => setChats(next)} onMessageSend={onMessageSend} uploadProps={uploadProps} />;
}

提示信息

通过 hints 可设置提示区域内容,点击提示内容后,提示内容将作为新的用户消息加入对话,并触发 onHintClick 回调。

Assistant

Semi Design 是由抖音前端团队和MED产品设计团队设计、开发并维护的设计系统,你可以向我提问任何关于 Semi 的问题。

代码
import { createSignal } from 'solid-js';
import { Chat, type ChatMessage as Message } from '@fibrous/semi-ai-solid';

const defaultMessage: Message[] = [
    {
        role: 'assistant',
        id: '1',
        createAt: 1715676751919,
        content: 'Semi Design 是由抖音前端团队和MED产品设计团队设计、开发并维护的设计系统,你可以向我提问任何关于 Semi 的问题。',
    },
];

const hintsExample = ['告诉我更多', 'Semi Design 的组件有哪些?', '我能够通过 DSM 定制自己的主题吗?'];

const roleInfo = {
    user: {
        name: 'User',
        avatar: '/assets/docs-icon.png',
    },
    assistant: {
        name: 'Assistant',
        avatar: '/assets/placeholder/logo.svg',
    },
    system: {
        name: 'System',
        avatar: '/assets/placeholder/logo.svg',
    },
};

const commonOuterStyle = {
    border: '1px solid var(--semi-color-border)',
    'border-radius': '16px',
    height: '400px',
};

let id = 0;
function getId() {
    return `id-${id++}`;
}
const uploadProps = { action: 'https://api.semi.design/upload' };

export default function ChatDemo() {
    const [chats, setChats] = createSignal<Message[]>(defaultMessage);
    const [hints, setHints] = createSignal(hintsExample);

    const onHintClick = () => {
        setHints([]);
    };

    const onMessageSend = () => {
        const newAssistantMessage: Message = {
            role: 'assistant',
            id: getId(),
            createAt: Date.now(),
            content: '这是一条 mock 回复信息',
        };
        setTimeout(() => {
            setChats((list) => [...list, newAssistantMessage]);
        }, 200);
    };

    return <Chat hints={hints()} onHintClick={onHintClick} style={commonOuterStyle} chats={chats()} roleConfig={roleInfo} onChatsChange={(next) => setChats(next)} onMessageSend={onMessageSend} onClear={() => setHints([])} uploadProps={uploadProps} />;
}

自定义提示信息渲染

通过 renderHintBox 自定义提示区域内容, 参数如下

export interface RenderHintBoxProps {
    readonly content: string;
    index: number;
    onHintClick: () => void;
}

使用示例如下:

Assistant

Semi Design 是由抖音前端团队和MED产品设计团队设计、开发并维护的设计系统,你可以向我提问任何关于 Semi 的问题。

代码
import { type JSX, createSignal } from 'solid-js';
import { Chat, type ChatMessage as Message, type ChatRenderHintBoxProps as RenderHintBoxProps } from '@fibrous/semi-ai-solid';
import { IconArrowRight } from '@fibrous/semi-icons-solid';

const defaultMessage: Message[] = [
    {
        role: 'assistant',
        id: '1',
        createAt: 1715676751919,
        content: 'Semi Design 是由抖音前端团队和MED产品设计团队设计、开发并维护的设计系统,你可以向我提问任何关于 Semi 的问题。',
    },
];

const hintsExample = ['告诉我更多', 'Semi Design 的组件有哪些?', '我能够通过 DSM 定制自己的主题吗?'];

const roleInfo = {
    user: {
        name: 'User',
        avatar: '/assets/docs-icon.png',
    },
    assistant: {
        name: 'Assistant',
        avatar: '/assets/placeholder/logo.svg',
    },
    system: {
        name: 'System',
        avatar: '/assets/placeholder/logo.svg',
    },
};

const commonOuterStyle = {
    border: '1px solid var(--semi-color-border)',
    'border-radius': '16px',
    height: '400px',
};

let id = 0;
function getId() {
    return `id-${id++}`;
}
const uploadProps = { action: 'https://api.semi.design/upload' };

const commonHintStyle: JSX.CSSProperties = {
    border: '1px solid var(--semi-color-border)',
    padding: '10px',
    'border-radius': '10px',
    background: 'none',
    color: 'var( --semi-color-text-1)',
    display: 'flex',
    'justify-content': 'space-between',
    'align-items': 'center',
    'text-align': 'left',
    cursor: 'pointer',
    'font-family': 'inherit',
    'font-size': '14px',
    'line-height': 'inherit',
};

const renderHintBox = (props: RenderHintBoxProps) => (
    <button type="button" style={commonHintStyle} onClick={() => props.onHintClick()}>
        {props.content}
        <IconArrowRight style={{ 'margin-left': '10px' }} />
    </button>
);

export default function ChatDemo() {
    const [chats, setChats] = createSignal<Message[]>(defaultMessage);
    const [hints, setHints] = createSignal(hintsExample);

    const onHintClick = () => {
        setHints([]);
    };

    const onMessageSend = () => {
        const newAssistantMessage: Message = {
            role: 'assistant',
            id: getId(),
            createAt: Date.now(),
            content: '这是一条 mock 回复信息',
        };
        setTimeout(() => {
            setChats((list) => [...list, newAssistantMessage]);
        }, 200);
        setHints([]);
    };

    return <Chat renderHintBox={renderHintBox} hints={hints()} onHintClick={onHintClick} style={commonOuterStyle} chats={chats()} roleConfig={roleInfo} onChatsChange={(next) => setChats(next)} onMessageSend={onMessageSend} onClear={() => setHints([])} uploadProps={uploadProps} />;
}

API 参考

Props

属性说明类型默认值
align对话布局方式,可选 leftRight(左右分布)或 leftAlign(左对齐)。ChatAlign"leftRight"
bottomSlot输入框下方的节点。JSX.Element-
canSend发送按钮是否可用。通常由内部逻辑决定,设置后以此值为准。boolean-
chatBoxRenderConfig消息框各部分的自定义渲染函数,类型见 ChatBoxRenderConfig。ChatBoxRenderConfig-
chats对话列表,受控使用,配合 onChatsChange 回写。只有整体替换数组才视为变化,就地修改同一数组不会更新界面。Message[]-
customMarkDownComponents自定义 Markdown 渲染组件,键为元素名,透传给消息内容的 MarkdownRender。MarkdownRenderComponents-
enableUpload是否启用上传。布尔值同时控制粘贴、点击和拖拽上传;传对象时分别设置 pasteUpload、clickUpload、dragUpload,未设置的项为 true。boolean | EnableUploadProps-
escapeHtml是否转义用户消息中的 HTML 标签,防止被 Markdown 解析器当作 HTML 处理导致内容丢失。booleantrue
hintCls提示区最外层的类名。string-
hints提示信息列表。点击某项后,该项作为新的用户消息加入对话,并触发 onHintClick。string[]-
hintStyle提示区最外层的样式。JSX.CSSProperties | string-
inputBoxCls输入框的类名。string-
inputBoxStyle输入框的样式。JSX.CSSProperties | string-
markdownRenderProps传给消息内容所用 MarkdownRender 的属性,默认 format 为 md。raw 与 components 由 Chat 设置,自定义渲染组件使用 customMarkDownComponents。MarkdownRenderProps-
mode对话模式,可选 bubble、noBubble 或 userBubble。ChatMode"bubble"
onChatsChange对话列表变化时触发,参数为新的列表。Type(chats: Message[]) => void-
onClear清除上下文时触发。Type() => void-
onHintClick点击提示信息时触发,参数为提示文本。Type(hint: string) => void-
onInputChange输入内容或附件变化时触发,参数为 { inputValue, attachment }。Type(props: { inputValue: string; attachment: FileItem[] }) => void-
onMessageBadFeedback消息负向反馈时触发,参数为消息。Type(message?: Message) => void-
onMessageCopy复制消息时触发,参数为消息。Type(message?: Message) => void-
onMessageDelete删除消息时触发,参数为消息。Type(message?: Message) => void-
onMessageGoodFeedback消息正向反馈时触发,参数为消息。Type(message?: Message) => void-
onMessageReset重置消息时触发;由 resetMessage 方法触发时参数为 null。Type(message: Message | null) => void-
onMessageSend发送消息时触发,参数为文本内容和附件。Type(content: string, attachment?: FileItem[]) => void-
onStopGenerator点击停止生成按钮时触发,参数为鼠标事件。Type(e: MouseEvent) => void-
placeholder输入框占位符。string-
ref根元素,带有下方 Methods 中的方法。TypeRef<ChatElement>-
renderDivider自定义清除上下文后的分割线,参数为消息,返回节点。每条分割线调用一次,替换函数时重新调用。Type(message?: Message) => JSX.Element-
renderHintBox自定义提示项的渲染,参数为 content、index 和 onHintClick,返回节点。Type(props: RenderHintBoxProps) => JSX.Element-
renderInputArea自定义输入区域,参数为 defaultNode、onSend、onClear 和 detailProps,返回节点。defaultNode 与 detailProps 里的节点是渲染函数,在要放置的位置调用。Type(props: RenderInputAreaProps) => JSX.Element-
roleConfig角色信息,键为消息的 role。avatar 是图片 URL,其他头像内容使用 chatBoxRenderConfig.renderRoleAvatar 渲染。color 传给 Avatar。RoleConfig-
sendHotKey发送快捷键,可选 enter 或 shift+enter。enter 时单独按 Enter 发送,Shift + Enter 换行;shift+enter 相反。SendHotKey"enter"
showClearContext是否在输入框中显示清除上下文按钮。booleanfalse
showStopGenerate是否在最后一条消息生成中时显示停止生成按钮。生成中发送按钮不可用,点击停止按钮触发 onStopGenerator。booleanfalse
topSlot对话区域顶部的节点。JSX.Element-
uploadProps传给 Upload 的属性。fileList 与 defaultFileList 由 Chat 管理,onChange 由 Chat 处理,不会传给 Upload;children 作为上传按钮的自定义触发元素。TypeOmit<UploadProps, 'fileList' | 'defaultFileList'>-
uploadTipProps上传按钮提示 Tooltip 的属性,content 为提示文字。TooltipProps-

Methods

方法说明类型
clearContext在对话末尾追加分割线以清除上下文,并触发 onClear;末尾已是分割线时无效果。Type() => void
resetMessage将最后一条消息重置为空内容的 loading 状态,并触发 onMessageReset,参数为 null。Type() => void
scrollToBottom滚动到最底部,animation 为 true 时平滑滚动,否则直接跳转。Type(animation?: boolean) => void
sendMessage发送消息,不传 attachment 时消息的内容为字符串。Type(content: string, attachment?: FileItem[]) => void

class、style 与其他未声明的属性(如 data-*、aria-*)落在根元素 div 上,class 与组件自身的类名合并。根元素带有 aria-label 或 aria-labelledby 且没有传 role 时,自动加上 role="group"。onDragOver、onDragStart、onDragEnd 监听同样落在根元素上,Chat 自己的拖拽上传处理先于使用方的监听执行。

RoleConfig

属性说明类型默认值
user用户信息ChatMetadata-
assistant助手信息ChatMetadata-
system系统信息ChatMetadata-

Metadata

属性说明类型默认值
name名称string-
avatar头像图片 URL;其他头像内容使用 chatBoxRenderConfig.renderRoleAvatar 渲染string-
color头像背景色,同 Avatar 组件的 color 参数, 支持 amber、 blue、 cyan、 green、 grey、 indigo、 light-blue、 light-green、 lime、 orange、 pink、 purple、 red、 teal、 violet、 yellowAvatarColorgrey

Message

属性说明类型默认值
role角色string-
name名称string-
id唯一标识string-
content文本内容string | Content[]-
parentId父节点idstring-
createAt创建时间number-
status消息状态,可选值为 loading | incomplete | complete | errorstringcomplete

Content

属性说明类型默认值
type类型, 可选值text | image_url | file_urlstring-
text当类型为 text 时的内容数据string-
image_url当类型为 image_url 时的内容数据{ url: string }-
file_url当类型为 file_url 时的内容数据{ url: string; name: string; size: string; type: string }-

Accessibility

键盘和焦点

  • 输入框中按 Enter 或 Shift + Enter 发送消息,由 sendHotKey 决定:enter 时 Enter 发送、Shift + Enter 换行,shift+enter 相反。
  • 在输入框按 Shift + Tab 回到上传按钮。默认的上传按钮是原生按钮,Enter 或 Space 打开文件选择;设置了上传提示时,提示文字是该按钮的描述,按钮获得焦点时显示。
  • 停止生成按钮仅在生成中出现。生成结束、按钮卸载时,如果焦点在它上面,焦点回到输入框,不会落到页面开头。
  • 除最后一条已完成的消息外,消息操作栏平时隐藏,仍在 Tab 顺序中,焦点进入其中的按钮时显示。删除按钮打开的确认框按 Esc 关闭,焦点回到删除按钮;在 Modal 中只关闭确认框,Modal 保持打开。
  • 消息列表是带名称的 role="log" 区域,最后一条消息生成中时带 aria-busy="true",读屏在回复完成后再播报,不逐段朗读。

设计变量

下表是组件内部的 Sass 变量,不能覆写。定制样式请覆写 --semi-* CSS 变量(见 Design Token 页),或写不分层的 CSS 规则。
Sass 变量值用法
$radius-chat_chatBox_contentvar(--semi-border-radius-large)聊天框内容圆角
$radius-chat_inputBox_container16px输入框容器圆角
$radius-chat_attachment_imgvar(--semi-border-radius-medium)附件图片圆角
$radius-chat_attachment_filevar(--semi-border-radius-medium)附件文件圆角
$radius-chat_hint_itemvar(--semi-border-radius-large)提示条圆角
$radius-chat_chatBox_content_codevar(--semi-border-radius-large)代码块圆角
$radius-chat_chatBox_content_code_topSlot_copyvar(--semi-border-radius-large)代码块顶部复制按钮圆角
$radius-chat_dropArea16px拖拽上传区域圆角
$color-chat_action_content-bgvar(--semi-color-bg-0)返回按钮/停止生成内容按钮背景颜色
$color-chat_action_content-bordervar(--semi-color-border)返回按钮/停止生成按钮描边颜色
$color-chat_dividervar(--semi-color-text-2)分割线文字颜色
$color-chat_chatBox_titlevar(--semi-color-text-0)聊天框标题颜色
$color-chat_chatBox_action_iconvar(--semi-color-text-2)聊天框操作区域按钮图标颜色
$color-chat_chatBox_action_icon-hovervar(--semi-color-text-0)聊天框操作区域按钮图标hover颜色
$color-chat_chatBox_action-bg-hovertransparent聊天框操作区域按钮hover背景颜色
$color-chat_chatBox_content_textvar(--semi-color-text-0)聊天框内容文字颜色
$color-chat_chatBox_content_bgvar(--semi-color-fill-0)聊天框内容背景颜色
$color-chat_chatBox_content_user-bgvar(--semi-color-primary)聊天框内容用户背景颜色
$color-chat_chatBox_content_user-textvar(--semi-color-white)聊天框内容用户文字颜色
$color-chat_chatBox_content_error-bgvar(--semi-color-danger-hover)聊天框内容错误背景颜色
$color-chat_chatBox_content_error-textvar(--semi-color-white)聊天框内容错误文字颜色
$color-chat_inputBottom_clearButton_iconvar(--semi-color-text-2)清空按钮图标颜色
$color-chat_inputBottom_uploadButton_iconvar(--semi-color-text-0)上传按钮图标颜色
$color-chat_inputBottom_sendButton_icon-disablevar(--semi-color-primary-disabled)发送按钮禁用态图标颜色
$color-chat_inputBox_container-bordervar(--semi-color-border)输入框容器边框颜色
$color-chat_attachment_clear_iconvar(--semi-color-text-2)附件清除图标颜色
$color-chat_attachment_file-bgvar(--semi-color-fill-0)附件文件背景颜色
$color-chat_attachment_failvar(--semi-color-danger)附件上传失败的提示图标颜色
$color-chat_chatBox_user_attachment_file-bgvar(--semi-color-bg-0)用户聊天框附件文件背景颜色
$color-chat_chatBox_other_attachment_file-bgvar(--semi-color-fill-2)聊天框附件文件背景颜色
$color-chat_attachment_file_iconvar(--semi-color-text-2)附件文件图标颜色
$color-chat_attachment_file_titlevar(--semi-color-text-0)附件文件标题颜色
$color-chat_attachment_file_metadata_textvar(--semi-color-text-2)附件文件元数据文字颜色
$color-chat_hint_item-bordervar(--semi-color-border)提示条边框颜色
$color-chat_hint_item-bgtransparent提示条背景颜色
$color-chat_hint_item-bg-hovervar(--semi-color-fill-0)提示条hover背景颜色
$color-chat_hint_content_textvar(--semi-color-text-1)提示条文字颜色
$color-chat_hint_iconvar(--semi-color-text-2)提示条图标颜色
$color-chat_chatBox_loading-bgvar(--semi-color-text-0)聊天内容加载图标圆圈颜色
$color-chat_chatBox_code_topSlotvar(--semi-white)代码块顶部字体颜色
$color-chat_chatBox_code_topSlot-bgvar(--semi-grey-4)代码块顶部背景色
$color-chat_chatBox_code_topSlot_toCopy-bg-hovervar(--semi-grey-5)代码块顶部复制按钮hover背景色
$color-chat_chatBox_code_contentvar(--semi-color-bg-0)代码块内容背景色
$color-chat_action_content-bg-hovervar(--semi-color-tertiary-light-hover)返回按钮/停止生成按钮hover背景颜色
$color-chat_dropArea-bgrgb(from var(--semi-grey-2) r g b / 0.9)拖拽区域文字颜色
$color-chat_dropArea-bordervar(--semi-color-border)拖拽区域边框颜色
$spacing-chat_paddingY12pxchat组件上下内边距
$spacing-chat_container-paddingX16px消息框水平内边距
$spacing-chat_action_content-bottom0返回按钮/停止生成按钮底部边距
$spacing-chat_chatBox-marginY8px聊天框上下外边距
$spacing-chat_chatBox-columnGap12px聊天框内容列间距
$spacing-chat_chatBox_action-columnGap10px聊天框操作区域按钮列间距
$spacing-chat_chatBox_action-marginX10px聊天框操作区域左右外边距
$spacing-chat_chatBox_action_btn-padding0聊天框操作区域按钮内边距
$spacing-chat_chatBox_content-paddingY8px聊天框内容上下内边距
$spacing-chat_chatBox_content-paddingX12px聊天框内容左右内边距
$spacing-chat_inputBox-paddingTop8px输入框顶部内边距
$spacing-chat_inputBox-paddingBottom8px输入框底部内边距
$spacing-chat_inputBox-paddingX16px输入框左右内边距
$spacing-chat_inputBox_container-padding11px输入框容器内边距
$spacing-chat_inputBox_inner-columnGap4px输入框容器列间距
$spacing-chat_inputBox-marginY4px
$spacing-chat_attachment-columnGap10px附件列间距
$spacing-chat_attachment-RowGap5px附件行间距
$spacing-chat_attachment-marginX12px附件左右外边距
$spacing-chat_attachment_clear-top8px附件清除图标顶部间距
$spacing-chat_attachment_clear-right8px附件清除图标右内边距
$spacing-chat_attachment_file-columnGap5px文件附件列间距
$spacing-chat_attachment_file-padding5px文件附件内边距
$spacing-chat_chatBox_loading_item-gap15px聊天内容加载图标间距
$spacing-chat_divider-marginY12px分割线上下外边距
$spacing-chat_chatBox_content_attachment-marginY4px聊天框内容文件/图片上下外间距
$spacing-chat_chatBox_content_attachment-marginRight4px聊天框内容文件/图片上下右间距
$spacing-chat_chatBox_content_code_topSlot-paddingX5px聊天框代码块顶部上下内边距
$spacing-chat_chatBox_content_code_topSlot-paddingY8px聊天框代码块顶部左右内边距
$spacing-chat_chatBox_content_code_topSlot_copy-columnGap5px聊天框代码块顶部复制按钮列间距
$spacing-chat_chatBox_content_code_topSlot_copy-padding5px聊天框代码块顶部复制按钮列间距
$spacing-chat_chatBox_wrap8px聊天框外层间距
$spacing-chat_hint-rowGap10px提示条行间距
$spacing-chat_hint-marginY12px提示条容器上下外边距
$spacing-chat_hint-marginLeft34px提示条容器左外边距
$spacing-chat_hint_item-marginY8px提示条上下外边距
$spacing-chat_hint_item-marginX12px提示条左右外边距
$spacing-chat_hint_item-columnGap20px提示条内容列间距
$spacing-chat_chatBox_loading-item-marginX18px聊天内容加载图标中心圆圈左右外边距
$spacing-chat_chatBox_loading-item-marginY6px聊天内容加载图标中心圆圈上下外边距
$width-chat_backBottom_wrapper42px返回按钮宽度
$width-chat_action_content-border1px返回按钮/停止生成按钮描边宽度
$width-chat_inputBottom_clearButton48px清空按钮宽度
$width-chat_inputBottom_uploadButton32px上传按钮宽度
$width-chat_inputBottom_sendButton32px发送按钮宽度
$width-chat_inputBox_container-border1px输入框容器边框宽度
$width-chat_hint_item-border1px提示条边框宽度
$width-chat_chatBox_loading8px加载中单个圆圈图标宽度
$width-chat_attachment_file_title90px附件文件标题最大宽度
$width-chat_max800pxchat组件最大宽度
$width-chat_dropArea-border5px拖拽上传边框宽度
$width-chat_chatBox_content_code_topSlot_copy150px聊天框代码块顶部复制按钮最小宽度
$width-chat_chatBox_avatar24px聊天框头像宽度
$height-chat_action_stop42px停止生成按钮高度
$font-chat_divider-fontWeight$font-weight-regular分割线字重
$font-chat_divider-fontSize$font-size-small分割线字体大小
$font-chat_chatBox_title-lineHeight20px聊天框标题行高
$font-chat_chatBox_title-fontSize$font-size-header-6聊天框标题字体大小
$font-chat_chatBox_title-fontWeight$font-weight-regular聊天框标题字重
$font-chat_inputBottom_clearButton_icon-fontSize30px输入区清空上下文按钮图标大小
$font-chat_attachment_file_title-fontSize$font-size-header-6附件文件标题字体大小
$font-chat_attachment_file_metadata-fontSize$font-size-regular附件文件元数据字体大小
$font-chat_hint_content-fontSize$font-size-regular提示条文字大小
$font-chat_chatBox_code_topSlot12px代码块顶部字体大小
$font-chat_chatBox_code_topSlot-lineHeight16px代码块顶部区域字体行高
$font-chat_dropArea_text48px拖拽上传区域文字大小
$z-chat_dropArea10拖拽上传区域z-index
$z-chat_action1返回按钮/停止生成按钮z-index