UNPKG

@douyinfe/semi-ui

Version:

A modern, comprehensive, flexible design system and UI library. Connect DesignOps & DevOps. Quickly build beautiful React apps. Maintained by Douyin-fe team.

1,480 lines (1,302 loc) 68.9 kB
--- localeCode: zh-CN order: 102 category: Ai title: AIChatDialogue AI对话 icon: doc-aiDialogue width: 60% brief: 用户展示 AI 聊天中的对话信息 showNew: true --- ## 使用场景 AIChatDialogue 组件可搭配 AIChatInput 使用,实现更丰富的、功能覆盖更全面、定制更加便捷的 AI 会话场景。 组件消息格式以 OpenAI 的 [Response Object](https://platform.openai.com/docs/api-reference/responses/object) 为原型,默认支持 OpenAI 社区 [Response](https://platform.openai.com/docs/api-reference/responses/create) / [Chat Completion](https://platform.openai.com/docs/api-reference/chat/create) 格式标准,对 GPT-5、GPT-4o 系列模型的响应均支持开箱即用,详见[消息数据转换](/zh-CN/ai/aiChatDialogue#%E6%B6%88%E6%81%AF%E6%95%B0%E6%8D%AE%E8%BD%AC%E6%8D%A2)。 ## 代码演示 ### 如何引入 ```jsx import import { AIChatDialogue } from '@douyinfe/semi-ui'; ``` ### 基本用法 通过设置 `chats` 和 `onChatsChange` 实现基础对话显示和交互。 使用 `align` 属性可以设置对话的布局,支持左右分布(`leftRight`, 默认)和左对齐(`leftAlign`)。 ```jsx live=true dir="column" noInline=true import React, { useState, useCallback } from 'react'; import { AIChatDialogue, RadioGroup, Radio } from '@douyinfe/semi-ui'; const defaultMessages = [ { role: 'system', id: '1', createAt: 1715676751919, content: "Hello, I'm your AI assistant.", }, { role: 'user', id: '2', createAt: 1715676751919, content: "给一个 Semi Design 的 Button 组件的使用示例", }, { role: 'assistant', id: '3', createAt: 1715676751919, content: "以下是一个 Semi 代码的使用示例:\n\`\`\`jsx \nimport React from 'react';\nimport { Button } from '@douyinfe/semi-ui';\n\nconst MyComponent = () => {\n return (\n <Button>Click me</Button>\n );\n};\nexport default MyComponent;\n\`\`\`\n", } ]; const roleConfig = { user: { name: 'User', avatar: 'https://lf3-static.bytednsdoc.com/obj/eden-cn/ptlz_zlp/ljhwZthlaukjlkulzlp/docs-icon.png' }, assistant: { name: 'Assistant', avatar: 'https://lf3-static.bytednsdoc.com/obj/eden-cn/ptlz_zlp/ljhwZthlaukjlkulzlp/other/logo.png' }, system: { name: 'System', avatar: 'https://lf3-static.bytednsdoc.com/obj/eden-cn/ptlz_zlp/ljhwZthlaukjlkulzlp/other/logo.png' } }; function AlignAndMode () { const [messages, setMessage] = useState(defaultMessages); const [mode, setMode] = useState('bubble'); const [align, setAlign] = useState('leftRight'); const onAlignChange = useCallback((e) => { setAlign(e.target.value); }, []); const onModeChange = useCallback((e) => { setMode(e.target.value); }, []); const onChatsChange = useCallback((chats) => { setMessage(chats); }, []); return ( <> <span style={{ display: 'flex', flexDirection: 'column', rowGap: '8px' }}> <span style={{ display: 'flex', alignItems: 'center', columnGap: '10px' }}> 模式 <RadioGroup onChange={onModeChange} value={mode} type={"button"}> <Radio value={'bubble'}>气泡</Radio> <Radio value={'noBubble'}>非气泡</Radio> <Radio value={'userBubble'}>用户会话气泡</Radio> </RadioGroup> </span> <span style={{ display: 'flex', alignItems: 'center', columnGap: '10px' }}> 会话布局方式 <RadioGroup onChange={onAlignChange} value={align} type={"button"}> <Radio value={'leftRight'}>左右分布</Radio> <Radio value={'leftAlign'}>左对齐</Radio> </RadioGroup> </span> </span> <div style={{ border: '1px solid var(--semi-color-border)', borderRadius: 12, marginTop: 10, padding: 20 }}> <AIChatDialogue key={align + mode} align={align} mode={mode} chats={messages} roleConfig={roleConfig} onChatsChange={onChatsChange} /> </div> </> ); } render(AlignAndMode); ``` ### 消息状态 chats 类型为 `Message[]`, `Message` 包含对话的各种信息,如角色 `role`、内容 `content`、状态 `status` 、唯一标识 `id`、创建时间 `createdAt` 等,具体见 [Message](#Message)。其中 status 和 [Response API Status](https://platform.openai.com/docs/api-reference/responses/object#responses/object-status) 相同,存在 6 种状态,对应 3 种官方样式(成功 / 请求中 / 失败)。 ```jsx live=true dir="column" noInline=true import React, { useState, useCallback } from 'react'; import { AIChatDialogue } from '@douyinfe/semi-ui'; const defaultMessages = [ { role: 'assistant', id: '1', createAt: 1715676751919, content: "请求成功", // 默认的 status 为 completed }, { id: 'loading', role: 'assistant', status: 'in_progress' // 状态展示同 queued、incomplete }, { role: 'assistant', id: 'error', content: '请求错误', status: 'failed' // 状态展示同 cancelled } ]; const roleConfig = { user: { name: 'User', avatar: 'https://lf3-static.bytednsdoc.com/obj/eden-cn/ptlz_zlp/ljhwZthlaukjlkulzlp/docs-icon.png' }, assistant: { name: 'Assistant', avatar: 'https://lf3-static.bytednsdoc.com/obj/eden-cn/ptlz_zlp/ljhwZthlaukjlkulzlp/other/logo.png' }, system: { name: 'System', avatar: 'https://lf3-static.bytednsdoc.com/obj/eden-cn/ptlz_zlp/ljhwZthlaukjlkulzlp/other/logo.png' } }; function StatusDemo () { const [messages, setMessage] = useState(defaultMessages); const onChatsChange = useCallback((chats) => { setMessage(chats); }, []); return ( <AIChatDialogue chats={messages} roleConfig={roleConfig} onChatsChange={onChatsChange} /> ); } render(StatusDemo); ``` ### 消息展示 消息内容展示的类型为 [ContentItem[]](https://platform.openai.com/docs/api-reference/responses/list#responses/list-data),支持文本 `text`、文件 `file`、图片 `image`、代码 `code`、思考块 `reasoning`、参考来源 `annotation`、工具调用 `tool call` 等消息块的展示,同时提供 `AIChatDialogue.Step` 组件用于步骤等信息的分步展示。 ```jsx live=true dir="column" noInline=true import React, { useState, useCallback } from 'react'; import { AIChatDialogue } from '@douyinfe/semi-ui'; import { IconSearchStroked, IconCodeStroked, IconBriefStroked } from '@douyinfe/semi-icons'; const defaultMessages = [ { role: 'assistant', id: '1', createAt: 1715676751919, content: '普通文本', }, { id: '2', role: 'user', content: [ { type: 'message', content: [ { type: 'input_text', text: '帮我生成类似的图片', }, { type: 'input_image', image_url: 'https://lf3-static.bytednsdoc.com/obj/eden-cn/ptlz_zlp/ljhwZthlaukjlkulzlp/root-web-sites/edit-bag.jpeg', file_id: 'demo-file-id' }, { type: 'input_text', text: '以下是文件展示', }, { type: 'input_file', file_url: 'https://www.semi.pdf', filename: 'semi.pdf', size: '100KB', }, { type: 'input_file', file_url: 'https://www.semi.json', filename: 'semi.json', size: '100KB', }, { type: 'input_file', file_url: 'https://www.semi.docx', filename: 'semi.docx', size: '100KB', } ], }, ], status: 'completed', }, { id: '3', role: 'assistant', content: [ { type: 'reasoning', status: 'completed', summary: [ { 'type': 'summary_text', 'text': '\n我需要思考并回答用户关于什么是 Semi 组件库的问题...' } ], }, { type: 'message', content: [ { type: 'output_text', text: 'Semi Design 是由抖音前端团队和MED产品设计团队设计、开发并维护的设计系统。' } ], status: 'completed', }, { id: 'fc_12345xyz', call_id: 'call_12345xyz', type: 'function_call', name: 'get_weather', status: 'completed', arguments: '{\'location\':\'Paris, France\'}' }, { type: 'message', content: [ { type: 'output_text', text: '恭喜你,你已经掌握了 semi design 的所有知识!', annotations: [ { title: 'semi.design', url: 'https://semi.design/', detail: 'semi design page', logo: 'https://lf3-static.bytednsdoc.com/obj/eden-cn/ptlz_zlp/ljhwZthlaukjlkulzlp/other/logo.png' }, { title: 'semi.design', url: 'https://semi.design/', detail: 'semi design page', logo: 'https://lf3-static.bytednsdoc.com/obj/eden-cn/ptlz_zlp/ljhwZthlaukjlkulzlp/other/logo.png' }, ] } ] }, { type: 'plan', content: [ { summary: '创建一份全面的北京旅游攻略,包含景点、住宿、交通、美食和实用旅行建议', steps: [ { summary: '搜索北京旅游景点介绍及门票信息', description: '正在搜索: 北京旅游景点介绍及门票信息', type: 'search', }, { summary: '读取指定文件的指定行内容', description: '正在创建文档: 北京旅游攻略', type: 'docs', }, { summary: '创建包含北京旅游攻略的文件', description: '正在创建代码文件: beijing_travel_guide.html', type: 'code', }, ], statues: 'completed' }, { summary: '总结北京旅游攻略的创建成果并呈现给用户', steps: [] } ], } ], status: 'completed', }, ]; const roleConfig = { user: { name: 'User', avatar: 'https://lf3-static.bytednsdoc.com/obj/eden-cn/ptlz_zlp/ljhwZthlaukjlkulzlp/docs-icon.png' }, assistant: { name: 'Assistant', avatar: 'https://lf3-static.bytednsdoc.com/obj/eden-cn/ptlz_zlp/ljhwZthlaukjlkulzlp/other/logo.png' }, system: { name: 'System', avatar: 'https://lf3-static.bytednsdoc.com/obj/eden-cn/ptlz_zlp/ljhwZthlaukjlkulzlp/other/logo.png' } }; function AllTypeMessageDemo () { const [messages, setMessage] = useState(defaultMessages); const onChatsChange = useCallback((chats) => { setMessage(chats); }, []); const mapStep = useCallback((steps) => { if (!steps) { return []; } return steps.map((item) => { let icon = null; switch (item.type) { case 'search': icon = <IconSearchStroked />; break; case 'docs': icon = <IconBriefStroked />; break; case 'code': icon = <IconCodeStroked />; break; } return { summary: item.summary, description: item.description, icon: icon, }; }); }, []); const customRender = useMemo(() => ({ 'plan': (item) => { // plan 为用户自定义类型 let steps = item.content.map((item) => { return { summary: item.summary, actions: mapStep(item.steps), status: 'completed' }; }); return <AIChatDialogue.Step steps={steps} />; }, }), [mapStep]); return ( <AIChatDialogue chats={messages} roleConfig={roleConfig} onChatsChange={onChatsChange} renderDialogueContentItem={customRender} /> ); } render(AllTypeMessageDemo); ``` ### 引用 通过 `references` 字段定义当前消息引用的文件或者文本, `showReference` 配置当前消息是否显示可被引用样式, `onReferenceClick` 配置引用按钮点击回调。具体和 AIChatInput 的搭配使用见 [AI 组件构建对话](/zh-CN/ai/aiComponent#AI%20%E7%BB%84%E4%BB%B6%E6%9E%84%E5%BB%BA%E5%AF%B9%E8%AF%9D) ```jsx live=true dir="column" noInline=true import React, { useState, useCallback } from 'react'; import { AIChatDialogue } from '@douyinfe/semi-ui'; const defaultMessages = [ { id: '1', role: 'user', content: '当前消息为引用 demo 的示例', references: [ { id: '1', type: 'text', content: '测试文本,这里是一段很长的文字,这里是一段很长的文字,这里是一段很长的文字,这里是一段很长的文字,这里是一段很长的文字,这里是一段很长的文字,这里是一段很长的文字,这里是一段很长的文字,这里是一段很长的文字,这里是一段很长的文字', }, { id: '2', name: '飞书文档.docx', }, { id: '3', name: 'Music.mp4', }, { id: '4', name: 'Image.jpeg', url: 'https://lf3-static.bytednsdoc.com/obj/eden-cn/ptlz_zlp/ljhwZthlaukjlkulzlp/root-web-sites/Resso.png' }, { id: '5', name: 'code.json', } ] } ]; const roleConfig = { user: { name: 'User', avatar: 'https://lf3-static.bytednsdoc.com/obj/eden-cn/ptlz_zlp/ljhwZthlaukjlkulzlp/docs-icon.png' }, assistant: { name: 'Assistant', avatar: 'https://lf3-static.bytednsdoc.com/obj/eden-cn/ptlz_zlp/ljhwZthlaukjlkulzlp/other/logo.png' }, system: { name: 'System', avatar: 'https://lf3-static.bytednsdoc.com/obj/eden-cn/ptlz_zlp/ljhwZthlaukjlkulzlp/other/logo.png' } }; function ReferencesDemo () { const [messages, setMessage] = useState(defaultMessages); const onChatsChange = useCallback((chats) => { setMessage(chats); }, []); const onReferenceClick = () => { console.log('You click the reference button!'); }; return ( <AIChatDialogue chats={messages} roleConfig={roleConfig} onChatsChange={onChatsChange} showReference onReferenceClick={onReferenceClick} /> ); } render(ReferencesDemo); ``` ### 选择 ```jsx live=true dir="column" noInline=true import React, { useState, useCallback } from 'react'; import { AIChatDialogue, RadioGroup, Radio } from '@douyinfe/semi-ui'; const defaultMessages = [ { role: 'system', id: '1', createAt: 1715676751919, content: "Hello, I'm your AI assistant.", }, { role: 'user', id: '2', createAt: 1715676751919, content: "给一个 Semi Design 的 Button 组件的使用示例", }, { role: 'assistant', id: '3', createAt: 1715676751919, content: "以下是一个 Semi 代码的使用示例:\n\`\`\`jsx \nimport React from 'react';\nimport { Button } from '@douyinfe/semi-ui';\n\nconst MyComponent = () => {\n return (\n <Button>Click me</Button>\n );\n};\nexport default MyComponent;\n\`\`\`\n", } ]; const roleConfig = { user: { name: 'User', avatar: 'https://lf3-static.bytednsdoc.com/obj/eden-cn/ptlz_zlp/ljhwZthlaukjlkulzlp/docs-icon.png' }, assistant: { name: 'Assistant', avatar: 'https://lf3-static.bytednsdoc.com/obj/eden-cn/ptlz_zlp/ljhwZthlaukjlkulzlp/other/logo.png' }, system: { name: 'System', avatar: 'https://lf3-static.bytednsdoc.com/obj/eden-cn/ptlz_zlp/ljhwZthlaukjlkulzlp/other/logo.png' } }; function SelectingDemo () { const ref = useRef(null); const [messages, setMessage] = useState(defaultMessages); const [align, setAlign] = useState('leftRight'); const [select, setSelect] = useState(true); const [selection, setSelection] = useState('allSelect'); useEffect(() => { ref.current.selectAll(); }, []); const onSelectChange = useCallback((e) => { setSelect(e.target.value); }, []); const onSelectionChange = useCallback((e) => { if (e.target.value === 'allSelect') { ref.current.selectAll(); } else { ref.current.deselectAll(); } setSelection(e.target.value); }, []); const onSelect = useCallback((selectionId) => { console.log('onSelect', selectionId); }, []); const onAlignChange = useCallback((e) => { setAlign(e.target.value); }, []); const onChatsChange = useCallback((chats) => { setMessage(chats); }, []); return ( <div> <span style={{ display: 'flex', flexDirection: 'column', rowGap: '8px' }}> <span style={{ display: 'flex', alignItems: 'center', columnGap: '10px' }}> 会话布局方式 <RadioGroup onChange={onAlignChange} value={align} type={"button"}> <Radio value={'leftRight'}>左右分布</Radio> <Radio value={'leftAlign'}>左对齐</Radio> </RadioGroup> </span> <span style={{ display: 'flex', alignItems: 'center', columnGap: '10px' }}> 是否开启选择 <RadioGroup onChange={onSelectChange} value={select} type={"button"}> <Radio value={true}>开启</Radio> <Radio value={false}>关闭</Radio> </RadioGroup> </span> <span style={{ display: 'flex', alignItems: 'center', columnGap: '10px' }}> 选择方式 <RadioGroup onChange={onSelectionChange} value={selection} type={"button"}> <Radio value={'allSelect'}>全选</Radio> <Radio value={'cancelSelect'}>取消全选</Radio> </RadioGroup> </span> </span> <div style={{ border: '1px solid var(--semi-color-border)', borderRadius: 12, marginTop: 10, padding: 20 }}> <AIChatDialogue ref={ref} align={align} mode="bubble" chats={messages} selecting={select} onSelect={onSelect} roleConfig={roleConfig} /> </div> </div> ); } render(SelectingDemo); ``` <!-- todo --> <!-- ### 编辑消息 --> <!-- ```jsx live=true dir="column" noInline=true ``` --> ### 提示 通过 `hints` 可设置提示区域内容, 点击提示内容后,提示内容将成为新的用户输入内容,并触发 `onHintClick` 回调。 ```jsx live=true dir="column" import React, { useState, useCallback } from 'react'; import { AIChatDialogue } from '@douyinfe/semi-ui'; () => { const defaultMessages = [ { role: 'assistant', id: '1', createAt: 1715676751919, content: 'Semi Design 是由抖音前端团队和MED产品设计团队设计、开发并维护的设计系统,你可以向我提问任何关于 Semi 的问题。', } ]; const hintsExample = [ "Semi 组件库有哪些常用组件?", "能否展示一个使用 Semi 组件库构建的页面示例?", "Semi 组件库有官方文档吗?", ]; const roleConfig = { user: { name: 'User', avatar: 'https://lf3-static.bytednsdoc.com/obj/eden-cn/ptlz_zlp/ljhwZthlaukjlkulzlp/docs-icon.png' }, assistant: { name: 'Assistant', avatar: 'https://lf3-static.bytednsdoc.com/obj/eden-cn/ptlz_zlp/ljhwZthlaukjlkulzlp/other/logo.png' }, system: { name: 'System', avatar: 'https://lf3-static.bytednsdoc.com/obj/eden-cn/ptlz_zlp/ljhwZthlaukjlkulzlp/other/logo.png' } }; const [messages, setMessage] = useState(defaultMessages); const [hints, setHints] = useState(hintsExample); const onChatsChange = useCallback((chats) => { console.log('onChatsChange', chats); setMessage(chats); }, []); const onHintClick = useCallback((hint) => { setHints([]); }, []); return ( <AIChatDialogue align="leftRight" mode="bubble" chats={messages} roleConfig={roleConfig} onChatsChange={onChatsChange} hints={hints} onHintClick={onHintClick} /> ); }; ``` ### 自定义渲染提示 通过 `renderHintBox` 可自定义提示区域内容, 参数如下 ```ts type renderHintBox = (props: {content: string; index: number,onHintClick: () => void}) => React.ReactNode; ``` ```jsx live=true dir="column" import React, { useState, useCallback } from 'react'; import { AIChatDialogue } from '@douyinfe/semi-ui'; import { IconArrowRight } from '@douyinfe/semi-icons'; () => { const defaultMessages = [ { role: 'assistant', id: '1', createAt: 1715676751919, content: 'Semi Design 是由抖音前端团队和MED产品设计团队设计、开发并维护的设计系统,你可以向我提问任何关于 Semi 的问题。', } ]; const hintsExample = [ "Semi 组件库有哪些常用组件?", "能否展示一个使用 Semi 组件库构建的页面示例?", "Semi 组件库有官方文档吗?", ]; const roleConfig = { user: { name: 'User', avatar: 'https://lf3-static.bytednsdoc.com/obj/eden-cn/ptlz_zlp/ljhwZthlaukjlkulzlp/docs-icon.png' }, assistant: { name: 'Assistant', avatar: 'https://lf3-static.bytednsdoc.com/obj/eden-cn/ptlz_zlp/ljhwZthlaukjlkulzlp/other/logo.png' }, system: { name: 'System', avatar: 'https://lf3-static.bytednsdoc.com/obj/eden-cn/ptlz_zlp/ljhwZthlaukjlkulzlp/other/logo.png' } }; const [messages, setMessage] = useState(defaultMessages); const [hints, setHints] = useState(hintsExample); const onChatsChange = useCallback((chats) => { console.log('onChatsChange', chats); setMessage(chats); }, []); const onHintClick = useCallback((hint) => { setHints([]); }, []); const commonHintStyle = useMemo(() => ({ border: '1px solid var(--semi-color-border)', padding: '10px', borderRadius: '10px', color: 'var( --semi-color-text-1)', display: 'flex', justifyContent: 'space-between', alignItems: 'center', cursor: 'pointer', fontSize: '14px' }), []); const renderHintBox = useCallback((props) => { const { content, onHintClick, index } = props; return ( <div style={commonHintStyle} onClick={onHintClick} key={index}> {content} <IconArrowRight style={{ marginLeft: 10 }}>click me</IconArrowRight> </div> ); }, []); return ( <AIChatDialogue align="leftRight" mode="bubble" chats={messages} roleConfig={roleConfig} onChatsChange={onChatsChange} hints={hints} onHintClick={onHintClick} renderHintBox={renderHintBox} /> ); }; ``` ### 自定义渲染会话框 通过 `chatBoxRenderConfig` 传入自定义渲染配置, chatBoxRenderConfig 类型如下 ```ts export interface RenderTitleProps { message?: Message; role?: Metadata; defaultTitle?: ReactNode } export interface RenderAvatarProps { message?: Message; role?: Metadata, defaultAvatar?: ReactNode } export interface RenderContentProps { message?: Message; role?: Metadata | Map<string, Metadata>; defaultContent?: ReactNode | ReactNode[]; className?: string; } export interface DefaultActionNodeObj { copyNode: ReactNode; likeNode: ReactNode; dislikeNode: ReactNode; resetNode: ReactNode; moreNode: ReactNode; } export interface RenderActionProps { message?: Message; defaultActions?: ReactNode | ReactNode[]; className: string; defaultActionsObj?: DefaultActionNodeObj; }; export interface FullDialogueNodes { avatar?: ReactNode; title?: ReactNode; content?: ReactNode; action?: ReactNode } export interface RenderFullDialogueProps { message?: Message; role?: Metadata; defaultNodes?: FullDialogueNodes; className: string } export interface DialogueRenderConfig { /* 自定义渲染标题 */ renderDialogueAction?: (props: RenderActionProps) => ReactNode; /* 自定义渲染头像 */ renderDialogueAvatar?: (props: RenderAvatarProps) => ReactNode; /* 自定义渲染内容区域 */ renderDialogueContent?: (props: RenderContentProps) => ReactNode; /* 自定义渲染消息操作栏 */ renderDialogueTitle?: (props: RenderTitleProps) => ReactNode; /* 完全自定义渲染整个聊天框 */ renderFullDialogue?: (props: RenderFullDialogueProps) => ReactNode } ``` 自定义渲染头像和标题,可通过 `renderChatBoxAvatar` 和 `renderChatBoxTitle` 实现。 ```jsx live=true dir="column" import React, { useState, useCallback } from 'react'; import { AIChatDialogue, Avatar } from '@douyinfe/semi-ui'; () => { const defaultMessages = [ { role: 'system', id: '1', createAt: 1715676751919, content: "Hello, I'm your AI assistant.", }, { role: 'user', id: '2', createAt: 1715676751919, content: "给一个 Semi Design 的 Button 组件的使用示例", }, { role: 'assistant', id: '3', createAt: 1715676751919, content: "以下是一个 Semi 代码的使用示例:\n\`\`\`jsx \nimport React from 'react';\nimport { Button } from '@douyinfe/semi-ui';\n\nconst MyComponent = () => {\n return (\n <Button>Click me</Button>\n );\n};\nexport default MyComponent;\n\`\`\`\n", } ]; const roleConfig = { user: { name: 'User', avatar: 'https://lf3-static.bytednsdoc.com/obj/eden-cn/ptlz_zlp/ljhwZthlaukjlkulzlp/docs-icon.png' }, assistant: { name: 'Assistant', avatar: 'https://lf3-static.bytednsdoc.com/obj/eden-cn/ptlz_zlp/ljhwZthlaukjlkulzlp/other/logo.png' }, system: { name: 'System', avatar: 'https://lf3-static.bytednsdoc.com/obj/eden-cn/ptlz_zlp/ljhwZthlaukjlkulzlp/other/logo.png' } }; const [messages, setMessage] = useState(defaultMessages); const onChatsChange = useCallback((chats) => { setMessage(chats); }, []); const renderConfig = { renderDialogueTitle: (props) => { return <div className="semi-ai-chat-dialogue-title">My-{props.role.name}</div>; }, renderDialogueAvatar: (props) => { return <Avatar src={props.role.avatar} size="extra-small" shape="square" > </Avatar>; }, renderDialogueAction: (props) => { return <div className={props.className}>{props.defaultActions[0]}</div>; }, }; return ( <AIChatDialogue align="leftRight" mode="bubble" chats={messages} roleConfig={roleConfig} onChatsChange={onChatsChange} dialogueRenderConfig={renderConfig} /> ); }; ``` ### 自定义渲染消息内容 通过 `renderDialogueContentItem` 按照消息类型返回内容渲染,用法如下 ```jsx live=true dir="column" noInline=true import React, { useState, useCallback } from 'react'; import { AIChatDialogue, MarkdownRender } from '@douyinfe/semi-ui'; const defaultMessages = [ { id: '1', role: 'user', content: '你好', }, { id: '2', role: 'assistant', content: '你好呀,请问有什么可以帮助你的吗~', status: 'completed', }, { id: '3', role: 'user', content: [ { type: 'message', role: 'user', content: [ { type: 'input_text', text: '帮我生成类似的图片', }, { type: 'input_image', image_url: 'https://lf3-static.bytednsdoc.com/obj/eden-cn/ptlz_zlp/ljhwZthlaukjlkulzlp/root-web-sites/edit-bag.jpeg', file_id: 'demo-file-id' }, { type: 'input_image', image_url: 'https://lf3-static.bytednsdoc.com/obj/eden-cn/ptlz_zlp/ljhwZthlaukjlkulzlp/root-web-sites/edit-bag.jpeg', file_id: 'demo-file-id' } ], }], }, { id: '4', role: 'assistant', content: [{ type: "reasoning", summary: [ { "type": "summary_text", "text": "\n用户问需要我帮助他生成类似图片,我需要先分析图片内容,然后生成类似的图片..." } ], annotations: [ { title: 'semi.design', url: 'https://semi.design/', detail: 'semi design page', logo: 'https://lf3-static.bytednsdoc.com/obj/eden-cn/ptlz_zlp/ljhwZthlaukjlkulzlp/other/logo.png' }, { title: 'semi.design', url: 'https://semi.design/', detail: 'semi design page', logo: 'https://lf3-static.bytednsdoc.com/obj/eden-cn/ptlz_zlp/ljhwZthlaukjlkulzlp/other/logo.png' }, ], status: "completed" }, { type: 'function_call', name: 'create_travel_guide', arguments: "{\n\"city\": \"北京\"\n}", status: 'completed', } ], status: 'completed', }]; const roleConfig = { user: { name: 'User', avatar: 'https://lf3-static.bytednsdoc.com/obj/eden-cn/ptlz_zlp/ljhwZthlaukjlkulzlp/docs-icon.png' }, assistant: { name: 'Assistant', avatar: 'https://lf3-static.bytednsdoc.com/obj/eden-cn/ptlz_zlp/ljhwZthlaukjlkulzlp/other/logo.png' }, system: { name: 'System', avatar: 'https://lf3-static.bytednsdoc.com/obj/eden-cn/ptlz_zlp/ljhwZthlaukjlkulzlp/other/logo.png' } }; function CustomRender () { const [messages, setMessage] = useState(defaultMessages); const onChatsChange = useCallback((chats) => { setMessage(chats); }, []); const userTextStyle = { backgroundColor: 'var(--semi-color-fill-1)', color: 'var(--semi-color-text-0)', borderRadius: '25px', padding: '6px 16px', }; const assistantStyle = { color: 'var(--semi-color-text-0)', padding: '6px 16px', }; const functionCallStyle = { backgroundColor: 'var(--semi-color-fill-1)', padding: '6px 16px', borderRadius: '25px', }; const customRenderReasoningContent = useCallback((props) => { return <React.Fragment> <AIChatDialogue.Annotation annotation={props.annotations} description={'参考资料'} maxCount={3} onClick={(e) => { e && e.stopPropagation(); Toast.success('Ready to open the sidebar!'); }} /> <div style={{ marginTop: '8px' }}> <MarkdownRender format='md' raw={props.summary[0].text} {...props.markdownRenderProps} /> </div> </React.Fragment>; }, []); const customRender = { "function_call": { "create_travel_guide": (item) => { return <div style={functionCallStyle}>Function Tool Call: {item.name} {item.arguments}</div>; } }, "input_text": (item, message) => { if (message.role === 'user') { return <div style={userTextStyle} className={'userTextStyle'}>{item.text}</div>; } return <div style={assistantStyle}>{item.text}</div>; }, "reasoning": (item) => { return <AIChatDialogue.Reasoning {...item} customRenderer={customRenderReasoningContent} />; }, "default": (item, message) => { if (message.role === 'user') { return <div style={userTextStyle} className={'userTextStyle'}>{item}</div>; } else { return <div style={assistantStyle}>{item}</div>; } } }; return ( <AIChatDialogue chats={messages} roleConfig={roleConfig} onChatsChange={onChatsChange} renderDialogueContentItem={customRender} /> ); } render(CustomRender); ``` ### 消息数据转换 当前组件的对话消息以 OpenAI 的 [Response Object](https://platform.openai.com/docs/api-reference/responses/object) 为原型,为了支持用户更好地无缝集成 [Chat Completion API](https://platform.openai.com/docs/api-reference/chat/create) 和 [Response API](https://platform.openai.com/docs/api-reference/responses/create),我们提供了四种 `Adapter` 转换函数,用户可直接使用该函数转换 API 的返回结果,得到可直接用于消息展示的数据,提供两种 `Adapter` 用于将 `ChatInput` 组件的数据处理成适配于 `Response API` 的 `input Message` 或者 `Chat Completion API` 中的 `Input Message` 格式。 ```ts // 将 Chat Completion API 返回的数据转换为 Chat Dialogue 中的 Message 格式 function chatCompletionToMessage(chatCompletion: ChatCompletion): Message[] // 将 Chat Completion API 流式返回数据转换为 Chat Dialogue 中的 Message 格式 function streamingChatCompletionToMessage(chatCompletionChunks: ChatCompletionChunk[], state?: StreamingChatState): { messages: Message[]; state?: StreamingChatState } // 将 Response API 返回的数据转换为 Chat Dialogue 中的 Message 格式 function responseToMessage(response: Response): Message // 将 Response API 返回流式数据转换为 Chat Dialogue 中的 Message 格式 function streamingResponseToMessage(chunks: ResponseChunk[], prevState: StreamingResponseState): { messages: Message[]; state?: StreamingResponseState } // 将 Chat Input 数据转换为 Chat Dialogue 中的 Message 格式,(同 Response API Input Message 格式) function chatInputToMessage(inputContent: MessageContent): Message // 将 Chat Input 数据转换为 Chat Completion API 中的 Input Message 格式 function chatInputToChatCompletion(inputContent: MessageContent): ChatCompletionInput ``` 比如,当用户使用 [Chat Completion API](https://platform.openai.com/docs/api-reference/chat/create) 接口返回非流式数据时,可以通过 `chatCompletionToMessage` 函数将 Chat Completion Object 转换为 Dialogue Message 消息块格式。注意,因为 `Chat Completion API` 可以通过 `n` 来控制每条输入消息生成多少个结果所以该函数的返回值为数组。(注意:如果 n > 1,用户需要自行决定将哪条数据添加到 message 中展示) ```jsx live=true noInline=true dir="column" import React, { useState, useCallback } from 'react'; import { AIChatDialogue } from '@douyinfe/semi-ui'; const roleConfig = { user: { name: 'User', avatar: 'https://lf3-static.bytednsdoc.com/obj/eden-cn/ptlz_zlp/ljhwZthlaukjlkulzlp/docs-icon.png' }, assistant: { name: 'Assistant', avatar: 'https://lf3-static.bytednsdoc.com/obj/eden-cn/ptlz_zlp/ljhwZthlaukjlkulzlp/other/logo.png' }, system: { name: 'System', avatar: 'https://lf3-static.bytednsdoc.com/obj/eden-cn/ptlz_zlp/ljhwZthlaukjlkulzlp/other/logo.png' } }; function ChatCompletionToMessageDemo() { const [messages, setMessage] = useState([]); const onChatsChange = useCallback((chats) => { setMessage(chats); }, []); useEffect(() => { const message = chatCompletionToMessage(CHAT_COMPLETION_DATA); setMessage([...message]); }, []); return ( <AIChatDialogue align="leftRight" mode="bubble" chats={messages} roleConfig={roleConfig} onChatsChange={onChatsChange} /> ); }; const CHAT_COMPLETION_DATA = { "id": "chatcmpl-B9MBs8CjcvOU2jLn4n570S5qMJKcT", "object": "chat.completion", "created": 1741569952, "model": "gpt-4.1-2025-04-14", "choices": [ { "index": 0, "message": { "role": "assistant", "content": "Hello! How can I assist you today?", "refusal": null, "annotations": [], "tool_calls": [ { "id": "call_abc123", "type": "function", "function": { "name": "get_current_weather", "arguments": "{\n\"location\": \"Boston, MA\"\n}" } } ] }, "logprobs": null, "finish_reason": "stop" } ], // ... }; render(ChatCompletionToMessageDemo); ``` 比如,当用户使用 [Chat Completion API](https://platform.openai.com/docs/api-reference/chat/create) 接口返回流式数据时,可以通过 `streamingChatCompletionToMessage` 函数将 Chat Completion Chunk Object List 转换为 Dialogue Message 消息块格式。 ```jsx live=true noInline=true dir="column" import React, { useState, useCallback } from 'react'; import { AIChatDialogue } from '@douyinfe/semi-ui'; const roleConfig = { user: { name: 'User', avatar: 'https://lf3-static.bytednsdoc.com/obj/eden-cn/ptlz_zlp/ljhwZthlaukjlkulzlp/docs-icon.png' }, assistant: { name: 'Assistant', avatar: 'https://lf3-static.bytednsdoc.com/obj/eden-cn/ptlz_zlp/ljhwZthlaukjlkulzlp/other/logo.png' }, system: { name: 'System', avatar: 'https://lf3-static.bytednsdoc.com/obj/eden-cn/ptlz_zlp/ljhwZthlaukjlkulzlp/other/logo.png' } }; function StreamingChatCompletionToMessageDemo() { const [messages, setMessage] = useState([]); const [state, setState] = useState(); const onChatsChange = useCallback((chats) => { setMessage(chats); }, []); useEffect(() => { const total = STREAMING_CHAT_COMPLETION_DATA.length; let i = 1; const timer = setInterval(() => { if (i > total) { clearInterval(timer); return; } const slice = STREAMING_CHAT_COMPLETION_DATA.slice(0, i); const { messages: partialMessages, state: nextState } = streamingChatCompletionToMessage(slice, state); setState(nextState); const merged = [...messages, partialMessages[0]]; setMessage(merged); i += 1; }, 100); return () => clearInterval(timer); }, []); return ( <AIChatDialogue align="leftRight" mode="bubble" chats={messages} roleConfig={roleConfig} onChatsChange={onChatsChange} /> ); }; const STREAMING_CHAT_COMPLETION_DATA = [ { "id": "chatcmpl-COjljxurV5GKrRUsg1wd7mIyQCiiT", "object": "chat.completion.chunk", "created": 1760011843, "model": "o3-mini-2025-01-31", "service_tier": "default", "system_fingerprint": "fp_6c43dcef8c", "choices": [{ "index": 0, "delta": { "role": "assistant", "content": "", "refusal": null }, "finish_reason": null }], "obfuscation": "ahPqlzj6DD" }, { "id": "chatcmpl-COjljxurV5GKrRUsg1wd7mIyQCiiT", "object": "chat.completion.chunk", "created": 1760011843, "model": "o3-mini-2025-01-31", "service_tier": "default", "system_fingerprint": "fp_6c43dcef8c", "choices": [{ "index": 0, "delta": { "content": "" }, "finish_reason": null }], "obfuscation": "i2PXRIwvc3D" }, // index 0: 输出文本增量 { "id": "chatcmpl-COjljxurV5GKrRUsg1wd7mIyQCiiT", "object": "chat.completion.chunk", "created": 1760011843, "model": "o3-mini-2025-01-31", "service_tier": "default", "system_fingerprint": "fp_6c43dcef8c", "choices": [{ "index": 0, "delta": { "content": "我正在使用 " }, "finish_reason": null }], "obfuscation": "3sslO5QylW" }, { "id": "chatcmpl-COjljxurV5GKrRUsg1wd7mIyQCiiT", "object": "chat.completion.chunk", "created": 1760011843, "model": "o3-mini-2025-01-31", "service_tier": "default", "system_fingerprint": "fp_6c43dcef8c", "choices": [{ "index": 0, "delta": { "content": "streamingChatCompletionToMessage" }, "finish_reason": null }], "obfuscation": "3sslO5QylW" }, // index 1: 工具调用增量(function_call / tool_calls) { "id": "chatcmpl-COjljxurV5GKrRUsg1wd7mIyQCiiT", "object": "chat.completion.chunk", "created": 1760011845, "model": "o3-mini-2025-01-31", "service_tier": "default", "system_fingerprint": "fp_6c43dcef8c", "choices": [{ "index": 1, "delta": { "tool_calls": [{ "id": "call_1", "function": { "name": "searchWeather", "arguments": "{\"city\":\"北京\"" } }] }, "finish_reason": null }], "obfuscation": "T1" }, { "id": "chatcmpl-COjljxurV5GKrRUsg1wd7mIyQCiiT", "object": "chat.completion.chunk", "created": 1760011846, "model": "o3-mini-2025-01-31", "service_tier": "default", "system_fingerprint": "fp_6c43dcef8c", "choices": [{ "index": 1, "delta": { "tool_calls": [{ "id": "call_1", "function": { "name": null, "arguments": ",\"day\":\"today\"}" } }] }, "finish_reason": null }], "obfuscation": "T2" }, { "id": "chatcmpl-COjljxurV5GKrRUsg1wd7mIyQCiiT", "object": "chat.completion.chunk", "created": 1760011844, "model": "o3-mini-2025-01-31", "service_tier": "default", "system_fingerprint": "fp_6c43dcef8c", "choices": [{ "index": 0, "delta": { "content": " 转换 Chat Completion Chunks" }, "finish_reason": null }], "obfuscation": "X1" }, { "id": "chatcmpl-COjljxurV5GKrRUsg1wd7mIyQCiiT", "object": "chat.completion.chunk", "created": 1760011844, "model": "o3-mini-2025-01-31", "service_tier": "default", "system_fingerprint": "fp_6c43dcef8c", "choices": [{ "index": 0, "delta": { "content": " 🥳" }, "finish_reason": null }], "obfuscation": "X2" }, // 终止信号 { "id": "chatcmpl-COjljxurV5GKrRUsg1wd7mIyQCiiT", "object": "chat.completion.chunk", "created": 1760011843, "model": "o3-mini-2025-01-31", "service_tier": "default", "system_fingerprint": "fp_6c43dcef8c", "choices": [{ "index": 0, "delta": {}, "finish_reason": "stop" }], "obfuscation": "n13SLf" }, { "id": "chatcmpl-COjljxurV5GKrRUsg1wd7mIyQCiiT", "object": "chat.completion.chunk", "created": 1760011843, "model": "o3-mini-2025-01-31", "service_tier": "default", "system_fingerprint": "fp_6c43dcef8c", "choices": [{ "index": 1, "delta": {}, "finish_reason": "stop" }], "obfuscation": "jt9rDb" } ]; render(StreamingChatCompletionToMessageDemo); ``` 当用户使用 [Response API](https://platform.openai.com/docs/api-reference/responses/create) 接口返回非流式数据时,可以通过 `responseToMessage` 函数将 Response Object 转换为 Dialogue Message 消息块格式。 ```jsx live=true noInline=true dir="column" import React, { useState, useCallback } from 'react'; import { AIChatDialogue } from '@douyinfe/semi-ui'; const roleConfig = { user: { name: 'User', avatar: 'https://lf3-static.bytednsdoc.com/obj/eden-cn/ptlz_zlp/ljhwZthlaukjlkulzlp/docs-icon.png' }, assistant: { name: 'Assistant', avatar: 'https://lf3-static.bytednsdoc.com/obj/eden-cn/ptlz_zlp/ljhwZthlaukjlkulzlp/other/logo.png' }, system: { name: 'System', avatar: 'https://lf3-static.bytednsdoc.com/obj/eden-cn/ptlz_zlp/ljhwZthlaukjlkulzlp/other/logo.png' } }; function ResponseToMessageDemo() { const [messages, setMessage] = useState([]); const onChatsChange = useCallback((chats) => { setMessage(chats); }, []); useEffect(() => { const responseMessage = responseToMessage(RESPONSE_DATA); setMessage([responseMessage]); }, []); return ( <AIChatDialogue align="leftRight" mode="bubble" chats={messages} roleConfig={roleConfig} onChatsChange={onChatsChange} /> ); }; const RESPONSE_DATA = { "id": "resp_67ccd3a9da748190baa7f1570fe91ac604becb25c45c1d41", "object": "response", "created_at": 1741476777, "status": "completed", "error": null, "incomplete_details": null, "instructions": null, "max_output_tokens": null, "model": "gpt-4o-2024-08-06", "output": [ { "id": "rs_6876cf02e0bc8192b74af0fb64b715ff06fa2fcced15a5ac", "type": "reasoning", "status": "completed", "summary": [ { "type": "summary_text", "text": "**用户询问什么是 Semi Design** 用户问 “Semi Design”需整合多源信息。首先发现抖音的 Semi Design 是设计系统,支持多平台且含 Design Token 和代码转换工具。印度 Semi Design 专注半导体培训,但用户可能更关注抖音案例。其他结果涉及半定制设计,但关联性较低。需确认是否有其他解释,但当前信息已覆盖主要维度。虽然继续推理可能提高完备性,但现阶段已足够支撑答案,可以开始输出给用户。" } ] }, { "type": "message", "id": "msg_67ccd3acc8d48190a77525dc6de64b4104becb25c45c1d41", "status": "completed", "role": "assistant", "content": [ { "type": "output_text", "text": "Semi Design 是由抖音前端团队和MED产品设计团队设计、开发并维护的设计系统", "annotations": [ { "title": 'Semi Design', "url": 'https://semi.design/zh-CN/start/getting-started', "detail": 'Semi Design 快速开始', "logo": 'https://lf3-static.bytednsdoc.com/obj/eden-cn/ptlz_zlp/ljhwZthlaukjlkulzlp/docs-icon.png' }, { "title": 'Semi Design', "url": 'https://semi.design/zh-CN/start/getting-started', "detail": 'Semi Design 快速开始', "logo": 'https://lf3-static.bytednsdoc.com/obj/eden-cn/ptlz_zlp/ljhwZthlaukjlkulzlp/docs-icon.png' }, { "title": 'Semi Design', "url": 'https://semi.design/zh-CN/start/getting-started', "detail": 'Semi Design 快速开始', "logo": 'https://lf3-static.bytednsdoc.com/obj/eden-cn/ptlz_zlp/ljhwZthlaukjlkulzlp/docs-icon.png' }, ] } ] }, { "id": "fc_12345xyz", "call_id": "call_12345xyz", "type": "function_call", "name": "get_semi_page", "status": "completed", "arguments": "{\"pageName\":\"AIChatDialogue\"}" }, ], // ... }; render(ResponseToMessageDemo); ``` 当用户使用 [Response API](https://platform.openai.com/docs/api-reference/responses/create) 接口返回流式数据时,可以通过 `streamingResponseToMessage` 函数将 Response Chunk Object List 转换为 Dialogue Message 消息块格式。 ```jsx live=true noInline=true dir="column" import React, { useState, useCallback } from 'react'; import { AIChatDialogue } from '@douyinfe/semi-ui'; const roleConfig = { user: { name: 'User', avatar: 'https://lf3-static.bytednsdoc.com/obj/eden-cn/ptlz_zlp/ljhwZthlaukjlkulzlp/docs-icon.png' }, assistant: { name: 'Assistant', avatar: 'https://lf3-static.bytednsdoc.com/obj/eden-cn/ptlz_zlp/ljhwZthlaukjlkulzlp/other/logo.png' }, system: { name: 'System', avatar: 'https://lf3-static.bytednsdoc.com/obj/eden-cn/ptlz_zlp/ljhwZthlaukjlkulzlp/other/logo.png' } }; const FIXED_SHUFFLED_INDICES = [ 0, // sequence_number: 0 1, // sequence_number: 1 2, // sequence_number: 2 3, // sequence_number: 3 4, // sequence_number: 4 6, // sequence_number: 6 (块5被跳过 / chunk 5 skipped) 6, // sequence_number: 6 (块6重复到达) 7, // sequence_number: 7 5, // sequence_number: 5 (块5延迟到达 / chunk 5 arrives late) 8, // sequence_number: 8 9, // sequence_number: 9 10, // sequence_number: 10 11, // sequence_number: 11 12, // sequence_number: 12 13, // sequence_number: 13 14, // sequence_number: 14 15, // sequence_number: 15 16, // sequence_number: 16 ]; function StreamingResponseToMessageDemo() { const [messages, setMessage] = useState([]); const [currentState, setCurrentState] = useState(null); const [currentLength, setCurrentLength] = useState(0); const onChatsChange = useCallback((chats) => { setMessage(chats); }, []); useEffect(() => { if (currentLength > FIXED_SHUFFLED_INDICES.length) { return; } const timer = setTimeout(() => { if (currentLength === 0) { setCurrentLength(1); return; } const currentIndices = FIXED_SHUFFLED_INDICES.slice(0, currentLength); const currentChunks = currentIndices.map(index => REASONING_CHUNKS[index]); const result = streamingResponseToMessage(currentChunks, currentState); if (result) { const { message: responseMessage, nextState } = result; if (responseMessage) { setMessage([responseMessage]); // 更新状态供下次使用 / Update state for next iteration setCurrentState(nextState);